Skip to main content

steel_worldgen/noise/
noise_chunk.rs

1//! `NoiseChunk`: cell-based terrain density evaluation with trilinear interpolation.
2//!
3//! Matches vanilla's `NoiseChunk` + `NoiseBasedChunkGenerator.doFill()` flow.
4//!
5//! Vanilla wraps density functions with `Interpolated` markers. Only the inner
6//! functions (arguments to `Interpolated`) are evaluated at cell corners; the
7//! outer operations (squeeze, min, etc.) are applied per-block after trilinear
8//! interpolation. Markers with identical evaluations share a channel.
9//!
10//! Cell dimensions depend on the dimension's noise settings.
11
12use std::marker::PhantomData;
13use std::simd::f32x4;
14
15use glam::IVec3;
16use steel_utils::SIMD_BATCH;
17use steel_worldgen::density::{ColumnCache, DimensionNoises, NoiseSettings};
18
19use crate::noise::{Beardifier, CornerColumnStore};
20
21/// Maximum number of interpolation channels supported.
22const MAX_INTERP: usize = 16;
23const INTERPOLATION_LANES: usize = 4;
24
25/// Maximum slice length (`z_corners` * `corners_y`) across all dimensions.
26/// Overworld: (16/4+1) * (384/8+1) = 5 * 49 = 245. Rounded up for headroom.
27const MAX_SLICE_LEN: usize = 256;
28/// The largest Vanilla noise cell height (overworld's is 8).
29const MAX_CELL_HEIGHT: usize = 16;
30
31/// Stores density values at cell corners for a single chunk and provides
32/// trilinear interpolation between corners for block-level resolution.
33///
34/// Supports multiple interpolation channels matching vanilla's multi-interpolator
35/// system. Distinct marker evaluations get channels filled at cell corners
36/// and interpolated independently.
37///
38/// Storage is per-corner `SoA` — `slice[corner_idx * MAX_INTERP + ch]` — so 4
39/// adjacent channels' values at a given corner sit in contiguous memory,
40/// allowing the terrain sampler to keep Vanilla's f32 density values without
41/// widening an intermediate result.
42pub struct NoiseChunk<N: DimensionNoises> {
43    /// One slice per cell-X boundary, holding density values at the cell
44    /// corners on that X-plane. Length is `cell_count_xz + 1`. Indexed as
45    /// `slices[cx][corner_idx * MAX_INTERP + ch]` where
46    /// `corner_idx = z_corner * corners_y + y_corner` (range `[0, slice_len)`)
47    /// and `ch` is the interpolation channel (range `[0, interp_count)`).
48    ///
49    /// We keep all slices materialized rather than alternating two buffers so
50    /// the slice-fill phase can run in parallel: each `cx` boundary's noise
51    /// tree evaluation is independent. The per-block trilerp loop then
52    /// indexes `slices[cx]` and `slices[cx + 1]` sequentially.
53    slices: Vec<Box<[f32; MAX_INTERP * MAX_SLICE_LEN]>>,
54    /// Number of active interpolation channels.
55    interp_count: usize,
56    /// Number of Y corners per Z column (`cell_count_y` + 1).
57    corners_y: usize,
58
59    /// Per-corner block-Y values, precomputed once at construction.
60    /// Same for every slice fill (depends only on `cell_min_y`,
61    /// `cell_height`, and `corners_y`).
62    block_ys: Vec<i32>,
63
64    /// First cell X/Z in world coordinates (cell index, not block).
65    first_cell_x: i32,
66    first_cell_z: i32,
67    /// Minimum cell Y index.
68    cell_min_y: i32,
69    /// Number of cells in Y direction.
70    cell_count_y: usize,
71    /// Number of cells per chunk in XZ.
72    cell_count_xz: usize,
73
74    _phantom: PhantomData<N>,
75}
76
77impl<N: DimensionNoises> NoiseChunk<N> {
78    /// Create a new `NoiseChunk` for the given chunk position.
79    ///
80    /// `chunk_min_block_x` and `chunk_min_block_z` are the world-space block
81    /// coordinates of the chunk's northwest corner.
82    #[must_use]
83    #[expect(
84        clippy::missing_panics_doc,
85        reason = "panic is a compile-time constant check"
86    )]
87    pub fn new(chunk_min_block_x: i32, chunk_min_block_z: i32) -> Self {
88        let cell_width = N::Settings::CELL_WIDTH;
89        let cell_height = N::Settings::CELL_HEIGHT;
90        let min_y = N::Settings::MIN_Y;
91        let height = N::Settings::HEIGHT;
92
93        let first_cell_x = chunk_min_block_x.div_euclid(cell_width);
94        let first_cell_z = chunk_min_block_z.div_euclid(cell_width);
95        let cell_min_y = min_y.div_euclid(cell_height);
96
97        let cell_count_xz = (16 / cell_width) as usize;
98        let cell_count_y = (height / cell_height) as usize;
99        let corners_y = cell_count_y + 1;
100        let z_corners = cell_count_xz + 1;
101        let slice_len = z_corners * corners_y;
102
103        let interp_count = N::interpolated_count();
104        assert!(
105            slice_len <= MAX_SLICE_LEN,
106            "slice_len {slice_len} exceeds MAX_SLICE_LEN {MAX_SLICE_LEN}"
107        );
108        assert!(
109            interp_count <= MAX_INTERP,
110            "interp_count {interp_count} exceeds MAX_INTERP {MAX_INTERP}"
111        );
112
113        let block_ys: Vec<i32> = (0..corners_y)
114            .map(|cy| (cy as i32 + cell_min_y) * cell_height)
115            .collect();
116
117        let n_slices = cell_count_xz + 1;
118        let mut slices = Vec::with_capacity(n_slices);
119        for _ in 0..n_slices {
120            // This is a per-chunk constructor, so the stack temporary is fine.
121            slices.push(Box::new([0.0_f32; MAX_INTERP * MAX_SLICE_LEN]));
122        }
123
124        Self {
125            slices,
126            interp_count,
127            corners_y,
128            block_ys,
129            first_cell_x,
130            first_cell_z,
131            cell_min_y,
132            cell_count_y,
133            cell_count_xz,
134            _phantom: PhantomData,
135        }
136    }
137
138    /// Fill the slice buffer for the given cell X. Free-standing function so
139    /// each parallel slice-fill can run on its own thread with its own
140    /// `ColumnCache` clone.
141    #[expect(
142        clippy::too_many_arguments,
143        reason = "slice filling needs the precomputed geometry and per-thread cache"
144    )]
145    fn fill_slice_into(
146        slice: &mut [f32; MAX_INTERP * MAX_SLICE_LEN],
147        cell_x: i32,
148        block_ys: &[i32],
149        blended_column: &mut [f32],
150        interp_count: usize,
151        corners_y: usize,
152        cell_count_xz: usize,
153        first_cell_z: i32,
154        noises: &N,
155        cache: &mut N::ColumnCache,
156        corner_columns: &CornerColumnStore,
157    ) {
158        let cell_width = N::Settings::CELL_WIDTH;
159
160        let block_x = cell_x * cell_width;
161        // Chunks sharing a corner column: two across each chunk boundary it lies on.
162        let chunk_boundary = |cell: i32| cell.rem_euclid(cell_count_xz as i32) == 0;
163        let x_users: u8 = if chunk_boundary(cell_x) { 2 } else { 1 };
164
165        let mut values = [0.0_f32; MAX_INTERP * SIMD_BATCH];
166
167        for cz in 0..=cell_count_xz {
168            let cell_z = first_cell_z + cz as i32;
169            let block_z = cell_z * cell_width;
170            let users = x_users * if chunk_boundary(cell_z) { 2 } else { 1 };
171            let column_base = cz * corners_y * MAX_INTERP;
172            let column_len = corners_y * MAX_INTERP;
173
174            if users > 1
175                && corner_columns.consume((cell_x, cell_z), |values| {
176                    let column = &mut slice[column_base..column_base + column_len];
177                    for (corner, stored) in column
178                        .as_chunks_mut::<MAX_INTERP>()
179                        .0
180                        .iter_mut()
181                        .zip(values.chunks_exact(interp_count))
182                    {
183                        corner[..interp_count].copy_from_slice(stored);
184                    }
185                })
186            {
187                continue;
188            }
189
190            // Ensure column cache for this (x, z)
191            cache.ensure(block_x, block_z, noises);
192
193            // SIMD-batch blended noise for the entire Y column.
194            noises.compute_noise_column(block_x, block_ys, block_z, blended_column);
195
196            let (y_batches, y_tail) = block_ys.as_chunks::<SIMD_BATCH>();
197            let (blended_batches, blended_tail) = blended_column.as_chunks::<SIMD_BATCH>();
198            debug_assert_eq!(y_batches.len(), blended_batches.len());
199            debug_assert_eq!(y_tail.len(), blended_tail.len());
200
201            for (batch, (ys, blended_noise_values)) in
202                y_batches.iter().zip(blended_batches.iter()).enumerate()
203            {
204                let cy = batch * SIMD_BATCH;
205                noises.fill_cell_corner_densities_y_simd::<SIMD_BATCH>(
206                    cache,
207                    block_x,
208                    *ys,
209                    block_z,
210                    *blended_noise_values,
211                    &mut values[..interp_count * SIMD_BATCH],
212                );
213                for lane in 0..SIMD_BATCH {
214                    let corner_idx = cz * corners_y + cy + lane;
215                    let base = corner_idx * MAX_INTERP;
216                    let value_base = lane * interp_count;
217                    slice[base..base + interp_count]
218                        .copy_from_slice(&values[value_base..value_base + interp_count]);
219                }
220            }
221
222            let tail_start = y_batches.len() * SIMD_BATCH;
223            for (tail_offset, (&block_y, &blended_noise_value)) in
224                y_tail.iter().zip(blended_tail.iter()).enumerate()
225            {
226                let cy = tail_start + tail_offset;
227
228                noises.fill_cell_corner_densities(
229                    cache,
230                    block_x,
231                    block_y,
232                    block_z,
233                    blended_noise_value,
234                    &mut values[..interp_count],
235                );
236
237                let corner_idx = cz * corners_y + cy;
238                let base = corner_idx * MAX_INTERP;
239                slice[base..base + interp_count].copy_from_slice(&values[..interp_count]);
240            }
241
242            if users > 1 {
243                corner_columns.produce((cell_x, cell_z), users, || {
244                    slice[column_base..column_base + column_len]
245                        .as_chunks::<MAX_INTERP>()
246                        .0
247                        .iter()
248                        .flat_map(|corner| &corner[..interp_count])
249                        .copied()
250                        .collect()
251                });
252            }
253        }
254    }
255
256    /// Fill the chunk with terrain blocks using multi-channel trilinear interpolation.
257    ///
258    /// For each block position:
259    /// 1. Trilinearly interpolate each channel independently from cell corners
260    /// 2. Apply outer operations (squeeze, min, etc.) via `combine_interpolated`
261    /// 3. Call `place_block` with the final density
262    ///
263    /// Cells at or above `unsampled_air_min_y` whose proving channels
264    /// ([`DimensionNoises::final_density_nonpositive_channels`]) are `<= 0`
265    /// at every interpolated block value, and that the beardifier cannot reach,
266    /// are skipped without calling `place_block`: their density is non-positive,
267    /// which the caller guarantees places nothing and changes no state there.
268    #[expect(
269        clippy::too_many_lines,
270        reason = "keeps the cell interpolation and block traversal together"
271    )]
272    pub fn fill<F>(
273        &mut self,
274        noises: &N,
275        cache: &mut N::ColumnCache,
276        beardifier: Option<&Beardifier>,
277        unsampled_air_min_y: i32,
278        corner_columns: &CornerColumnStore,
279        mut place_block: F,
280    ) where
281        F: FnMut(usize, i32, usize, f32, &[f32], &mut N::ColumnCache),
282    {
283        let cell_width = N::Settings::CELL_WIDTH;
284        let cell_height = N::Settings::CELL_HEIGHT;
285        let cell_count_xz = self.cell_count_xz;
286        let cell_count_y = self.cell_count_y;
287        let interp_count = self.interp_count;
288        let corners_y = self.corners_y;
289        let first_cell_x = self.first_cell_x;
290        let first_cell_z = self.first_cell_z;
291        let block_ys: &[i32] = &self.block_ys;
292
293        // Pre-fill ALL slices sequentially. Each `(cell_x boundary)` slice is an
294        // independent noise-tree evaluation; the grid in `cache` is set up by the
295        // caller via `init_grid` and is read-only here, while each slice only
296        // overwrites the cache's per-column active fields — so one cache is reused
297        // across slices without cloning. The chunk pipeline already parallelises
298        // across chunks, so parallelising the 5 slices here would nest rayon work
299        // and add coordination + cache-clone overhead with no spare cores to use.
300        let n_slices = cell_count_xz + 1;
301        let mut local_blended = vec![0.0_f32; corners_y];
302        for (cx_off, slice) in self.slices[..n_slices].iter_mut().enumerate() {
303            let cell_x = first_cell_x + cx_off as i32;
304            Self::fill_slice_into(
305                slice,
306                cell_x,
307                block_ys,
308                &mut local_blended,
309                interp_count,
310                corners_y,
311                cell_count_xz,
312                first_cell_z,
313                noises,
314                cache,
315                corner_columns,
316            );
317        }
318
319        let air_channels = N::final_density_nonpositive_channels();
320        let mut air_cells = vec![false; cell_count_y];
321        let y_count = cell_height as usize;
322        debug_assert!(y_count <= MAX_CELL_HEIGHT);
323        let inverse_height = f32x4::splat(1.0 / cell_height as f32);
324        let channel_batches = interp_count.div_ceil(INTERPOLATION_LANES);
325        let all_batches = (1_usize << channel_batches) - 1;
326        let proving_batches = air_channels.map_or(0, |channels| {
327            channels.iter().fold(0, |mask, &channel| {
328                mask | (1 << (channel / INTERPOLATION_LANES))
329            })
330        });
331
332        // Reuse Z partials across X without changing vanilla's Z/X/Y arithmetic.
333        let mut z_partials =
334            vec![[f32x4::splat(0.0); 2]; cell_width as usize * corners_y * channel_batches];
335
336        for cell_x_idx in 0..cell_count_xz {
337            let s0: &[f32] = &self.slices[cell_x_idx][..];
338            let s1: &[f32] = &self.slices[cell_x_idx + 1][..];
339
340            for cell_z_idx in 0..cell_count_xz {
341                let z0_base = cell_z_idx * corners_y;
342                let z1_base = (cell_z_idx + 1) * corners_y;
343
344                let cell_min_x = (self.first_cell_x + cell_x_idx as i32) * cell_width;
345                let cell_min_z = (self.first_cell_z + cell_z_idx as i32) * cell_width;
346                for (cell_y_idx, air) in air_cells.iter_mut().enumerate() {
347                    let cell_min_world_y = (self.cell_min_y + cell_y_idx as i32) * cell_height;
348                    *air = air_channels.is_some_and(|channels| {
349                        cell_min_world_y >= unsampled_air_min_y
350                            && !beardifier.is_some_and(|beard| {
351                                beard.may_affect(
352                                    IVec3::new(cell_min_x, cell_min_world_y, cell_min_z),
353                                    IVec3::new(
354                                        cell_min_x + cell_width - 1,
355                                        cell_min_world_y + cell_height - 1,
356                                        cell_min_z + cell_width - 1,
357                                    ),
358                                )
359                            })
360                            && channels.iter().all(|&ch| {
361                                let i0 = (z0_base + cell_y_idx) * MAX_INTERP + ch;
362                                let i1 = (z1_base + cell_y_idx) * MAX_INTERP + ch;
363                                [i0, i0 + MAX_INTERP, i1, i1 + MAX_INTERP]
364                                    .into_iter()
365                                    .all(|i| s0[i] <= 0.0 && s1[i] <= 0.0)
366                            })
367                    });
368                }
369
370                for z_in_cell in 0..cell_width as usize {
371                    let factor_z = f32x4::splat(z_in_cell as f32 / cell_width as f32);
372                    for corner_y in 0..corners_y {
373                        let i0 = (z0_base + corner_y) * MAX_INTERP;
374                        let i1 = (z1_base + corner_y) * MAX_INTERP;
375                        let row = (z_in_cell * corners_y + corner_y) * channel_batches;
376                        for (batch, channel) in
377                            (0..interp_count).step_by(INTERPOLATION_LANES).enumerate()
378                        {
379                            let corner = |slice: &[f32], base| {
380                                // SAFETY: construction bounds the corner count by
381                                // MAX_SLICE_LEN and interp_count by MAX_INTERP.
382                                // Both Z planes and corner_y are in that grid;
383                                // MAX_INTERP is a multiple of four, so the padded
384                                // final batch also stays inside its corner.
385                                f32x4::from_slice(unsafe {
386                                    slice.get_unchecked(
387                                        base + channel..base + channel + INTERPOLATION_LANES,
388                                    )
389                                })
390                            };
391
392                            let left0 = corner(s0, i0);
393                            let left1 = corner(s0, i1);
394                            let right0 = corner(s1, i0);
395                            let right1 = corner(s1, i1);
396                            z_partials[row + batch] = [
397                                left0 + factor_z * (left1 - left0),
398                                right0 + factor_z * (right1 - right0),
399                            ];
400                        }
401                    }
402                }
403
404                for x_in_cell in 0..cell_width {
405                    let factor_x = x_in_cell as f32 / cell_width as f32;
406                    let factor_x_v = f32x4::splat(factor_x);
407                    let local_x = (cell_x_idx as i32 * cell_width + x_in_cell) as usize;
408
409                    for z_in_cell in 0..cell_width {
410                        let local_z = (cell_z_idx as i32 * cell_width + z_in_cell) as usize;
411
412                        for cell_y_idx in (0..cell_count_y).rev() {
413                            let cell_world_y = (self.cell_min_y + cell_y_idx as i32) * cell_height;
414                            let row =
415                                (z_in_cell as usize * corners_y + cell_y_idx) * channel_batches;
416                            let mut values_by_y = [[0.0_f32; MAX_INTERP]; MAX_CELL_HEIGHT];
417
418                            if N::interpolated_count() <= INTERPOLATION_LANES {
419                                for (batch, channel) in
420                                    (0..interp_count).step_by(INTERPOLATION_LANES).enumerate()
421                                {
422                                    let [v00, v10] = z_partials[row + batch];
423                                    let [v01, v11] = z_partials[row + channel_batches + batch];
424                                    let v0 = v00 + factor_x_v * (v10 - v00);
425                                    let v1 = v01 + factor_x_v * (v11 - v01);
426                                    let step = (v1 - v0) * inverse_height;
427                                    let mut value = v0 + step * f32x4::splat(0.0);
428
429                                    for values in values_by_y.iter_mut().take(y_count) {
430                                        values[channel..channel + INTERPOLATION_LANES]
431                                            .copy_from_slice(&value.to_array());
432                                        value += step;
433                                    }
434                                }
435
436                                if air_cells[cell_y_idx]
437                                    && air_channels.is_some_and(|channels| {
438                                        Self::channels_are_nonpositive(
439                                            &values_by_y[..y_count],
440                                            channels,
441                                        )
442                                    })
443                                {
444                                    continue;
445                                }
446                            } else {
447                                let fill_batches =
448                                    |batches: usize, values_by_y: &mut [[f32; MAX_INTERP]]| {
449                                        for (batch, channel) in (0..interp_count)
450                                            .step_by(INTERPOLATION_LANES)
451                                            .enumerate()
452                                        {
453                                            if batches & (1 << batch) == 0 {
454                                                continue;
455                                            }
456
457                                            let [v00, v10] = z_partials[row + batch];
458                                            let [v01, v11] =
459                                                z_partials[row + channel_batches + batch];
460                                            let v0 = v00 + factor_x_v * (v10 - v00);
461                                            let v1 = v01 + factor_x_v * (v11 - v01);
462                                            let step = (v1 - v0) * inverse_height;
463                                            let mut value = v0 + step * f32x4::splat(0.0);
464
465                                            for values in values_by_y.iter_mut().take(y_count) {
466                                                values[channel..channel + INTERPOLATION_LANES]
467                                                    .copy_from_slice(&value.to_array());
468                                                value += step;
469                                            }
470                                        }
471                                    };
472                                let first_batches = if air_cells[cell_y_idx] {
473                                    proving_batches
474                                } else {
475                                    all_batches
476                                };
477
478                                fill_batches(first_batches, &mut values_by_y);
479
480                                if air_cells[cell_y_idx]
481                                    && air_channels.is_some_and(|channels| {
482                                        Self::channels_are_nonpositive(
483                                            &values_by_y[..y_count],
484                                            channels,
485                                        )
486                                    })
487                                {
488                                    continue;
489                                }
490
491                                let remaining_batches = all_batches & !first_batches;
492                                if remaining_batches != 0 {
493                                    fill_batches(remaining_batches, &mut values_by_y);
494                                }
495                            }
496
497                            for y_in_cell in (0..cell_height).rev() {
498                                let world_y = cell_world_y + y_in_cell;
499                                let interpolated = &values_by_y[y_in_cell as usize][..interp_count];
500
501                                // Apply outer operations per-block.
502                                // x/z are 0 because vanilla's outer operations (squeeze, add, mul,
503                                // quarter_negative, blend_alpha, blend_offset) are x/z-independent;
504                                // only Y matters for YClampedGradient.
505                                let mut density = noises.combine_interpolated(
506                                    cache,
507                                    &interpolated[..interp_count],
508                                    0,
509                                    world_y,
510                                    0,
511                                );
512
513                                // Vanilla integrates beardifier as `add(final_density, beardifier)`
514                                // wrapped in `cacheAllInCell` — i.e. evaluated per-block, after the
515                                // outer ops on `final_density` have run. Adding it at cell corners
516                                // would put it inside the squeeze and trilerp it linearly across
517                                // the cell, both of which diverge from vanilla for large beardifier
518                                // values inside a structure's pieces.
519                                let world_x = cell_x_idx as i32 * cell_width
520                                    + x_in_cell
521                                    + self.first_cell_x * cell_width;
522                                let world_z = cell_z_idx as i32 * cell_width
523                                    + z_in_cell
524                                    + self.first_cell_z * cell_width;
525                                if let Some(beard) = beardifier {
526                                    density += beard.compute(world_x, world_y, world_z) as f32;
527                                }
528
529                                place_block(
530                                    local_x,
531                                    world_y,
532                                    local_z,
533                                    density,
534                                    &interpolated[..interp_count],
535                                    cache,
536                                );
537                            }
538                        }
539                    }
540                }
541            }
542
543            // No swap needed: all slices are pre-filled and indexed directly
544            // via `self.slices[cell_x_idx]` / `[cell_x_idx + 1]`.
545        }
546    }
547
548    fn channels_are_nonpositive(values_by_y: &[[f32; MAX_INTERP]], channels: &[usize]) -> bool {
549        values_by_y
550            .iter()
551            .all(|values| channels.iter().all(|&channel| values[channel] <= 0.0))
552    }
553
554    /// Pre-fills the density/richness buffers used by material ore rules.
555    ///
556    /// `MaterialRuleContext.getDensitiesInChunk` samples these functions over
557    /// complete chunk volume before the surface scan begins. Steel keeps the
558    /// dense Y prefix where a rule may accept a block. Compact `f32` results
559    /// avoid retaining the noise chunk across generation stages.
560    /// The result has `256 * material_ore_vein_value_height() *
561    /// material_ore_vein_value_count()` entries, ordered by Y, Z, X, then value.
562    #[must_use]
563    pub fn prefill_material_ore_vein_values(
564        &self,
565        noises: &N,
566        cache: &mut N::ColumnCache,
567    ) -> Box<[f32]> {
568        let value_count = N::material_ore_vein_value_count();
569        if value_count == 0 {
570            return Box::default();
571        }
572
573        let cell_width = N::Settings::CELL_WIDTH;
574        let cell_height = N::Settings::CELL_HEIGHT;
575        let min_y = N::Settings::MIN_Y;
576        let value_height = N::material_ore_vein_value_height();
577        let mut values = vec![0.0; 16 * 16 * value_height * value_count];
578        let mut interpolated = [0.0_f32; MAX_INTERP];
579
580        for cell_x_idx in 0..self.cell_count_xz {
581            for cell_z_idx in 0..self.cell_count_xz {
582                let z0_base = cell_z_idx * self.corners_y;
583                let z1_base = (cell_z_idx + 1) * self.corners_y;
584                let s0 = &*self.slices[cell_x_idx];
585                let s1 = &*self.slices[cell_x_idx + 1];
586
587                for x_in_cell in 0..cell_width {
588                    let factor_x = x_in_cell as f32 / cell_width as f32;
589                    let local_x = (cell_x_idx as i32 * cell_width + x_in_cell) as usize;
590                    let world_x =
591                        self.first_cell_x * cell_width + cell_x_idx as i32 * cell_width + x_in_cell;
592
593                    for z_in_cell in 0..cell_width {
594                        let factor_z = z_in_cell as f32 / cell_width as f32;
595                        let local_z = (cell_z_idx as i32 * cell_width + z_in_cell) as usize;
596                        let world_z = self.first_cell_z * cell_width
597                            + cell_z_idx as i32 * cell_width
598                            + z_in_cell;
599
600                        for cell_y_idx in (0..self.cell_count_y).rev() {
601                            let i0_base = (z0_base + cell_y_idx) * MAX_INTERP;
602                            let i1_base = (z1_base + cell_y_idx) * MAX_INTERP;
603                            let i0_next = i0_base + MAX_INTERP;
604                            let i1_next = i1_base + MAX_INTERP;
605                            let mut values_by_y = [[0.0_f32; MAX_INTERP]; MAX_CELL_HEIGHT];
606                            let y_count = cell_height as usize;
607                            debug_assert!(y_count <= MAX_CELL_HEIGHT);
608
609                            for channel in 0..self.interp_count {
610                                let n000 = s0[i0_base + channel];
611                                let n001 = s0[i1_base + channel];
612                                let n100 = s1[i0_base + channel];
613                                let n101 = s1[i1_base + channel];
614                                let n010 = s0[i0_next + channel];
615                                let n011 = s0[i1_next + channel];
616                                let n110 = s1[i0_next + channel];
617                                let n111 = s1[i1_next + channel];
618                                let v00 = n000 + factor_z * (n001 - n000);
619                                let v01 = n010 + factor_z * (n011 - n010);
620                                let v10 = n100 + factor_z * (n101 - n100);
621                                let v11 = n110 + factor_z * (n111 - n110);
622                                let v0 = v00 + factor_x * (v10 - v00);
623                                let v1 = v01 + factor_x * (v11 - v01);
624                                let step = (v1 - v0) * (1.0_f32 / cell_height as f32);
625                                let mut value = v0 + step * 0.0;
626                                for values in values_by_y.iter_mut().take(y_count) {
627                                    values[channel] = value;
628                                    value += step;
629                                }
630                            }
631
632                            for y_in_cell in (0..cell_height).rev() {
633                                let world_y =
634                                    (self.cell_min_y + cell_y_idx as i32) * cell_height + y_in_cell;
635                                let relative_y = (world_y - min_y) as usize;
636                                if relative_y >= value_height {
637                                    continue;
638                                }
639
640                                interpolated[..self.interp_count].copy_from_slice(
641                                    &values_by_y[y_in_cell as usize][..self.interp_count],
642                                );
643
644                                let offset =
645                                    (relative_y * 16 * 16 + local_z * 16 + local_x) * value_count;
646                                noises.fill_material_ore_vein_values(
647                                    cache,
648                                    &interpolated[..self.interp_count],
649                                    world_x,
650                                    world_y,
651                                    world_z,
652                                    &mut values[offset..offset + value_count],
653                                );
654                            }
655                        }
656                    }
657                }
658            }
659        }
660
661        values.into_boxed_slice()
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use steel_utils::random::{Random, legacy_random::LegacyRandom, xoroshiro::Xoroshiro};
668
669    use super::NoiseChunk;
670    use crate::density::{ColumnCache, DimensionNoises, NoiseSettings};
671    use crate::density_functions::{
672        end::EndNoises, nether::NetherNoises, overworld::OverworldNoises,
673    };
674    use crate::noise::CornerColumnStore;
675    use crate::noise_parameters::get_noise_parameters;
676
677    type Visit = ((usize, i32, usize), u32);
678
679    fn create_noises<N: DimensionNoises>() -> N {
680        let seed = 1;
681        let splitter = if N::Settings::LEGACY_RANDOM_SOURCE {
682            LegacyRandom::from_seed(seed).next_positional()
683        } else {
684            Xoroshiro::from_seed(seed).next_positional()
685        };
686        N::create(seed, &splitter, &get_noise_parameters())
687    }
688
689    fn fill_visits<N: DimensionNoises>(
690        noises: &N,
691        chunk: (i32, i32),
692        air_min_y: i32,
693        corner_columns: &CornerColumnStore,
694    ) -> Vec<Visit> {
695        let (min_x, min_z) = (chunk.0 * 16, chunk.1 * 16);
696        let mut noise_chunk = NoiseChunk::<N>::new(min_x, min_z);
697        let mut cache = N::ColumnCache::default();
698        cache.init_grid(min_x, min_z, noises);
699        let mut visits = Vec::new();
700        noise_chunk.fill(
701            noises,
702            &mut cache,
703            None,
704            air_min_y,
705            corner_columns,
706            |x, y, z, density, _, _| {
707                visits.push(((x, y, z), density.to_bits()));
708            },
709        );
710        visits
711    }
712
713    /// Skipping every provable cell must leave the visited blocks and their
714    /// densities unchanged, and only drop blocks whose density is not positive.
715    #[expect(
716        clippy::neg_cmp_op_on_partial_ord,
717        reason = "NaN counts as not positive, matching the aquifer's `density > 0` solid test"
718    )]
719    fn assert_air_skip_sound<N: DimensionNoises>() {
720        let noises = create_noises::<N>();
721
722        let mut skipped = 0;
723        for chunk in [(0, 0), (-7, 3), (40, -25), (-300, -120), (1000, 800)] {
724            let all = fill_visits(&noises, chunk, i32::MAX, &CornerColumnStore::default());
725            let mut kept = fill_visits(&noises, chunk, i32::MIN, &CornerColumnStore::default())
726                .into_iter()
727                .peekable();
728            for (pos, bits) in all {
729                if kept.peek().is_some_and(|&(kept_pos, _)| kept_pos == pos) {
730                    let (_, kept_bits) = kept.next().expect("peeked");
731                    assert_eq!(kept_bits, bits, "density changed at {pos:?} in {chunk:?}");
732                } else {
733                    assert!(
734                        !(f32::from_bits(bits) > 0.0),
735                        "skipped solid block {pos:?} in {chunk:?}"
736                    );
737                    skipped += 1;
738                }
739            }
740            assert!(
741                kept.next().is_none(),
742                "skip run visited a block the full run did not"
743            );
744        }
745        assert!(skipped > 0, "no cell was skipped");
746    }
747
748    /// Neighbors filled through one shared store must match independent fills.
749    fn assert_corner_reuse_matches<N: DimensionNoises>() {
750        let noises = create_noises::<N>();
751        let shared = CornerColumnStore::default();
752        for x in -1..=1 {
753            for z in -1..=1 {
754                let chunk = (x + 30, z - 12);
755                assert_eq!(
756                    fill_visits(&noises, chunk, i32::MIN, &shared),
757                    fill_visits(&noises, chunk, i32::MIN, &CornerColumnStore::default()),
758                    "chunk {chunk:?}"
759                );
760            }
761        }
762    }
763
764    #[test]
765    fn air_column_rejects_rounding_overshoot() {
766        let lower = -5.0 * f32::from_bits(1);
767        let upper = 0.0_f32;
768        assert!(lower <= 0.0 && upper <= 0.0);
769        let step = (upper - lower) * (1.0 / 8.0);
770        let mut value = lower + step * 0.0;
771        let mut values_by_y = [[0.0; super::MAX_INTERP]; 8];
772        for values in &mut values_by_y {
773            values[0] = value;
774            value += step;
775        }
776        assert!(values_by_y[7][0] > 0.0);
777        assert!(!NoiseChunk::<OverworldNoises>::channels_are_nonpositive(
778            &values_by_y,
779            &[0],
780        ));
781    }
782
783    #[test]
784    fn overworld_corner_reuse_matches() {
785        assert_corner_reuse_matches::<OverworldNoises>();
786    }
787
788    #[test]
789    fn nether_corner_reuse_matches() {
790        assert_corner_reuse_matches::<NetherNoises>();
791    }
792
793    #[test]
794    fn end_corner_reuse_matches() {
795        assert_corner_reuse_matches::<EndNoises>();
796    }
797
798    #[test]
799    fn overworld_air_skip_is_sound() {
800        assert_air_skip_sound::<OverworldNoises>();
801    }
802
803    #[test]
804    fn nether_air_skip_is_sound() {
805        assert_air_skip_sound::<NetherNoises>();
806    }
807
808    #[test]
809    fn end_air_skip_is_sound() {
810        assert_air_skip_sound::<EndNoises>();
811    }
812}