Skip to content

Commit 7b4db40

Browse files
authored
Merge pull request #966 from louis-e/perf/shrink-grid-precision
Halve peak grid storage: f32 for elevation heights + water blend
2 parents 8009695 + 18417be commit 7b4db40

3 files changed

Lines changed: 64 additions & 20 deletions

File tree

src/elevation/mod.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ use selector::select_provider;
1919
/// Holds processed elevation data and metadata
2020
#[derive(Clone)]
2121
pub struct ElevationData {
22-
/// Height values in Minecraft Y coordinates (as f64, rounded to i32 at final block placement)
23-
pub(crate) heights: Vec<Vec<f64>>,
22+
/// Height values in Minecraft Y coordinates.
23+
///
24+
/// Stored as `f32` on purpose: heights are already rounded to integer
25+
/// block Ys at placement time, so the full f64 precision was wasted on a
26+
/// grid that can easily hit 10+ million cells on a city-sized bbox
27+
/// (≈80 MB at f64, halved at f32). Postprocess still runs in f64 for
28+
/// numerical stability; the downcast happens once at construction.
29+
pub(crate) heights: Vec<Vec<f32>>,
2430
/// Width of the elevation grid (may be smaller than world width due to capping)
2531
pub(crate) width: usize,
2632
/// Height of the elevation grid (may be smaller than world height due to capping)
@@ -223,8 +229,17 @@ pub fn fetch_elevation_data(
223229
}
224230
}
225231

