@@ -34,6 +34,78 @@ use colored::Colorize;
3434use indicatif:: { ProgressBar , ProgressStyle } ;
3535use 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