mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 20:32:34 +00:00
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 <lilalexmed@proton.me>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<Vec<(i32, i32)>>,
|
||||
/// 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<DashMap<(StructureKeys, i32, i32), Option<StructurePosition>>>,
|
||||
}
|
||||
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<StructurePosition>,
|
||||
) -> Option<StructurePosition> {
|
||||
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)]
|
||||
|
||||
@@ -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<K> = HashSet<K>;
|
||||
type FastHashMap<K, V> = HashMap<K, V>;
|
||||
// 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<K> = rustc_hash::FxHashSet<K>;
|
||||
type FastHashMap<K, V> = rustc_hash::FxHashMap<K, V>;
|
||||
|
||||
/// Trait to unify Block and Sky light logic
|
||||
pub trait LightProvider {
|
||||
@@ -62,10 +64,6 @@ pub struct LightPropagator<P: LightProvider> {
|
||||
pub(crate) queue: VecDeque<PropagationEntry>,
|
||||
pub(crate) visited: FastHashSet<BlockPos>,
|
||||
pub(crate) decrease_queue: VecDeque<(BlockPos, u8)>,
|
||||
|
||||
// Batched updates
|
||||
pending_updates: FastHashMap<(i32, i32), Vec<(BlockPos, u8)>>,
|
||||
shadow_cache: FastHashMap<BlockPos, u8>,
|
||||
_marker: std::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
@@ -76,8 +74,6 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
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<P: LightProvider> LightPropagator<P> {
|
||||
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<P: LightProvider> LightPropagator<P> {
|
||||
}
|
||||
|
||||
// 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<P: LightProvider> LightPropagator<P> {
|
||||
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<P: LightProvider> LightPropagator<P> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user