Skip to content

Commit 7823555

Browse files
authored
Merge pull request #967 from louis-e/perf/ground-gen-optimizations
Per-chunk ground-Y cache + recalibrate CI benchmark baseline
2 parents 7b4db40 + 195919c commit 7823555

2 files changed

Lines changed: 137 additions & 32 deletions

File tree

.github/workflows/pr-benchmark.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ jobs:
6868
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
6969
gen_time=${{ steps.benchmark.outputs.gen_time }}
7070
71-
baseline_time=20
71+
# Calibrated after the multi-source elevation pipeline landed (PR #939),
72+
# the road-flatten perf fix (PR #965), and the grid-precision shrink.
73+
# Update this value whenever main-branch generation changes materially
74+
# so the verdict thresholds below stay meaningful.
75+
baseline_time=30
7276
diff=$((duration - baseline_time))
7377
abs_diff=${diff#-}
7478

src/ground_generation.rs

Lines changed: 132 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,78 @@ use colored::Colorize;
3434
use indicatif::{ProgressBar, ProgressStyle};
3535
use rand::Rng;
3636

37+
/// Per-chunk cache of ground Y values.
38+
///
39+
/// Each Minecraft-chunk worth of surface/vegetation/depth logic fires
40+
/// roughly 20-plus `get_ground_level` lookups per cell (own column + 8
41+
/// water-column neighbours + 8 depth-fill neighbours + a handful of
42+
/// slope/surface checks). At a typical city bbox that's ~10⁸ calls,
43+
/// each touching the road-override map, an elevation-grid bilinear, and
44+
/// a few f32→f64 casts. Precomputing one Y per cell up front — via a
45+
/// flat 256-entry stack array aligned to the chunk's 16×16 footprint —
46+
/// turns the 20-plus per-cell calls into stack array reads for
47+
/// everything inside the chunk; neighbours that escape the chunk
48+
/// boundary fall back to `editor.get_ground_level`. The cache is
49+
/// populated once per chunk, read many times, then dropped.
50+
struct ChunkGroundCache {
51+
/// Row-major `16*lz + lx` where `lx = x - base_x`, `lz = z - base_z`.
52+
/// Positions outside `[min_x..=max_x, min_z..=max_z]` are never read.
53+
grid: [i32; 256],
54+
base_x: i32,
55+
base_z: i32,
56+
min_x: i32,
57+
max_x: i32,
58+
min_z: i32,
59+
max_z: i32,
60+
}
61+
62+
impl ChunkGroundCache {
63+
#[inline]
64+
fn populate(
65+
editor: &WorldEditor,
66+
chunk_x: i32,
67+
chunk_z: i32,
68+
min_x: i32,
69+
max_x: i32,
70+
min_z: i32,
71+
max_z: i32,
72+
) -> Self {
73+
let base_x = chunk_x << 4;
74+
let base_z = chunk_z << 4;
75+
let mut grid = [0i32; 256];
76+
for x in min_x..=max_x {
77+
for z in min_z..=max_z {
78+
let lx = (x - base_x) as usize;
79+
let lz = (z - base_z) as usize;
80+
grid[lz * 16 + lx] = editor.get_ground_level(x, z);
81+
}
82+
}
83+
ChunkGroundCache {
84+
grid,
85+
base_x,
86+
base_z,
87+
min_x,
88+
max_x,
89+
min_z,
90+
max_z,
91+
}
92+
}
93+
94+
/// Get the ground Y at `(nx, nz)`. Cached for cells inside this chunk's
95+
/// populated range; falls through to `editor.get_ground_level` for
96+
/// neighbour reads that cross a chunk boundary.
97+
#[inline]
98+
fn get(&self, editor: &WorldEditor, nx: i32, nz: i32) -> i32 {
99+
if nx >= self.min_x && nx <= self.max_x && nz >= self.min_z && nz <= self.max_z {
100+
let lx = (nx - self.base_x) as usize;
101+
let lz = (nz - self.base_z) as usize;
102+
self.grid[lz * 16 + lx]
103+
} else {
104+
editor.get_ground_level(nx, nz)
105+
}
106+
}
107+
}
108+
37109
/// Generate the ground layer for the entire bounding box.
38110
///
39111
/// This must be called after all OSM element processing is complete and the
@@ -87,6 +159,23 @@ pub fn generate_ground_layer(
87159
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
88160
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
89161

162+
// Precompute a per-chunk ground-Y cache so subsequent lookups
163+
// (main column + water-column + depth-fill neighbours, ~20+ per
164+
// cell) hit a stack array instead of re-running the bilinear
165+
// elevation interpolation. Only populated when terrain is on —
166+
// the flat-ground path never calls `editor.get_ground_level`.
167+
let chunk_ground_cache = terrain_enabled.then(|| {
168+
ChunkGroundCache::populate(
169+
editor,
170+
chunk_x,
171+
chunk_z,
172+
chunk_min_x,
173+
chunk_max_x,
174+
chunk_min_z,
175+
chunk_max_z,
176+
)
177+
});
178+
90179
for x in chunk_min_x..=chunk_max_x {
91180
for z in chunk_min_z..=chunk_max_z {
92181
// Skip blocks outside the rotated original bounding box
@@ -98,10 +187,11 @@ pub fn generate_ground_layer(
98187
continue;
99188
}
100189

101-
// Get ground level, when terrain is enabled, look it up once per block
102-
// When disabled, use constant ground_level (no function call overhead)
103-
let ground_y = if terrain_enabled {
104-
editor.get_ground_level(x, z)
190+
// Get ground level. When terrain is enabled, pull from the
191+
// per-chunk cache (one populated lookup, no bilinear); when
192+
// disabled, use the constant ground_level.
193+
let ground_y = if let Some(ref cache) = chunk_ground_cache {
194+
cache.get(editor, x, z)
105195
} else {
106196
args.ground_level
107197
};
@@ -160,7 +250,17 @@ pub fn generate_ground_layer(
160250
// bilinear lookup and fixes the false negatives in
161251
// the osm_gap detection below.
162252
let has_water_in_column = |wx: i32, wz: i32| {
163-
let gy = editor.get_ground_level(wx, wz);
253+
// Pull from the chunk cache so the 9-neighbour
254+
// fan-out around each cell doesn't trigger nine
255+
// bilinear interpolations per cell. In flat-ground
256+
// mode every column has the same constant Y, so
257+
// we skip the `editor.get_ground_level` fallback
258+
// (road overrides in flat mode always resolve to
259+
// the same `args.ground_level` anyway).
260+
let gy = match chunk_ground_cache {
261+
Some(ref cache) => cache.get(editor, wx, wz),
262+
None => args.ground_level,
263+
};
164264
for dy in 0..=2 {
165265
if editor.check_for_block_absolute(
166266
wx,
@@ -527,7 +627,7 @@ pub fn generate_ground_layer(
527627
// gap on cliff faces. Check all 8 neighbors (cardinal
528628
// + diagonal) and fill down to the lowest neighbor's
529629
// ground level so no void is ever visible.
530-
let depth = if terrain_enabled {
630+
let depth = if let Some(ref cache) = chunk_ground_cache {
531631
let mut min_neighbor_y = ground_y;
532632
for &(dx, dz) in &[
533633
(-1i32, 0i32),
@@ -539,7 +639,7 @@ pub fn generate_ground_layer(
539639
(1, -1),
540640
(1, 1),
541641
] {
542-
let ny = editor.get_ground_level(x + dx, z + dz);
642+
let ny = cache.get(editor, x + dx, z + dz);
543643
if ny < min_neighbor_y {
544644
min_neighbor_y = ny;
545645
}
@@ -841,31 +941,32 @@ pub fn generate_ground_layer(
841941
// quarries, landuse areas, and other OSM elements on slopes don't
842942
// leave visible gaps. Uses set_block_if_absent so it won't overwrite
843943
// material-specific under-blocks already placed above.
844-
if terrain_enabled
845-
&& !editor.check_for_block_absolute(x, ground_y, z, Some(&[WATER]), None)
846-
&& !did_underfill
847-
{
848-
let mut min_neighbor_y = ground_y;
849-
for &(dx, dz) in &[
850-
(-1i32, 0i32),
851-
(1, 0),
852-
(0, -1),
853-
(0, 1),
854-
(-1, -1),
855-
(-1, 1),
856-
(1, -1),
857-
(1, 1),
858-
] {
859-
let ny = editor.get_ground_level(x + dx, z + dz);
860-
if ny < min_neighbor_y {
861-
min_neighbor_y = ny;
944+
if let Some(ref cache) = chunk_ground_cache {
945+
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[WATER]), None)
946+
&& !did_underfill
947+
{
948+
let mut min_neighbor_y = ground_y;
949+
for &(dx, dz) in &[
950+
(-1i32, 0i32),
951+
(1, 0),
952+
(0, -1),
953+
(0, 1),
954+
(-1, -1),
955+
(-1, 1),
956+
(1, -1),
957+
(1, 1),
958+
] {
959+
let ny = cache.get(editor, x + dx, z + dz);
960+
if ny < min_neighbor_y {
961+
min_neighbor_y = ny;
962+
}
963+
}
964+
let depth = (ground_y - min_neighbor_y + 1).clamp(2, 32);
965+
let y_max = ground_y - 1;
966+
let y_min = (ground_y - depth).max(MIN_Y + 1);
967+
if y_min <= y_max {
968+
editor.fill_column_absolute(STONE, x, z, y_min, y_max, true);
862969
}
863-
}
864-
let depth = (ground_y - min_neighbor_y + 1).clamp(2, 32);
865-
let y_max = ground_y - 1;
866-
let y_min = (ground_y - depth).max(MIN_Y + 1);
867-
if y_min <= y_max {
868-
editor.fill_column_absolute(STONE, x, z, y_min, y_max, true);
869970
}
870971
}
871972

0 commit comments

Comments
 (0)