From 1ca089df173f218f1af946a0a98cf3e3cf0daca0 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sat, 11 Jul 2026 18:46:29 +0200 Subject: [PATCH] perf(generation): speed up chunk generation(#2335) * perf(generation): cache computed structure starts set_structure_references runs for every chunk and, for each nearby structure candidate, recomputed the structure's placement from scratch. For jigsaw structures (villages, ancient cities, ...) that means re-running the full jigsaw expansion for every chunk whose references overlap the structure -- the same start recomputed many times over. A structure's placement depends only on its start chunk and the world seed (the surface-height estimate it uses is position-independent and min_y is constant per dimension), so memoize it in GlobalStructureCache and reuse it. In the bench, structure references drop from ~342us to ~105us. * perf(lighting): use a fast hasher in the generation light engine The BFS light propagator's visited/shadow_cache/pending_updates maps were aliased to std HashSet/HashMap (SipHash) despite being named "Fast". They are probed on every neighbour of every propagated block, so the hash function dominates. Point the aliases at rustc-hash's FxHash (already a dependency). Lighting generation drops from ~65ms to ~36ms and full chunk generation from ~103ms to ~68ms in the bench, with identical output. * perf(lighting): propagate light through storage, not a shadow cache The BFS light propagator kept a hashed shadow cache of in-flight light values plus a per-chunk batched write buffer, layered on top of the light storage. The storage is itself a fast array lookup, so the extra hashing and buffering cost more than they saved. Read and write it directly and treat it as the single source of truth. Lighting generation drops from ~36ms to ~22ms and full chunk generation from ~68ms to ~52ms, output unchanged (all pumpkin-world tests, including the fixed-seed ancient-city parity test, still pass). --------- Co-authored-by: Alexander Medvedev --- pumpkin-world/src/generation/proto_chunk.rs | 58 +++++++++----- .../src/generation/structure/placement.rs | 33 ++++++++ pumpkin-world/src/lighting/engine.rs | 80 ++++--------------- 3 files changed, 89 insertions(+), 82 deletions(-) diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 33d4d82bb..46a8b4027 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -1350,6 +1350,9 @@ impl ProtoChunk { ); let mut references = Vec::new(); + // Constant across every chunk in the dimension, so hoist it out of the loop + // and out of the (cached) structure-start computation below. + let chunk_min_y = self.bottom_y() as i32; for set in StructureSet::ALL { let mut candidate_chunks = Vec::new(); @@ -1396,26 +1399,43 @@ impl ProtoChunk { for entry in set.structures { let structure = Structure::get(&entry.structure); - let context = StructureGeneratorContext { - seed, - chunk_x: candidate_chunk_x, - chunk_z: candidate_chunk_z, - random: create_chunk_random(seed, candidate_chunk_x, candidate_chunk_z), - sea_level: settings.sea_level, - min_y: self.bottom_y() as i32, - height_sampler: Some(&mut height_sampler), - structure_key: Some(entry.structure), - }; + // A structure's placement depends only on its start chunk and the + // world seed, so cache it: otherwise every surrounding chunk whose + // references overlap it would re-run the (expensive) jigsaw + // expansion. `context` is only built on a cache miss. + let start_data = global_cache.get_or_compute_structure_start( + entry.structure, + candidate_chunk_x, + candidate_chunk_z, + || { + let context = StructureGeneratorContext { + seed, + chunk_x: candidate_chunk_x, + chunk_z: candidate_chunk_z, + random: create_chunk_random( + seed, + candidate_chunk_x, + candidate_chunk_z, + ), + sea_level: settings.sea_level, + min_y: chunk_min_y, + height_sampler: Some(&mut height_sampler), + structure_key: Some(entry.structure), + }; + lazily_generate_structure( + &entry.structure, + structure, + context, + &biome_supplier, + &mut multi_noise_sampler, + ) + }, + ); - if let Some(start_data) = lazily_generate_structure( - &entry.structure, - structure, - context, - &biome_supplier, - &mut multi_noise_sampler, - ) && start_data - .get_bounding_box() - .intersects_raw_xz(start_x, start_z, end_x, end_z) + if let Some(start_data) = start_data + && start_data + .get_bounding_box() + .intersects_raw_xz(start_x, start_z, end_x, end_z) { references.push((entry.structure, start_data.collector.clone())); break; diff --git a/pumpkin-world/src/generation/structure/placement.rs b/pumpkin-world/src/generation/structure/placement.rs index fd5c26f07..e1e246a18 100644 --- a/pumpkin-world/src/generation/structure/placement.rs +++ b/pumpkin-world/src/generation/structure/placement.rs @@ -13,6 +13,10 @@ use std::f64::consts::PI; use std::sync::OnceLock; use crate::ProtoChunk; +use dashmap::DashMap; +use pumpkin_data::structures::StructureKeys; + +use super::structures::StructurePosition; /// A thread-safe global cache for structures that require world-wide placement calculations /// rather than localized chunk-based math (e.g., Strongholds using Concentric Rings). /// @@ -21,6 +25,12 @@ use crate::ProtoChunk; pub struct GlobalStructureCache { /// A cached list of mathematically predicted (`chunk_x`, `chunk_z`) coordinates. stronghold_chunks: OnceLock>, + /// Memoized structure starts, keyed by (structure, start chunk x, start chunk z). + /// + /// A jigsaw structure's placement is fully determined by its start chunk and the + /// world seed, so it is computed once here instead of being recomputed for every + /// surrounding chunk whose structure references overlap it. + structure_starts: OnceLock>>, } impl GlobalStructureCache { /// Creates a new, empty global structure cache. @@ -28,6 +38,7 @@ impl GlobalStructureCache { pub const fn new() -> Self { Self { stronghold_chunks: OnceLock::new(), + structure_starts: OnceLock::new(), } } @@ -37,6 +48,28 @@ impl GlobalStructureCache { .map_or(&[], std::vec::Vec::as_slice) } + /// Returns the memoized structure start for the given structure and start chunk, + /// computing it via `compute` on the first request and caching the result. + /// + /// Because a structure's placement depends only on its start chunk and the world + /// seed, every chunk whose references overlap that structure can reuse the cached + /// result instead of re-running the expensive jigsaw expansion. + pub fn get_or_compute_structure_start( + &self, + key: StructureKeys, + chunk_x: i32, + chunk_z: i32, + compute: impl FnOnce() -> Option, + ) -> Option { + let cache = self.structure_starts.get_or_init(DashMap::new); + if let Some(cached) = cache.get(&(key, chunk_x, chunk_z)) { + return cached.value().clone(); + } + let computed = compute(); + cache.insert((key, chunk_x, chunk_z), computed.clone()); + computed + } + /// Retrieves the list of chunk coordinates for Concentric Ring structures. /// If the cache is empty, it calculates the 128 ring positions mathematically. #[allow(clippy::cast_precision_loss)] diff --git a/pumpkin-world/src/lighting/engine.rs b/pumpkin-world/src/lighting/engine.rs index 21b6b22e4..021c95fd0 100644 --- a/pumpkin-world/src/lighting/engine.rs +++ b/pumpkin-world/src/lighting/engine.rs @@ -8,12 +8,14 @@ use pumpkin_data::BlockDirection; use pumpkin_util::HeightMap; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::hash::RandomState; +use std::collections::VecDeque; //use std::time::Instant; -type FastHashSet = HashSet; -type FastHashMap = HashMap; +// These are hit on every neighbour of every propagated block, so the hash +// function dominates. Use rustc-hash's fast hasher instead of the std default +// (SipHash), which is what "Fast" was meant to imply. +type FastHashSet = rustc_hash::FxHashSet; +type FastHashMap = rustc_hash::FxHashMap; /// Trait to unify Block and Sky light logic pub trait LightProvider { @@ -62,10 +64,6 @@ pub struct LightPropagator { pub(crate) queue: VecDeque, pub(crate) visited: FastHashSet, pub(crate) decrease_queue: VecDeque<(BlockPos, u8)>, - - // Batched updates - pending_updates: FastHashMap<(i32, i32), Vec<(BlockPos, u8)>>, - shadow_cache: FastHashMap, _marker: std::marker::PhantomData

, } @@ -76,8 +74,6 @@ impl LightPropagator

