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:
TheDarkSword
2026-07-11 18:46:29 +02:00
committed by GitHub
parent 5c1558788a
commit 1ca089df17
3 changed files with 89 additions and 82 deletions

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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 {