232+
// Downcast the f64 postprocess output to the f32 storage format. One-time
233+
// cost paid here so the large grid sits at half the memory for the rest
234+
// of the generation run. NaN/infinity preservation is a requirement —
235+
// downstream `is_finite` checks rely on non-finite sentinels surviving.
236+
let mc_heights_f32: Vec<Vec<f32>> = mc_heights
237+
.into_iter()
238+
.map(|row| row.into_iter().map(|v| v as f32).collect())
239+
.collect();
240+
226241
Ok(ElevationData {
227-
heights: mc_heights,
242+
heights: mc_heights_f32,
228243
width: grid_width,
229244
height: grid_height,
230245
world_width,

src/ground.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,14 @@ impl Ground {
175175
// continuous — the renderer's hard `> 0.5` threshold then traces
176176
// a clean curved shoreline contour instead of the raw ESA 10 m
177177
// rectangular grid edge.
178-
let w00 = lc.water_blend_grid[z0][x0];
179-
let w10 = lc.water_blend_grid[z0][x1];
180-
let w01 = lc.water_blend_grid[z1][x0];
181-
let w11 = lc.water_blend_grid[z1][x1];
178+
// Widen f32 storage to f64 for the bilinear arithmetic. This
179+
// doesn't recover the ~10⁻⁷ precision lost at storage, but it
180+
// prevents extra rounding from accumulating in the four
181+
// multiply-adds + the threshold comparison downstream.
182+
let w00 = lc.water_blend_grid[z0][x0] as f64;
183+
let w10 = lc.water_blend_grid[z0][x1] as f64;
184+
let w01 = lc.water_blend_grid[z1][x0] as f64;
185+
let w11 = lc.water_blend_grid[z1][x1] as f64;
182186

183187
// Bilinear interpolation
184188
let top = w00 * (1.0 - tx) + w10 * tx;
@@ -297,10 +301,17 @@ impl Ground {
297301
let z1 = (z0 + 1).min(data.height - 1);
298302
let dx = fx - x0 as f64;
299303
let dz = fz - z0 as f64;
300-
let v00 = data.heights[z0][x0];
301-
let v10 = data.heights[z0][x1];
302-
let v01 = data.heights[z1][x0];
303-
let v11 = data.heights[z1][x1];
304+
// Widen f32 storage to f64 for the bilinear arithmetic. The real
305+
// property we rely on: across the Minecraft Y range (roughly −64 up
306+
// through a few thousand even with --disable-height-limit), f32's
307+
// mantissa gives ~10⁻⁷ precision per stored cell, which is far
308+
// smaller than the 0.5-block half-width used by `round()` below.
309+
// So for any value that isn't pathologically close to a half-integer
310+
// boundary, the final `result.round() as i32` matches the f64 path.
311+
let v00 = data.heights[z0][x0] as f64;
312+
let v10 = data.heights[z0][x1] as f64;
313+
let v01 = data.heights[z1][x0] as f64;
314+
let v11 = data.heights[z1][x1] as f64;
304315
let lerp_top = v00 + (v10 - v00) * dx;
305316
let lerp_bot = v01 + (v11 - v01) * dx;
306317
let result = lerp_top + (lerp_bot - lerp_top) * dz;
@@ -318,7 +329,12 @@ impl Ground {
318329
world_height: usize,
319330
) {
320331
if let Some(ref mut data) = self.elevation_data {
321-
data.heights = heights;
332+
// Rotation operators build a fresh f64 work grid; downcast here to
333+
// match `ElevationData::heights`'s f32 storage layout.
334+
data.heights = heights
335+
.into_iter()
336+
.map(|row| row.into_iter().map(|v| v as f32).collect())
337+
.collect();
322338
data.width = grid_width;
323339
data.height = grid_height;
324340
data.world_width = world_width;
@@ -389,8 +405,8 @@ impl Ground {
389405
let mut img: image::ImageBuffer<Rgb<u8>, Vec<u8>> =
390406
RgbImage::new(width as u32, height as u32);
391407

392-
let mut min_height: f64 = f64::MAX;
393-
let mut max_height: f64 = f64::MIN;
408+
let mut min_height: f32 = f32::MAX;
409+
let mut max_height: f32 = f32::MIN;
394410

395411
for row in heights {
396412
for &h in row {

src/land_cover.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,18 @@ pub struct LandCoverData {
6464
/// 0 = non-water, 1 = shore water, 2+ = progressively deeper water.
6565
pub water_distance: Vec<Vec<u8>>,
6666
/// Pre-smoothed water-ness field in [0, 1] — a Gaussian-blurred version
67-
/// of the binary `grid == LC_WATER` mask. Used by `ground.water_blend()`
68-
/// and compared against a hard 0.5 threshold in the renderer so the
69-
/// shoreline follows the smoothed contour's 0.5 isoline instead of the
70-
/// raw ESA 10 m rectangular grid edge.
71-
pub water_blend_grid: Vec<Vec<f64>>,
67+
/// of the binary `grid == LC_WATER` mask. Sampled via `ground.water_blend()`
68+
/// and compared against a hard 0.5 threshold inside `ground_generation`
69+
/// (water classification path) so the shoreline follows the smoothed
70+
/// contour's 0.5 isoline instead of the raw ESA 10 m rectangular grid
71+
/// edge.
72+
///
73+
/// Stored as `f32` on purpose — the grid can be tens of millions of cells
74+
/// on large bboxes, and the values are bounded to `[0, 1]` and only ever
75+
/// compared against a 0.5 threshold, so f32's ~7 decimal digits are
76+
/// overkill. Halving the storage saves ~46 MB peak on a Munich-sized
77+
/// area.
78+
pub water_blend_grid: Vec<Vec<f32>>,
7279
/// Grid width (matches elevation grid width)
7380
pub width: usize,
7481
/// Grid height (matches elevation grid height)
@@ -94,7 +101,7 @@ impl LandCoverData {
94101
/// - Coarser grid-to-world (large bbox, capped at 4096): each cell already
95102
/// represents many blocks, so a 3-cell blur represents many blocks of
96103
/// softening — appropriate for the coarser effective resolution.
97-
fn compute_water_blend_smooth(grid: &[Vec<u8>], width: usize, height: usize) -> Vec<Vec<f64>> {
104+
fn compute_water_blend_smooth(grid: &[Vec<u8>], width: usize, height: usize) -> Vec<Vec<f32>> {
98105
const SIGMA_CELLS: f64 = 3.0;
99106

100107
if width == 0 || height == 0 {
@@ -110,7 +117,13 @@ fn compute_water_blend_smooth(grid: &[Vec<u8>], width: usize, height: usize) ->
110117
.collect()
111118
})
112119
.collect();
120+
// Gaussian blur runs in f64 for numerical stability, then we drop down to
121+
// f32 for storage — values land in [0, 1] and are only ever compared to a
122+
// 0.5 threshold, so precision beyond f32 is wasted.
113123
crate::elevation::postprocess::gaussian_blur_grid(&binary, SIGMA_CELLS)
124+
.into_iter()
125+
.map(|row| row.into_iter().map(|v| v as f32).collect())
126+
.collect()
114127
}
115128

116129
/// Metadata parsed from a COG (Cloud-Optimized GeoTIFF) IFD.

0 commit comments

Comments
 (0)