From 9f94ce1d0dfd99b361558c2196ee30b9300dfdac Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Mon, 4 Aug 2025 15:43:47 +0200 Subject: [PATCH] Made Chunks faster Moreeee Speeeedddddd --- pumpkin-world/benches/chunk.rs | 26 ++- pumpkin-world/src/biome/mod.rs | 21 ++- pumpkin-world/src/biome/multi_noise.rs | 20 ++- .../src/generation/implementation/mod.rs | 15 ++ pumpkin-world/src/generation/proto_chunk.rs | 162 ++++++++++++------ pumpkin-world/src/generation/surface/mod.rs | 13 +- pumpkin-world/src/level.rs | 155 +++++++++-------- pumpkin-world/src/lib.rs | 36 +++- 8 files changed, 298 insertions(+), 150 deletions(-) diff --git a/pumpkin-world/benches/chunk.rs b/pumpkin-world/benches/chunk.rs index 800d7d708..f33fef1cf 100644 --- a/pumpkin-world/benches/chunk.rs +++ b/pumpkin-world/benches/chunk.rs @@ -3,7 +3,7 @@ use pumpkin_data::noise_router::OVERWORLD_BASE_NOISE_ROUTER; use pumpkin_world::{ GENERATION_SETTINGS, GeneratorSetting, GlobalRandomConfig, ProtoNoiseRouters, bench_create_and_populate_biome, bench_create_and_populate_noise, - bench_create_and_populate_noise_with_surface, + bench_create_and_populate_noise_with_surface, generation::proto_chunk::TerrainCache, }; fn bench_terrain_gen(c: &mut Criterion) { @@ -13,13 +13,31 @@ fn bench_terrain_gen(c: &mut Criterion) { let surface_config = GENERATION_SETTINGS .get(&GeneratorSetting::Overworld) .unwrap(); + let terrain_cache = TerrainCache::from_random(&random_config); + let default_block = surface_config.default_block.get_state(); c.bench_function("overworld biome", |b| { - b.iter(|| bench_create_and_populate_biome(&base_router, &random_config, surface_config)); + b.iter(|| { + bench_create_and_populate_biome( + &base_router, + &random_config, + surface_config, + &terrain_cache, + default_block, + ) + }); }); c.bench_function("overworld noise", |b| { - b.iter(|| bench_create_and_populate_noise(&base_router, &random_config, surface_config)); + b.iter(|| { + bench_create_and_populate_noise( + &base_router, + &random_config, + surface_config, + &terrain_cache, + default_block, + ) + }); }); c.bench_function("overworld surface", |b| { @@ -28,6 +46,8 @@ fn bench_terrain_gen(c: &mut Criterion) { &base_router, &random_config, surface_config, + &terrain_cache, + default_block, ) }); }); diff --git a/pumpkin-world/src/biome/mod.rs b/pumpkin-world/src/biome/mod.rs index 0b609b7f2..df3a27deb 100644 --- a/pumpkin-world/src/biome/mod.rs +++ b/pumpkin-world/src/biome/mod.rs @@ -64,9 +64,12 @@ mod test { GENERATION_SETTINGS, GeneratorSetting, GlobalRandomConfig, ProtoChunk, chunk::palette::BIOME_NETWORK_MAX_BITS, dimension::Dimension, - generation::noise_router::{ - multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, - proto_noise_router::ProtoNoiseRouters, + generation::{ + noise_router::{ + multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, + proto_noise_router::ProtoNoiseRouters, + }, + proto_chunk::TerrainCache, }, }; @@ -108,11 +111,19 @@ mod test { let surface_settings = GENERATION_SETTINGS .get(&GeneratorSetting::Overworld) .unwrap(); + let terrain_cache = TerrainCache::from_random(&random_config); + let default_block = surface_settings.default_block.get_state(); for data in expected_data.into_iter() { let chunk_pos = Vector2::new(data.x, data.z); - let mut chunk = - ProtoChunk::new(chunk_pos, &noise_router, &random_config, surface_settings); + let mut chunk = ProtoChunk::new( + chunk_pos, + &noise_router, + &random_config, + surface_settings, + &terrain_cache, + default_block, + ); chunk.populate_biomes(Dimension::Overworld); for (biome_x, biome_y, biome_z, biome_id) in data.data { diff --git a/pumpkin-world/src/biome/multi_noise.rs b/pumpkin-world/src/biome/multi_noise.rs index 9c62efdee..cf3dfbb07 100644 --- a/pumpkin-world/src/biome/multi_noise.rs +++ b/pumpkin-world/src/biome/multi_noise.rs @@ -40,9 +40,12 @@ mod test { GENERATION_SETTINGS, GeneratorSetting, GlobalRandomConfig, ProtoChunk, biome::{BiomeSupplier, MultiNoiseBiomeSupplier}, dimension::Dimension, - generation::noise_router::{ - multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, - proto_noise_router::ProtoNoiseRouters, + generation::{ + noise_router::{ + multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, + proto_noise_router::ProtoNoiseRouters, + }, + proto_chunk::TerrainCache, }, }; @@ -61,8 +64,15 @@ mod test { let surface_config = GENERATION_SETTINGS .get(&GeneratorSetting::Overworld) .unwrap(); - - let mut chunk = ProtoChunk::new(chunk_pos, &noise_rounter, &random_config, surface_config); + let terrain_cache = TerrainCache::from_random(&random_config); + let mut chunk = ProtoChunk::new( + chunk_pos, + &noise_rounter, + &random_config, + surface_config, + &terrain_cache, + surface_config.default_block.get_state(), + ); for (x, y, z, tem, hum, con, ero, dep, wei) in expected_data.into_iter() { let point = chunk.multi_noise_sampler.sample(x, y, z); diff --git a/pumpkin-world/src/generation/implementation/mod.rs b/pumpkin-world/src/generation/implementation/mod.rs index 1c9977e3f..093ef59b7 100644 --- a/pumpkin-world/src/generation/implementation/mod.rs +++ b/pumpkin-world/src/generation/implementation/mod.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; +use pumpkin_data::BlockState; use pumpkin_data::noise_router::{ END_BASE_NOISE_ROUTER, NETHER_BASE_NOISE_ROUTER, OVERWORLD_BASE_NOISE_ROUTER, }; @@ -11,6 +12,7 @@ use super::{ settings::gen_settings_from_dimension, }; use crate::chunk::format::LightContainer; +use crate::generation::proto_chunk::TerrainCache; use crate::level::Level; use crate::world::BlockRegistryExt; use crate::{chunk::ChunkLight, dimension::Dimension}; @@ -40,11 +42,16 @@ pub struct VanillaGenerator { random_config: GlobalRandomConfig, base_router: ProtoNoiseRouters, dimension: Dimension, + + terrain_cache: TerrainCache, + + default_block: &'static BlockState, } impl GeneratorInit for VanillaGenerator { fn new(seed: Seed, dimension: Dimension) -> Self { let random_config = GlobalRandomConfig::new(seed.0, false); + // TODO: The generation settings contains (part of?) the noise routers too; do we keep the separate or // use only the generation settings? let base = match dimension { @@ -52,11 +59,17 @@ impl GeneratorInit for VanillaGenerator { Dimension::Nether => NETHER_BASE_NOISE_ROUTER, Dimension::End => END_BASE_NOISE_ROUTER, }; + let terrain_cache = TerrainCache::from_random(&random_config); + let generation_settings = gen_settings_from_dimension(&dimension); + + let default_block = generation_settings.default_block.get_state(); let base_router = ProtoNoiseRouters::generate(&base, &random_config); Self { random_config, base_router, dimension, + terrain_cache, + default_block, } } } @@ -83,6 +96,8 @@ impl WorldGenerator for VanillaGenerator { &self.base_router, &self.random_config, generation_settings, + &self.terrain_cache, + self.default_block, ); proto_chunk.populate_biomes(self.dimension); proto_chunk.populate_noise(); diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index f36842b5a..7f46bb168 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -8,9 +8,10 @@ use pumpkin_data::{ use pumpkin_util::{ HeightMap, math::{position::BlockPos, vector2::Vector2, vector3::Vector3}, - random::{RandomGenerator, RandomImpl, get_decorator_seed, xoroshiro128::Xoroshiro}, + random::{RandomGenerator, get_decorator_seed, xoroshiro128::Xoroshiro}, }; +use crate::generation::noise::perlin::DoublePerlinNoiseSampler; use crate::{ BlockStateId, biome::{BiomeSupplier, MultiNoiseBiomeSupplier, end::TheEndBiomeSupplier, hash_seed}, @@ -102,6 +103,7 @@ impl FluidLevelSamplerImpl for StandardChunkFluidLevelSampler { pub struct ProtoChunk<'a> { pub chunk_pos: Vector2, pub noise_sampler: ChunkNoiseGenerator<'a>, + pub terrain_cache: &'a TerrainCache, // TODO: These can technically go to an even higher level and we can reuse them across chunks pub multi_noise_sampler: MultiNoiseSampler<'a>, pub surface_height_estimate_sampler: SurfaceHeightEstimateSampler<'a>, @@ -122,12 +124,35 @@ pub struct ProtoChunk<'a> { // may want to use chunk status } +pub struct TerrainCache { + pub terrain_builder: SurfaceTerrainBuilder, + pub surface_noise: DoublePerlinNoiseSampler, + pub secondary_noise: DoublePerlinNoiseSampler, +} + +impl TerrainCache { + pub fn from_random(random_config: &GlobalRandomConfig) -> Self { + let random = &random_config.base_random_deriver; + let mut noise_builder = DoublePerlinNoiseBuilder::new(random_config); + let terrain_builder = SurfaceTerrainBuilder::new(&mut noise_builder, random); + let surface_noise = noise_builder.get_noise_sampler_for_id("surface"); + let secondary_noise = noise_builder.get_noise_sampler_for_id("surface_secondary"); + Self { + terrain_builder, + surface_noise, + secondary_noise, + } + } +} + impl<'a> ProtoChunk<'a> { pub fn new( chunk_pos: Vector2, base_router: &'a ProtoNoiseRouters, random_config: &'a GlobalRandomConfig, settings: &'a GenerationSettings, + terrain_cache: &'a TerrainCache, + default_block: &'static BlockState, ) -> Self { let generation_shape = &settings.shape; @@ -184,11 +209,11 @@ impl<'a> ProtoChunk<'a> { let surface_height_estimate_sampler = SurfaceHeightEstimateSampler::generate(&base_router.surface_estimator, &surface_config); - let default_block = settings.default_block.get_state(); let default_heightmap = vec![i16::MIN; CHUNK_AREA].into_boxed_slice(); Self { chunk_pos, settings, + terrain_cache, default_block, random_config, noise_sampler: sampler, @@ -463,61 +488,68 @@ impl<'a> ProtoChunk<'a> { pub fn populate_noise(&mut self) { let horizontal_cell_block_count = self.noise_sampler.horizontal_cell_block_count(); let vertical_cell_block_count = self.noise_sampler.vertical_cell_block_count(); - let horizontal_cells = CHUNK_DIM / horizontal_cell_block_count; let min_y = self.noise_sampler.min_y(); let minimum_cell_y = min_y / vertical_cell_block_count as i8; let cell_height = self.noise_sampler.height() / vertical_cell_block_count as u16; + let start_block_x = self.start_block_x(); + let start_block_z = self.start_block_z(); + let start_cell_x = self.start_cell_x(); + let start_cell_z = self.start_cell_z(); + // TODO: Block state updates when we implement those self.noise_sampler.sample_start_density(); for cell_x in 0..horizontal_cells { self.noise_sampler.sample_end_density(cell_x); let sample_start_x = - (self.start_cell_x() + cell_x as i32) * horizontal_cell_block_count as i32; + (start_cell_x + cell_x as i32) * horizontal_cell_block_count as i32; for cell_z in 0..horizontal_cells { + let sample_start_z = + (start_cell_z + cell_z as i32) * horizontal_cell_block_count as i32; + for cell_y in (0..cell_height).rev() { self.noise_sampler .on_sampled_cell_corners(cell_x, cell_y, cell_z); let sample_start_y = (minimum_cell_y as i32 + cell_y as i32) * vertical_cell_block_count as i32; - let sample_start_z = - (self.start_cell_z() + cell_z as i32) * horizontal_cell_block_count as i32; + + let block_y_base = sample_start_y; + let delta_y_step = 1.0 / vertical_cell_block_count as f64; + for local_y in (0..vertical_cell_block_count).rev() { - let block_y = (minimum_cell_y as i32 + cell_y as i32) - * vertical_cell_block_count as i32 - + local_y as i32; - let delta_y = local_y as f64 / vertical_cell_block_count as f64; + let block_y = block_y_base + local_y as i32; + let delta_y = local_y as f64 * delta_y_step; self.noise_sampler.interpolate_y(delta_y); + let block_x_base = + start_block_x + cell_x as i32 * horizontal_cell_block_count as i32; + let delta_x_step = 1.0 / horizontal_cell_block_count as f64; + for local_x in 0..horizontal_cell_block_count { - let block_x = self.start_block_x() - + cell_x as i32 * horizontal_cell_block_count as i32 - + local_x as i32; - let delta_x = local_x as f64 / horizontal_cell_block_count as f64; + let block_x = block_x_base + local_x as i32; + let delta_x = local_x as f64 * delta_x_step; self.noise_sampler.interpolate_x(delta_x); + let block_z_base = + start_block_z + cell_z as i32 * horizontal_cell_block_count as i32; + let delta_z_step = 1.0 / horizontal_cell_block_count as f64; + for local_z in 0..horizontal_cell_block_count { - let block_z = self.start_block_z() - + cell_z as i32 * horizontal_cell_block_count as i32 - + local_z as i32; - let delta_z = local_z as f64 / horizontal_cell_block_count as f64; + let block_z = block_z_base + local_z as i32; + let delta_z = local_z as f64 * delta_z_step; self.noise_sampler.interpolate_z(delta_z); - // TODO: Can the math here be simplified? Do the above values come - // to the same results? - let cell_offset_x = block_x - sample_start_x; + // The `cell_offset` calculations are still a good idea for clarity and correctness + // but let's confirm the values. + // block_x = start_block_x + cell_x*H + local_x + // sample_start_x = start_cell_x*H + cell_x*H = (start_cell_x+cell_x)*H + // These can be simplified. + let cell_offset_x = local_x as i32; let cell_offset_y = block_y - sample_start_y; - let cell_offset_z = block_z - sample_start_z; - - #[cfg(debug_assertions)] - { - assert!(cell_offset_x >= 0); - assert!(cell_offset_y >= 0); - assert!(cell_offset_z >= 0); - } + let cell_offset_z = local_z as i32; let block_state = self .noise_sampler @@ -540,27 +572,10 @@ impl<'a> ProtoChunk<'a> { } } } - self.noise_sampler.swap_buffers(); } } - pub fn generate_entities(&self) { - let start_x = self.start_block_x(); - let start_z = self.start_block_z(); - - let population_seed = - Xoroshiro::get_population_seed(self.random_config.seed, start_x, start_z); - let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(population_seed)); - let biome = self.get_biome(&Vector3::new( - start_x, - self.bottom_section_coord() as i32 + self.height() as i32 - 1, - start_z, - )); - while random.next_f32() < biome.creature_spawn_probability {} - todo!() - } - pub fn get_biome_for_terrain_gen(&self, global_block_pos: &Vector3) -> &'static Biome { let seed_biome_pos = biome::get_biome_blend( self.bottom_y(), @@ -582,14 +597,15 @@ impl<'a> ProtoChunk<'a> { let min_y = self.bottom_y(); let random = &self.random_config.base_random_deriver; - let mut noise_builder = DoublePerlinNoiseBuilder::new(self.random_config); - let terrain_builder = SurfaceTerrainBuilder::new(&mut noise_builder, random); + let noise_builder = DoublePerlinNoiseBuilder::new(self.random_config); let mut context = MaterialRuleContext::new( min_y, self.height(), noise_builder, random, - &terrain_builder, + &self.terrain_cache.terrain_builder, + &self.terrain_cache.surface_noise, + &self.terrain_cache.secondary_noise, ); for local_x in 0..16 { for local_z in 0..16 { @@ -607,7 +623,9 @@ impl<'a> ProtoChunk<'a> { let this_biome = self.get_biome_for_terrain_gen(&Vector3::new(x, biome_y, z)); if this_biome == &Biome::ERODED_BADLANDS { - terrain_builder.place_badlands_pillar(self, x, z, top_block); + self.terrain_cache + .terrain_builder + .place_badlands_pillar(self, x, z, top_block); // Get the top block again if we placed a pillar! top_block = self.top_block_height_exclusive(&Vector2::new(local_x, local_z)); @@ -678,7 +696,7 @@ impl<'a> ProtoChunk<'a> { &mut self.surface_height_estimate_sampler, ); - terrain_builder.place_iceberg( + self.terrain_cache.terrain_builder.place_iceberg( self, this_biome, x, @@ -787,6 +805,7 @@ mod test { density_function::{NoiseFunctionComponentRange, PassThrough}, proto_noise_router::{ProtoNoiseFunctionComponent, ProtoNoiseRouters}, }, + proto_chunk::TerrainCache, settings::{GENERATION_SETTINGS, GeneratorSetting}, }, }; @@ -796,6 +815,8 @@ mod test { const SEED: u64 = 0; static RANDOM_CONFIG: LazyLock = LazyLock::new(|| GlobalRandomConfig::new(SEED, false)); // TODO: use legacy when needed + static TERRAIN_CACHE: LazyLock = + LazyLock::new(|| TerrainCache::from_random(&RANDOM_CONFIG)); static BASE_NOISE_ROUTER: LazyLock = LazyLock::new(|| ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &RANDOM_CONFIG)); @@ -847,6 +868,8 @@ mod test { &base_router, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -904,6 +927,8 @@ mod test { &base_router, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -961,6 +986,8 @@ mod test { &base_router, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1018,6 +1045,8 @@ mod test { &base_router, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1075,6 +1104,8 @@ mod test { &base_router, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1101,6 +1132,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1122,6 +1155,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1143,6 +1178,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1169,6 +1206,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1195,6 +1234,8 @@ mod test { &BASE_NOISE_ROUTER2, &RANDOM_CONFIG2, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1221,6 +1262,8 @@ mod test { &BASE_NOISE_ROUTER2, &RANDOM_CONFIG2, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_noise(); @@ -1247,6 +1290,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_biomes(Dimension::Overworld); @@ -1276,6 +1321,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_biomes(Dimension::Overworld); @@ -1300,11 +1347,14 @@ mod test { let surface_config = GENERATION_SETTINGS .get(&GeneratorSetting::Overworld) .unwrap(); + let terrain_cache = TerrainCache::from_random(&RANDOM_CONFIG2); let mut chunk = ProtoChunk::new( Vector2::new(-6, 11), &BASE_NOISE_ROUTER2, &RANDOM_CONFIG2, surface_config, + &terrain_cache, + surface_config.default_block.get_state(), ); chunk.populate_biomes(Dimension::Overworld); @@ -1329,11 +1379,15 @@ mod test { let surface_config = GENERATION_SETTINGS .get(&GeneratorSetting::Overworld) .unwrap(); + let terrain_cache = TerrainCache::from_random(&RANDOM_CONFIG2); + let mut chunk = ProtoChunk::new( Vector2::new(-7, 9), &BASE_NOISE_ROUTER2, &RANDOM_CONFIG2, surface_config, + &terrain_cache, + surface_config.default_block.get_state(), ); chunk.populate_biomes(Dimension::Overworld); @@ -1358,11 +1412,15 @@ mod test { let surface_config = GENERATION_SETTINGS .get(&GeneratorSetting::Overworld) .unwrap(); + let terrain_cache = TerrainCache::from_random(&RANDOM_CONFIG2); + let mut chunk = ProtoChunk::new( Vector2::new(-2, 15), &BASE_NOISE_ROUTER2, &RANDOM_CONFIG2, surface_config, + &terrain_cache, + surface_config.default_block.get_state(), ); chunk.populate_biomes(Dimension::Overworld); @@ -1393,6 +1451,8 @@ mod test { &BASE_NOISE_ROUTER, &RANDOM_CONFIG, surface_config, + &TERRAIN_CACHE, + surface_config.default_block.get_state(), ); chunk.populate_biomes(Dimension::Overworld); diff --git a/pumpkin-world/src/generation/surface/mod.rs b/pumpkin-world/src/generation/surface/mod.rs index a09f742a4..eddbc7ece 100644 --- a/pumpkin-world/src/generation/surface/mod.rs +++ b/pumpkin-world/src/generation/surface/mod.rs @@ -44,8 +44,8 @@ pub struct MaterialRuleContext<'a> { last_est_heiht_unique_horizontal_pos_value: i64, unique_horizontal_pos_value: i64, surface_min_y: i32, - pub surface_noise: DoublePerlinNoiseSampler, - pub secondary_noise: DoublePerlinNoiseSampler, + pub surface_noise: &'a DoublePerlinNoiseSampler, + pub secondary_noise: &'a DoublePerlinNoiseSampler, pub stone_depth_below: i32, pub stone_depth_above: i32, pub terrain_builder: &'a SurfaceTerrainBuilder, @@ -55,9 +55,11 @@ impl<'a> MaterialRuleContext<'a> { pub fn new( min_y: i8, height: u16, - mut noise_builder: DoublePerlinNoiseBuilder<'a>, + noise_builder: DoublePerlinNoiseBuilder<'a>, random_deriver: &'a RandomDeriver, terrain_builder: &'a SurfaceTerrainBuilder, + surface_noise: &'a DoublePerlinNoiseSampler, + secondary_noise: &'a DoublePerlinNoiseSampler, ) -> Self { const HORIZONTAL_POS: i64 = -i64::MAX; // Vanilla Self { @@ -76,8 +78,8 @@ impl<'a> MaterialRuleContext<'a> { biome: &Biome::PLAINS, run_depth: 0, secondary_depth: 0.0, - surface_noise: noise_builder.get_noise_sampler_for_id("surface"), - secondary_noise: noise_builder.get_noise_sampler_for_id("surface_secondary"), + surface_noise, + secondary_noise, noise_builder, stone_depth_below: 0, stone_depth_above: 0, @@ -325,6 +327,7 @@ pub struct NoiseThresholdMaterialCondition { impl NoiseThresholdMaterialCondition { pub fn test(&self, context: &mut MaterialRuleContext) -> bool { + // TODO: we want to cache these let sampler = context .noise_builder .get_noise_sampler_for_id(self.noise.strip_prefix("minecraft:").unwrap()); diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index fd9f76a19..f6fd33acb 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -31,7 +31,7 @@ use std::{ use tokio::{ select, sync::{ - Mutex, Notify, RwLock, + Notify, RwLock, mpsc::{self, UnboundedReceiver}, }, task::JoinHandle, @@ -73,7 +73,7 @@ pub struct Level { /// Semaphore to limit concurrent chunk generation tasks //chunk_generation_semaphore: Arc, /// Map to deduplicate chunk generation and avoid DashMap write lock - chunk_generation_locks: Arc, Arc>>>, + //chunk_generation_locks: Arc, Arc>>>, /// Tracks tasks associated with this world instance tasks: TaskTracker, /// Notification that interrupts tasks for shutdown @@ -155,7 +155,7 @@ impl Level { shutdown_notifier: Notify::new(), // Limits concurrent chunk generation tasks to 2x the number of CPUs //chunk_generation_semaphore: Arc::new(Semaphore::new(num_cpus::get())), - chunk_generation_locks: Arc::new(Mutex::new(HashMap::new())), + //chunk_generation_locks: Arc::new(Mutex::new(HashMap::new())), world_gen_pool: Arc::new( ThreadPoolBuilder::new() //.num_threads((num_cpus::get() - 1).min(1)) @@ -785,83 +785,81 @@ impl Level { let block_registry = block_registry.clone(); let self_clone = self_clone.clone(); - let notify = { - let mut locks = self_clone.chunk_generation_locks.lock().await; - if let Some(existing) = locks.get(&pos) { - Some(existing.clone()) - } else { - let notify = Arc::new(Notify::new()); - locks.insert(pos, notify.clone()); - None + // let notify = { + // let mut locks = self_clone.chunk_generation_locks.lock().await; + // if let Some(existing) = locks.get(&pos) { + // Some(existing.clone()) + // } else { + // let notify = Arc::new(Notify::new()); + // locks.insert(pos, notify.clone()); + // None + // } + // }; + + // if let Some(notify) = notify { + // // Wait for the chunk to be generated by another task + // notify.notified().await; + // // After being notified, the chunk should be in loaded_chunks + // // However, it might have been unloaded between notification and access + // if let Some(chunk) = loaded_chunks.get(&pos) { + // let chunk = chunk.clone(); + // if !send_chunk(true, chunk, &channel) { + // // Stop any additional queued generations + // cloned_continue_to_generate.store(false, Ordering::Relaxed); + // } + // } else { + // // Chunk was unloaded after notification, skip this iteration + // // The chunk generation will be retried if needed + // log::info!("Chunk at {pos:?} was unloaded after generation notification"); + // } + // } else { + //let _permit = chunk_generation_semaphore + // .acquire() + // .await + // .expect("Semaphore closed"); + // let handle = tokio::runtime::Handle::current(); + world_gen_pool.spawn(move || { + // Acquire a permit from the semaphore to limit concurrent generation + + // Rayon tasks are queued, so also check it here + if !cloned_continue_to_generate.load(Ordering::Relaxed) { + return; } - }; - if let Some(notify) = notify { - // Wait for the chunk to be generated by another task - notify.notified().await; - // After being notified, the chunk should be in loaded_chunks - // However, it might have been unloaded between notification and access - if let Some(chunk) = loaded_chunks.get(&pos) { - let chunk = chunk.clone(); - if !send_chunk(true, chunk, &channel) { - // Stop any additional queued generations - cloned_continue_to_generate.store(false, Ordering::Relaxed); - } - } else { - // Chunk was unloaded after notification, skip this iteration - // The chunk generation will be retried if needed - log::info!("Chunk at {pos:?} was unloaded after generation notification"); + let result = { + // Deduplicate chunk generation using chunk_generation_locks + + // We are responsible for generating the chunk + let mut generated_chunk = + world_gen.generate_chunk(&self_clone, block_registry.as_ref(), &pos); + generated_chunk.heightmap = generated_chunk.calculate_heightmap(); + let arc_chunk = Arc::new(RwLock::new(generated_chunk)); + loaded_chunks.insert(pos, arc_chunk.clone()); + + // Store the notify for later removal + (arc_chunk, pos) + }; + + // TODO: this is slow and causes dead locks + // Remove the notify and wake up any waiters + // Do this outside the rayon thread to avoid deadlock + // let (arc_chunk, pos) = result; + // { + // let self_clone = self_clone.clone(); + // handle.spawn(async move { + // let mut locks = self_clone.chunk_generation_locks.lock().await; + // if let Some(notify) = locks.remove(&pos) { + // notify.notify_waiters(); + // } + // }); + // } + + if !send_chunk(true, result.0, &channel) { + // Stop any additional queued generations + cloned_continue_to_generate.store(false, Ordering::Relaxed); } - } else { - //let _permit = chunk_generation_semaphore - // .acquire() - // .await - // .expect("Semaphore closed"); - let handle = tokio::runtime::Handle::current(); - world_gen_pool.spawn(move || { - // Acquire a permit from the semaphore to limit concurrent generation - - // Rayon tasks are queued, so also check it here - if !cloned_continue_to_generate.load(Ordering::Relaxed) { - return; - } - - let result = { - // Deduplicate chunk generation using chunk_generation_locks - - // We are responsible for generating the chunk - let mut generated_chunk = world_gen.generate_chunk( - &self_clone, - block_registry.as_ref(), - &pos, - ); - generated_chunk.heightmap = generated_chunk.calculate_heightmap(); - let arc_chunk = Arc::new(RwLock::new(generated_chunk)); - loaded_chunks.insert(pos, arc_chunk.clone()); - - // Store the notify for later removal - (arc_chunk, pos) - }; - - // Remove the notify and wake up any waiters - // Do this outside the rayon thread to avoid deadlock - let (arc_chunk, pos) = result; - { - let self_clone = self_clone.clone(); - handle.spawn(async move { - let mut locks = self_clone.chunk_generation_locks.lock().await; - if let Some(notify) = locks.remove(&pos) { - notify.notify_waiters(); - } - }); - } - - if !send_chunk(true, arc_chunk, &channel) { - // Stop any additional queued generations - cloned_continue_to_generate.store(false, Ordering::Relaxed); - } - }); - } + }); + //} } }; @@ -922,6 +920,7 @@ impl Level { let load_channel = channel.clone(); let loaded_chunks = self.loaded_entity_chunks.clone(); + let world_gen_pool = self.world_gen_pool.clone(); let handle_load = async move { while let Some(data) = load_bridge_recv.recv().await { let is_ok = match data { @@ -977,7 +976,7 @@ impl Level { let cloned_continue_to_generate = continue_to_generate.clone(); //let semaphore = chunk_generation_semaphore.clone(); - tokio::spawn(async move { + world_gen_pool.spawn(move || { // Acquire a permit from the semaphore to limit concurrent generation //let _permit = semaphore.acquire().await.expect("Semaphore closed"); diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 332cd4391..c4aa4600f 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -1,5 +1,6 @@ use dimension::Dimension; use generation::settings::GenerationSettings; +use pumpkin_data::BlockState; use pumpkin_util::math::vector2::Vector2; pub mod biome; @@ -43,12 +44,23 @@ pub use generation::{ GlobalRandomConfig, noise_router::proto_noise_router::ProtoNoiseRouters, proto_chunk::ProtoChunk, settings::GENERATION_SETTINGS, settings::GeneratorSetting, }; + +use crate::generation::proto_chunk::TerrainCache; pub fn bench_create_and_populate_noise( base_router: &ProtoNoiseRouters, random_config: &GlobalRandomConfig, settings: &GenerationSettings, + terrain_cache: &TerrainCache, + default_block: &'static BlockState, ) { - let mut chunk = ProtoChunk::new(Vector2::new(0, 0), base_router, random_config, settings); + let mut chunk = ProtoChunk::new( + Vector2::new(0, 0), + base_router, + random_config, + settings, + terrain_cache, + default_block, + ); chunk.populate_noise(); } @@ -56,8 +68,17 @@ pub fn bench_create_and_populate_biome( base_router: &ProtoNoiseRouters, random_config: &GlobalRandomConfig, settings: &GenerationSettings, + terrain_cache: &TerrainCache, + default_block: &'static BlockState, ) { - let mut chunk = ProtoChunk::new(Vector2::new(0, 0), base_router, random_config, settings); + let mut chunk = ProtoChunk::new( + Vector2::new(0, 0), + base_router, + random_config, + settings, + terrain_cache, + default_block, + ); chunk.populate_biomes(Dimension::Overworld); } @@ -65,8 +86,17 @@ pub fn bench_create_and_populate_noise_with_surface( base_router: &ProtoNoiseRouters, random_config: &GlobalRandomConfig, settings: &GenerationSettings, + terrain_cache: &TerrainCache, + default_block: &'static BlockState, ) { - let mut chunk = ProtoChunk::new(Vector2::new(0, 0), base_router, random_config, settings); + let mut chunk = ProtoChunk::new( + Vector2::new(0, 0), + base_router, + random_config, + settings, + terrain_cache, + default_block, + ); chunk.populate_biomes(Dimension::Overworld); chunk.populate_noise(); chunk.build_surface();