{ queue: VecDeque::with_capacity(4096), visited: FastHashSet::default(), decrease_queue: VecDeque::new(), - pending_updates: FastHashMap::default(), - shadow_cache: FastHashMap::default(), _marker: std::marker::PhantomData, } } @@ -86,42 +82,25 @@ impl LightPropagator

{ self.queue.clear(); self.visited.clear(); self.decrease_queue.clear(); - self.pending_updates.clear(); - self.shadow_cache.clear(); } - /// Flushes pending updates to chunk storage - fn apply_updates(&mut self, cache: &mut Cache) { - if self.pending_updates.is_empty() { - return; - } - - for (_, updates) in self.pending_updates.drain() { - for (pos, val) in updates { - P::set_light(cache, pos, val); - } - } - } - - /// Core Propagation Logic (BFS) + /// Core Propagation Logic (BFS). + /// + /// Reads and writes light directly through the light storage (a fast array + /// lookup) instead of maintaining a separate hashed shadow cache and batched + /// write buffer; the storage is the single source of truth. pub fn propagate(&mut self, cache: &mut Cache) { - self.shadow_cache.clear(); - // Cache metadata for bounds checking let cache_x = cache.x; let cache_z = cache.z; let cache_size = cache.size; + let min_y = cache.bottom_y() as i32; + let max_y = min_y + cache.height() as i32; while let Some(entry) = self.queue.pop_front() { let pos = entry.pos; - // Check shadow cache first, fall back to storage - let current_light = self - .shadow_cache - .get(&pos) - .copied() - .unwrap_or_else(|| P::get_light(cache, pos)); - + let current_light = P::get_light(cache, pos); if current_light <= 1 { continue; } @@ -142,8 +121,6 @@ impl LightPropagator

{ } // Skip neighbor if it's outside world bounds - let min_y = cache.bottom_y() as i32; - let max_y = min_y + cache.height() as i32; if neighbor_pos.0.y < min_y || neighbor_pos.0.y >= max_y { continue; } @@ -160,25 +137,10 @@ impl LightPropagator

{ let opacity = state.to_state().opacity; let new_level = P::propagate_level(current_light, opacity, dir); - - // Check shadow cache first, fall back to storage - let neighbor_light = self - .shadow_cache - .get(&neighbor_pos) - .copied() - .unwrap_or_else(|| P::get_light(cache, neighbor_pos)); + let neighbor_light = P::get_light(cache, neighbor_pos); if new_level > neighbor_light { - // Update shadow cache for this propagation cycle - self.shadow_cache.insert(neighbor_pos, new_level); - - // Queue for batch write - let chunk_x = neighbor_pos.0.x >> 4; - let chunk_z = neighbor_pos.0.z >> 4; - self.pending_updates - .entry((chunk_x, chunk_z)) - .or_default() - .push((neighbor_pos, new_level)); + P::set_light(cache, neighbor_pos, new_level); // Add to propagation queue if bright enough if new_level > 1 && self.visited.insert(neighbor_pos) { @@ -189,15 +151,7 @@ impl LightPropagator

{ } } } - - // Batch write every 64 chunks worth of updates - if self.pending_updates.len() > 64 { - self.apply_updates(cache); - } } - - // Final flush of any remaining updates - self.apply_updates(cache); } /// Handle light removal @@ -327,7 +281,7 @@ impl SkyLightPropagator { // Pre-allocate with exact size needed let capacity = ((end_x - start_x) * (end_z - start_z)) as usize; let mut surface_heights = - FastHashMap::with_capacity_and_hasher(capacity, RandomState::default()); + FastHashMap::with_capacity_and_hasher(capacity, rustc_hash::FxBuildHasher); // Process in Z-outer, X-inner order for better cache locality for z in start_z..end_z {