Skip to main content

steel_worldgen/noise/
improved_noise.rs

1//! Improved Perlin noise implementation matching vanilla Minecraft's ImprovedNoise.java
2//!
3//! This is the base noise generator used by `PerlinNoise` for octave-based noise.
4
5use crate::random::Random;
6use std::ops;
7use std::simd::Simd;
8use std::simd::cmp::{SimdPartialEq, SimdPartialOrd};
9#[cfg(target_feature = "avx512f")]
10use std::simd::f64x4;
11use std::simd::num::{SimdFloat, SimdInt, SimdUint};
12use std::simd::ptr::SimdConstPtr;
13use std::simd::{Mask, Select, SimdCast, SimdElement, StdFloat};
14use steel_math::{
15    GRADIENT, fast_floor, fast_floor_simd, grad_dot, grad_dot_simd, lerp2, lerp3, lerp3_simd,
16    smoothstep, smoothstep_derivative, smoothstep_simd,
17};
18
19/// Improved Perlin noise generator.
20///
21/// This implements the improved Perlin noise algorithm as used in Minecraft.
22/// Each instance has a permutation table and offset values initialized from
23/// a random source.
24#[derive(Debug, Clone)]
25pub struct ImprovedNoise {
26    /// Permutation table (256 bytes)
27    p: [u8; 256],
28    /// X offset for the noise coordinates
29    pub xo: f64,
30    /// Y offset for the noise coordinates
31    pub yo: f64,
32    /// Z offset for the noise coordinates
33    pub zo: f64,
34    yo_floor: i32,
35    yo_fraction: f64,
36    zo_floor: i32,
37    zo_fraction: f64,
38}
39
40impl ImprovedNoise {
41    /// Creates a new `ImprovedNoise` from a random source.
42    ///
43    /// Initializes the permutation table using Fisher-Yates shuffle
44    /// and sets random offsets.
45    pub fn new<R: Random>(random: &mut R) -> Self {
46        let xo = random.next_f64() * 256.0;
47        let yo = random.next_f64() * 256.0;
48        let zo = random.next_f64() * 256.0;
49
50        let mut p = [0u8; 256];
51        #[expect(
52            clippy::needless_range_loop,
53            reason = "index is used as the initial permutation value"
54        )]
55        for i in 0..256 {
56            p[i] = i as u8;
57        }
58
59        // Fisher-Yates shuffle matching vanilla's implementation
60        for i in 0..256 {
61            let offset = random.next_i32_bounded((256 - i) as i32) as usize;
62            p.swap(i, i + offset);
63        }
64
65        let yo_floor = fast_floor(yo);
66        let yo_fraction = yo - f64::from(yo_floor);
67        let zo_floor = fast_floor(zo);
68        let zo_fraction = zo - f64::from(zo_floor);
69
70        Self {
71            p,
72            xo,
73            yo,
74            zo,
75            yo_floor,
76            yo_fraction,
77            zo_floor,
78            zo_fraction,
79        }
80    }
81
82    /// Sample noise at the given coordinates.
83    ///
84    /// This is the standard 3D Perlin noise sampling without Y scaling.
85    #[inline]
86    #[must_use]
87    pub fn noise(&self, x: f64, y: f64, z: f64) -> f64 {
88        let x = x + self.xo;
89        let y = y + self.yo;
90        let z = z + self.zo;
91
92        let xf = fast_floor(x);
93        let yf = fast_floor(y);
94        let zf = fast_floor(z);
95
96        let xr = x - f64::from(xf);
97        let yr = y - f64::from(yf);
98        let zr = z - f64::from(zf);
99
100        self.sample_and_lerp(xf, yf, zf, xr, yr, zr, yr)
101    }
102
103    /// Calculate Perlin noise using SIMD vectors.
104    #[inline]
105    #[must_use]
106    pub fn noise_simd<F, const N: usize>(
107        &self,
108        x: Simd<F, N>,
109        y: Simd<F, N>,
110        z: Simd<F, N>,
111    ) -> Simd<F, N>
112    where
113        F: SimdElement + SimdCast,
114        Simd<F, N>: SimdFloat<Cast<i32> = Simd<i32, N>>
115            + SimdPartialOrd
116            + SimdPartialEq<Mask = Mask<<F as SimdElement>::Mask, N>>
117            + ops::Add<Output = Simd<F, N>>
118            + ops::Sub<Output = Simd<F, N>>
119            + ops::Mul<Output = Simd<F, N>>
120            + ops::Neg<Output = Simd<F, N>>,
121    {
122        let x = x + Simd::splat(self.xo).cast();
123        let y = y + Simd::splat(self.yo).cast();
124        let z = z + Simd::splat(self.zo).cast();
125
126        let xf = fast_floor_simd::<F, i32, N>(x);
127        let yf = fast_floor_simd::<F, i32, N>(y);
128        let zf = fast_floor_simd::<F, i32, N>(z);
129
130        let xr = x - xf.cast();
131        let yr = y - yf.cast();
132        let zr = z - zf.cast();
133
134        self.sample_and_lerp_simd(xf, yf, zf, xr, yr, zr, yr)
135    }
136
137    /// Sample noise at `(x, 0.0, z)`.
138    #[inline]
139    #[must_use]
140    pub fn noise_xz(&self, x: f64, z: f64) -> f64 {
141        let x = x + self.xo;
142        let z = z + self.zo;
143
144        let xf = fast_floor(x);
145        let zf = fast_floor(z);
146
147        let xr = x - f64::from(xf);
148        let zr = z - f64::from(zf);
149
150        self.sample_and_lerp(
151            xf,
152            self.yo_floor,
153            zf,
154            xr,
155            self.yo_fraction,
156            zr,
157            self.yo_fraction,
158        )
159    }
160
161    /// Sample noise at `(x, y, 0.0)`.
162    #[inline]
163    #[must_use]
164    pub fn noise_xy(&self, x: f64, y: f64) -> f64 {
165        let x = x + self.xo;
166        let y = y + self.yo;
167
168        let xf = fast_floor(x);
169        let yf = fast_floor(y);
170
171        let xr = x - f64::from(xf);
172        let yr = y - f64::from(yf);
173
174        self.sample_and_lerp(xf, yf, self.zo_floor, xr, yr, self.zo_fraction, yr)
175    }
176
177    /// Sample noise at the given coordinates, accumulating partial derivatives.
178    ///
179    /// Returns the noise value and adds the partial derivatives (dx, dy, dz)
180    /// into `derivative_out`. Used by `BlendedNoise` for terrain generation.
181    #[must_use]
182    pub fn noise_with_derivative(
183        &self,
184        x: f64,
185        y: f64,
186        z: f64,
187        derivative_out: &mut [f64; 3],
188    ) -> f64 {
189        let x = x + self.xo;
190        let y = y + self.yo;
191        let z = z + self.zo;
192
193        let xf = fast_floor(x);
194        let yf = fast_floor(y);
195        let zf = fast_floor(z);
196
197        let xr = x - f64::from(xf);
198        let yr = y - f64::from(yf);
199        let zr = z - f64::from(zf);
200
201        self.sample_with_derivative(xf, yf, zf, xr, yr, zr, derivative_out)
202    }
203
204    /// Sample noise with Y scale and fudge parameters.
205    ///
206    /// The `y_scale` and `y_fudge` parameters are used for terrain generation
207    /// where vertical noise needs special handling.
208    ///
209    /// # Arguments
210    /// * `x`, `y`, `z` - The coordinates to sample
211    /// * `y_scale` - Y scaling factor (0.0 to disable)
212    /// * `y_fudge` - Y fudge factor for floor snapping
213    #[must_use]
214    #[expect(
215        clippy::similar_names,
216        reason = "yr_fudge and y_fudge match vanilla naming"
217    )]
218    pub fn noise_with_y_scale(&self, x: f64, y: f64, z: f64, y_scale: f64, y_fudge: f64) -> f64 {
219        let x = x + self.xo;
220        let y = y + self.yo;
221        let z = z + self.zo;
222
223        let xf = fast_floor(x);
224        let yf = fast_floor(y);
225        let zf = fast_floor(z);
226
227        let xr = x - f64::from(xf);
228        let yr = y - f64::from(yf);
229        let zr = z - f64::from(zf);
230
231        // Calculate Y fudge for terrain generation
232        #[expect(
233            clippy::if_not_else,
234            reason = "matches vanilla's conditional structure"
235        )]
236        let yr_fudge = if y_scale != 0.0 {
237            let fudge_limit = if y_fudge >= 0.0 && y_fudge < yr {
238                y_fudge
239            } else {
240                yr
241            };
242            // SHIFT_UP_EPSILON = 1.0E-7F in Java (float literal promoted to double)
243            (fudge_limit / y_scale + f64::from(1.0e-7_f32)).floor() * y_scale
244        } else {
245            0.0
246        };
247        self.sample_and_lerp(xf, yf, zf, xr, yr - yr_fudge, zr, yr)
248    }
249
250    /// Sample noise at grid point and interpolate.
251    ///
252    /// The 8 corner gradient-dot products are evaluated as 2 × `f64x4` so the
253    /// per-lane math stays identical to the scalar path (`((gx*xr) + (gy*yr)) + (gz*zr)`),
254    /// which preserves bit-identical output. Inspired by C2ME's `c2me-opts-math`
255    /// flat-gradient SIMD form.
256    #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
257    fn sample_and_lerp(
258        &self,
259        x: i32,
260        y: i32,
261        z: i32,
262        xr: f64,
263        yr: f64,
264        zr: f64,
265        yr_original: f64,
266    ) -> f64 {
267        let (d000, d100, d010, d110, d001, d101, d011, d111) = {
268            #[cfg(target_feature = "avx512f")]
269            {
270                let x = x as u8;
271                let y = y as u8;
272                let z = z as u8;
273                let x0 = self.p[x as usize];
274                let x1 = self.p[x.wrapping_add(1) as usize];
275
276                let xy00 = self.p[x0.wrapping_add(y) as usize];
277                let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
278                let xy10 = self.p[x1.wrapping_add(y) as usize];
279                let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
280
281                let h_z0 = [
282                    self.p[xy00.wrapping_add(z) as usize] as usize,
283                    self.p[xy10.wrapping_add(z) as usize] as usize,
284                    self.p[xy01.wrapping_add(z) as usize] as usize,
285                    self.p[xy11.wrapping_add(z) as usize] as usize,
286                ];
287                let h_z1 = [
288                    self.p[xy00.wrapping_add(z).wrapping_add(1) as usize] as usize,
289                    self.p[xy10.wrapping_add(z).wrapping_add(1) as usize] as usize,
290                    self.p[xy01.wrapping_add(z).wrapping_add(1) as usize] as usize,
291                    self.p[xy11.wrapping_add(z).wrapping_add(1) as usize] as usize,
292                ];
293
294                let xr_v = f64x4::from_array([xr, xr - 1.0, xr, xr - 1.0]);
295                let yr_v = f64x4::from_array([yr, yr, yr - 1.0, yr - 1.0]);
296                let zr_v0 = f64x4::splat(zr);
297                let zr_v1 = f64x4::splat(zr - 1.0);
298
299                let [d000, d100, d010, d110] = grad_dot_simd(h_z0, xr_v, yr_v, zr_v0).to_array();
300                let [d001, d101, d011, d111] = grad_dot_simd(h_z1, xr_v, yr_v, zr_v1).to_array();
301                (d000, d100, d010, d110, d001, d101, d011, d111)
302            }
303
304            #[cfg(not(target_feature = "avx512f"))]
305            {
306                let px1 = x.wrapping_add(1);
307                let py1 = y.wrapping_add(1);
308                let pz1 = z.wrapping_add(1);
309                let xr1 = xr - 1.0;
310                let yr1 = yr - 1.0;
311                let zr1 = zr - 1.0;
312
313                let d000 = grad_dot_flat(&self.p, x, y, z, xr, yr, zr);
314                let d100 = grad_dot_flat(&self.p, px1, y, z, xr1, yr, zr);
315                let d010 = grad_dot_flat(&self.p, x, py1, z, xr, yr1, zr);
316                let d110 = grad_dot_flat(&self.p, px1, py1, z, xr1, yr1, zr);
317                let d001 = grad_dot_flat(&self.p, x, y, pz1, xr, yr, zr1);
318                let d101 = grad_dot_flat(&self.p, px1, y, pz1, xr1, yr, zr1);
319                let d011 = grad_dot_flat(&self.p, x, py1, pz1, xr, yr1, zr1);
320                let d111 = grad_dot_flat(&self.p, px1, py1, pz1, xr1, yr1, zr1);
321                (d000, d100, d010, d110, d001, d101, d011, d111)
322            }
323        };
324
325        let alpha_x = smoothstep(xr);
326        let alpha_y = smoothstep(yr_original);
327        let alpha_z = smoothstep(zr);
328        lerp3(
329            alpha_x, alpha_y, alpha_z, d000, d100, d010, d110, d001, d101, d011, d111,
330        )
331    }
332
333    #[inline]
334    fn p_simd<const N: usize>(&self, idx: Simd<u8, N>) -> Simd<u8, N> {
335        let offset = idx.cast::<usize>();
336        let p = Simd::splat(self.p.as_ptr()).wrapping_add(offset);
337        // SAFETY: `idx` is a `Simd<u8, N>`, meaning each lane's index is at most 255.
338        // `self.p` has length 256, so all offsets are guaranteed to be within bounds of `self.p`.
339        unsafe { Simd::gather_ptr(p) }
340    }
341
342    /// Sample noise at grid point and interpolate.
343    #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
344    fn sample_and_lerp_simd<F, const N: usize>(
345        &self,
346        x: Simd<i32, N>,
347        y: Simd<i32, N>,
348        z: Simd<i32, N>,
349        xr: Simd<F, N>,
350        yr: Simd<F, N>,
351        zr: Simd<F, N>,
352        yr_original: Simd<F, N>,
353    ) -> Simd<F, N>
354    where
355        F: SimdElement + SimdCast,
356        Simd<F, N>: ops::Mul<Output = Simd<F, N>>
357            + ops::Add<Output = Simd<F, N>>
358            + ops::Sub<Output = Simd<F, N>>
359            + ops::Neg<Output = Simd<F, N>>,
360    {
361        let x = x.cast::<u8>();
362        let y = y.cast::<u8>();
363        let z = z.cast::<u8>();
364        // Get permutation indices for the 8 corners
365        let x0 = self.p_simd(x);
366        let x1 = self.p_simd(x + Simd::splat(1));
367
368        let xy00 = self.p_simd(x0 + y);
369        let xy01 = self.p_simd(x0 + y + Simd::splat(1));
370        let xy10 = self.p_simd(x1 + y);
371        let xy11 = self.p_simd(x1 + y + Simd::splat(1));
372
373        let h000 = self.p_simd(xy00 + z).cast::<usize>().to_array();
374        let h100 = self.p_simd(xy10 + z).cast::<usize>().to_array();
375        let h010 = self.p_simd(xy01 + z).cast::<usize>().to_array();
376        let h110 = self.p_simd(xy11 + z).cast::<usize>().to_array();
377        let h001 = self
378            .p_simd(xy00 + z + Simd::splat(1))
379            .cast::<usize>()
380            .to_array();
381        let h101 = self
382            .p_simd(xy10 + z + Simd::splat(1))
383            .cast::<usize>()
384            .to_array();
385        let h011 = self
386            .p_simd(xy01 + z + Simd::splat(1))
387            .cast::<usize>()
388            .to_array();
389        let h111 = self
390            .p_simd(xy11 + z + Simd::splat(1))
391            .cast::<usize>()
392            .to_array();
393
394        // Calculate gradient dot products at each corner
395        let d000 = grad_dot_simd(h000, xr, yr, zr);
396        let d100 = grad_dot_simd(h100, xr - Simd::splat(1.0).cast::<F>(), yr, zr);
397        let d010 = grad_dot_simd(h010, xr, yr - Simd::splat(1.0).cast::<F>(), zr);
398        let d110 = grad_dot_simd(
399            h110,
400            xr - Simd::splat(1.0).cast::<F>(),
401            yr - Simd::splat(1.0).cast::<F>(),
402            zr,
403        );
404        let d001 = grad_dot_simd(h001, xr, yr, zr - Simd::splat(1.0).cast::<F>());
405        let d101 = grad_dot_simd(
406            h101,
407            xr - Simd::splat(1.0).cast::<F>(),
408            yr,
409            zr - Simd::splat(1.0).cast::<F>(),
410        );
411        let d011 = grad_dot_simd(
412            h011,
413            xr,
414            yr - Simd::splat(1.0).cast::<F>(),
415            zr - Simd::splat(1.0).cast::<F>(),
416        );
417        let d111 = grad_dot_simd(
418            h111,
419            xr - Simd::splat(1.0).cast::<F>(),
420            yr - Simd::splat(1.0).cast::<F>(),
421            zr - Simd::splat(1.0).cast::<F>(),
422        );
423
424        // Apply smoothstep interpolation
425        let x_alpha = smoothstep_simd(xr);
426        let y_alpha = smoothstep_simd(yr_original);
427        let z_alpha = smoothstep_simd(zr);
428
429        lerp3_simd(
430            x_alpha, y_alpha, z_alpha, d000, d100, d010, d110, d001, d101, d011, d111,
431        )
432    }
433
434    /// Generic N-lane form of [`Self::noise_with_y_scale_4x`]. Each lane runs the
435    /// exact per-lane math of the scalar [`Self::noise_with_y_scale`], so any
436    /// supported lane width yields bit-identical per-lane results — only the
437    /// SIMD batch size changes. `f64x4` ≡ `noise_with_y_scale_simd::<4>`.
438    #[inline]
439    #[must_use]
440    pub fn noise_with_y_scale_simd<const N: usize>(
441        &self,
442        x: f64,
443        ys: Simd<f64, N>,
444        z: f64,
445        y_scale: f64,
446        y_fudges: Simd<f64, N>,
447    ) -> Simd<f64, N> {
448        // Shared x/z offset and floor
449        let x = x + self.xo;
450        let z = z + self.zo;
451        let xf = fast_floor(x);
452        let zf = fast_floor(z);
453        let xr = x - f64::from(xf);
454        let zr = z - f64::from(zf);
455
456        // Per-lane y offset and floor
457        let ys = ys + Simd::splat(self.yo);
458        let ys_floor = fast_floor_simd::<f64, i32, N>(ys);
459        let yrs = ys - ys_floor.cast();
460
461        // Y fudge (per-lane)
462        let yr_fudge: Simd<f64, N> = if y_scale == 0.0 {
463            Simd::splat(0.0)
464        } else {
465            let y_scale_v: Simd<f64, N> = Simd::splat(y_scale);
466            let zero: Simd<f64, N> = Simd::splat(0.0);
467            let mask = y_fudges.simd_ge(zero) & y_fudges.simd_lt(yrs);
468            let fudge_limits = mask.select(y_fudges, yrs);
469            let epsilon: Simd<f64, N> = Simd::splat(f64::from(1.0e-7_f32));
470            ((fudge_limits / y_scale_v) + epsilon).floor() * y_scale_v
471        };
472
473        let yrs_adjusted = yrs - yr_fudge;
474
475        self.sample_and_lerp_y_simd(xf, zf, xr, zr, ys_floor, yrs_adjusted, yrs)
476    }
477
478    /// Vectorized sample-and-lerp for N Y values sharing x/z grid position.
479    /// Generic counterpart of [`Self::sample_and_lerp_4x`].
480    #[expect(
481        clippy::too_many_arguments,
482        reason = "mirrors scalar sample_and_lerp with Nx SIMD y-batching"
483    )]
484    #[inline]
485    fn sample_and_lerp_y_simd<const N: usize>(
486        &self,
487        xf: i32,
488        zf: i32,
489        xr: f64,
490        zr: f64,
491        ys_floor: Simd<i32, N>,
492        yrs: Simd<f64, N>,
493        yrs_original: Simd<f64, N>,
494    ) -> Simd<f64, N> {
495        let xf = xf as u8;
496        let zf = zf as u8;
497        // Shared x permutation lookups (2 instead of 2×N)
498        let x0 = self.p[xf as usize];
499        let x1 = self.p[xf.wrapping_add(1) as usize];
500
501        let yf = ys_floor.cast();
502
503        // Per-lane y-dependent permutation lookups
504        let mut h000 = [0usize; N];
505        let mut h100 = [0usize; N];
506        let mut h010 = [0usize; N];
507        let mut h110 = [0usize; N];
508        let mut h001 = [0usize; N];
509        let mut h101 = [0usize; N];
510        let mut h011 = [0usize; N];
511        let mut h111 = [0usize; N];
512
513        for i in 0..N {
514            let y = yf[i];
515            let xy00 = self.p[x0.wrapping_add(y) as usize];
516            let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
517            let xy10 = self.p[x1.wrapping_add(y) as usize];
518            let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
519            h000[i] = self.p[xy00.wrapping_add(zf) as usize] as usize;
520            h100[i] = self.p[xy10.wrapping_add(zf) as usize] as usize;
521            h010[i] = self.p[xy01.wrapping_add(zf) as usize] as usize;
522            h110[i] = self.p[xy11.wrapping_add(zf) as usize] as usize;
523            h001[i] = self.p[xy00.wrapping_add(zf).wrapping_add(1) as usize] as usize;
524            h101[i] = self.p[xy10.wrapping_add(zf).wrapping_add(1) as usize] as usize;
525            h011[i] = self.p[xy01.wrapping_add(zf).wrapping_add(1) as usize] as usize;
526            h111[i] = self.p[xy11.wrapping_add(zf).wrapping_add(1) as usize] as usize;
527        }
528
529        // Vectorized gradient dot products
530        let xr_v: Simd<f64, N> = Simd::splat(xr);
531        let zr_v: Simd<f64, N> = Simd::splat(zr);
532        let one: Simd<f64, N> = Simd::splat(1.0);
533        let xr_m1 = xr_v - one;
534        let yr_m1 = yrs - one;
535        let zr_m1 = zr_v - one;
536
537        let d000 = grad_dot_simd(h000, xr_v, yrs, zr_v);
538        let d100 = grad_dot_simd(h100, xr_m1, yrs, zr_v);
539        let d010 = grad_dot_simd(h010, xr_v, yr_m1, zr_v);
540        let d110 = grad_dot_simd(h110, xr_m1, yr_m1, zr_v);
541        let d001 = grad_dot_simd(h001, xr_v, yrs, zr_m1);
542        let d101 = grad_dot_simd(h101, xr_m1, yrs, zr_m1);
543        let d011 = grad_dot_simd(h011, xr_v, yr_m1, zr_m1);
544        let d111 = grad_dot_simd(h111, xr_m1, yr_m1, zr_m1);
545
546        // Smoothstep — x and z are shared across lanes
547        let x_alpha: Simd<f64, N> = Simd::splat(smoothstep(xr));
548        let y_alpha = smoothstep_simd(yrs_original);
549        let z_alpha: Simd<f64, N> = Simd::splat(smoothstep(zr));
550
551        lerp3_simd(
552            x_alpha, y_alpha, z_alpha, d000, d100, d010, d110, d001, d101, d011, d111,
553        )
554    }
555
556    /// Sample noise at grid point, interpolate, and accumulate derivatives.
557    #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
558    fn sample_with_derivative(
559        &self,
560        x: i32,
561        y: i32,
562        z: i32,
563        xr: f64,
564        yr: f64,
565        zr: f64,
566        derivative_out: &mut [f64; 3],
567    ) -> f64 {
568        let x = x as u8;
569        let y = y as u8;
570        let z = z as u8;
571
572        let x0 = self.p[x as usize];
573        let x1 = self.p[x.wrapping_add(1) as usize];
574        let xy00 = self.p[x0.wrapping_add(y) as usize];
575        let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
576        let xy10 = self.p[x1.wrapping_add(y) as usize];
577        let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
578
579        let h000 = self.p[xy00.wrapping_add(z) as usize] as usize;
580        let h100 = self.p[xy10.wrapping_add(z) as usize] as usize;
581        let h010 = self.p[xy01.wrapping_add(z) as usize] as usize;
582        let h110 = self.p[xy11.wrapping_add(z) as usize] as usize;
583        let h001 = self.p[xy00.wrapping_add(z).wrapping_add(1) as usize] as usize;
584        let h101 = self.p[xy10.wrapping_add(z).wrapping_add(1) as usize] as usize;
585        let h011 = self.p[xy01.wrapping_add(z).wrapping_add(1) as usize] as usize;
586        let h111 = self.p[xy11.wrapping_add(z).wrapping_add(1) as usize] as usize;
587
588        let g000 = Simd::from_array(GRADIENT[h000 & 15]);
589        let g100 = Simd::from_array(GRADIENT[h100 & 15]);
590        let g010 = Simd::from_array(GRADIENT[h010 & 15]);
591        let g110 = Simd::from_array(GRADIENT[h110 & 15]);
592        let g001 = Simd::from_array(GRADIENT[h001 & 15]);
593        let g101 = Simd::from_array(GRADIENT[h101 & 15]);
594        let g011 = Simd::from_array(GRADIENT[h011 & 15]);
595        let g111 = Simd::from_array(GRADIENT[h111 & 15]);
596
597        // Gradient dot products at each corner
598        let d000 = grad_dot(h000, xr, yr, zr);
599        let d100 = grad_dot(h100, xr - 1.0, yr, zr);
600        let d010 = grad_dot(h010, xr, yr - 1.0, zr);
601        let d110 = grad_dot(h110, xr - 1.0, yr - 1.0, zr);
602        let d001 = grad_dot(h001, xr, yr, zr - 1.0);
603        let d101 = grad_dot(h101, xr - 1.0, yr, zr - 1.0);
604        let d011 = grad_dot(h011, xr, yr - 1.0, zr - 1.0);
605        let d111 = grad_dot(h111, xr - 1.0, yr - 1.0, zr - 1.0);
606
607        let alpha_x = smoothstep(xr);
608        let alpha_y = smoothstep(yr);
609        let alpha_z = smoothstep(zr);
610
611        // Interpolate gradient components for direct derivative contribution
612        let d1_v = lerp3_simd(
613            Simd::splat(alpha_x),
614            Simd::splat(alpha_y),
615            Simd::splat(alpha_z),
616            g000,
617            g100,
618            g010,
619            g110,
620            g001,
621            g101,
622            g011,
623            g111,
624        );
625
626        // Smoothstep correction terms via differences
627        let d2x = lerp2(
628            alpha_y,
629            alpha_z,
630            d100 - d000,
631            d110 - d010,
632            d101 - d001,
633            d111 - d011,
634        );
635        let d2y = lerp2(
636            alpha_z,
637            alpha_x,
638            d010 - d000,
639            d011 - d001,
640            d110 - d100,
641            d111 - d101,
642        );
643        let d2z = lerp2(
644            alpha_x,
645            alpha_y,
646            d001 - d000,
647            d101 - d100,
648            d011 - d010,
649            d111 - d110,
650        );
651
652        let x_sd = smoothstep_derivative(xr);
653        let y_sd = smoothstep_derivative(yr);
654        let z_sd = smoothstep_derivative(zr);
655
656        // Accumulate derivatives (vanilla uses +=)
657        derivative_out[0] += d1_v[0] + x_sd * d2x;
658        derivative_out[1] += d1_v[1] + y_sd * d2y;
659        derivative_out[2] += d1_v[2] + z_sd * d2z;
660        lerp3(
661            alpha_x, alpha_y, alpha_z, d000, d100, d010, d110, d001, d101, d011, d111,
662        )
663    }
664}
665
666/// Helps the compiler factor permutation reads in the scalar path.
667#[cfg(not(target_feature = "avx512f"))]
668#[inline]
669fn grad_dot_flat(p: &[u8; 256], px: i32, py: i32, pz: i32, fx: f64, fy: f64, fz: f64) -> f64 {
670    let qx = (px & 0xFF) as u8;
671    let qy = (py & 0xFF) as u8;
672    let qz = (pz & 0xFF) as u8;
673    let a = p[qx as usize];
674    let b = p[a.wrapping_add(qy) as usize];
675    let hash = p[b.wrapping_add(qz) as usize];
676    grad_dot(hash as usize, fx, fy, fz)
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use crate::random::xoroshiro::Xoroshiro;
683    use std::simd::f64x4;
684
685    #[test]
686    fn test_noise_with_y_scale_4x_matches_scalar() {
687        let mut rng = Xoroshiro::from_seed(42);
688        let noise = ImprovedNoise::new(&mut rng);
689
690        // Test various coordinate combinations
691        let test_x_zs: &[(f64, f64)] = &[
692            (0.0, 0.0),
693            (1.5, 3.7),
694            (-5.2, 100.3),
695            (0.001, -0.001),
696            (1000.0, -500.0),
697        ];
698        let test_ys: &[[f64; 4]] = &[
699            [0.0, 1.0, 2.0, 3.0],
700            [64.0, 64.5, 65.0, 65.5],
701            [-5.0, -2.5, 0.0, 2.5],
702            [0.25, 0.5, 0.75, 1.0],
703            [-100.0, -50.0, 50.0, 100.0],
704        ];
705        let y_scales = [0.0, 1.0, 8.0];
706
707        for &(x, z) in test_x_zs {
708            for ys in test_ys {
709                for &y_scale in &y_scales {
710                    let y_fudges: [f64; 4] = if y_scale == 0.0 {
711                        [0.0; 4]
712                    } else {
713                        *ys // use ys as fudge values (matching BlendedNoise usage)
714                    };
715
716                    let simd_result = noise.noise_with_y_scale_simd(
717                        x,
718                        f64x4::from_array(*ys),
719                        z,
720                        y_scale,
721                        f64x4::from_array(y_fudges),
722                    );
723
724                    for i in 0..4 {
725                        let scalar = noise.noise_with_y_scale(x, ys[i], z, y_scale, y_fudges[i]);
726                        let simd_val = simd_result[i];
727                        assert!(
728                            (scalar - simd_val).abs() < 1e-14,
729                            "Mismatch at x={x}, y={}, z={z}, y_scale={y_scale}: \
730                             scalar={scalar}, simd={simd_val}, diff={}",
731                            ys[i],
732                            (scalar - simd_val).abs(),
733                        );
734                    }
735                }
736            }
737        }
738    }
739
740    #[test]
741    fn test_noise_simd_matches_scalar() {
742        let mut rng = Xoroshiro::from_seed(42);
743        let noise = ImprovedNoise::new(&mut rng);
744
745        let batches = [
746            (
747                [0.0, 1.25, -5.5, 1000.75],
748                [0.0, 64.5, -20.25, 255.75],
749                [0.0, -30.75, 4096.5, -1000.25],
750            ),
751            (
752                [
753                    255.25 - noise.xo,
754                    256.25 - noise.xo,
755                    -1.75 - noise.xo,
756                    -256.25 - noise.xo,
757                ],
758                [
759                    255.5 - noise.yo,
760                    256.5 - noise.yo,
761                    -1.5 - noise.yo,
762                    -256.5 - noise.yo,
763                ],
764                [
765                    255.75 - noise.zo,
766                    256.75 - noise.zo,
767                    -1.25 - noise.zo,
768                    -256.25 - noise.zo,
769                ],
770            ),
771        ];
772
773        for (xs, ys, zs) in batches {
774            let simd = noise.noise_simd(
775                f64x4::from_array(xs),
776                f64x4::from_array(ys),
777                f64x4::from_array(zs),
778            );
779            for i in 0..4 {
780                let scalar = noise.noise(xs[i], ys[i], zs[i]);
781                #[expect(
782                    clippy::float_cmp,
783                    reason = "SIMD path must be bit-identical to scalar noise for vanilla determinism"
784                )]
785                let matches = scalar == simd[i];
786                assert!(
787                    matches,
788                    "Mismatch at ({}, {}, {}): scalar={}, simd={}",
789                    xs[i], ys[i], zs[i], scalar, simd[i],
790                );
791            }
792        }
793    }
794
795    #[test]
796    fn test_noise_with_y_scale_simd8_matches_scalar() {
797        use std::simd::f64x8;
798
799        let mut rng = Xoroshiro::from_seed(42);
800        let noise = ImprovedNoise::new(&mut rng);
801
802        let test_x_zs: &[(f64, f64)] = &[
803            (0.0, 0.0),
804            (1.5, 3.7),
805            (-5.2, 100.3),
806            (0.001, -0.001),
807            (1000.0, -500.0),
808        ];
809        let test_ys: &[[f64; 8]] = &[
810            [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
811            [64.0, 64.5, 65.0, 65.5, 66.0, 66.5, 67.0, 67.5],
812            [-5.0, -2.5, 0.0, 2.5, 5.0, 7.5, 10.0, 12.5],
813            [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
814            [-100.0, -50.0, -25.0, -10.0, 10.0, 25.0, 50.0, 100.0],
815        ];
816        let y_scales = [0.0, 1.0, 8.0];
817
818        for &(x, z) in test_x_zs {
819            for ys in test_ys {
820                for &y_scale in &y_scales {
821                    let y_fudges: [f64; 8] = if y_scale == 0.0 { [0.0; 8] } else { *ys };
822
823                    let simd_result = noise.noise_with_y_scale_simd(
824                        x,
825                        f64x8::from_array(*ys),
826                        z,
827                        y_scale,
828                        f64x8::from_array(y_fudges),
829                    );
830
831                    for i in 0..8 {
832                        let scalar = noise.noise_with_y_scale(x, ys[i], z, y_scale, y_fudges[i]);
833                        let simd_val = simd_result[i];
834                        assert!(
835                            (scalar - simd_val).abs() < 1e-14,
836                            "Mismatch at x={x}, y={}, z={z}, y_scale={y_scale}: \
837                             scalar={scalar}, simd={simd_val}, diff={}",
838                            ys[i],
839                            (scalar - simd_val).abs(),
840                        );
841                    }
842                }
843            }
844        }
845    }
846
847    #[test]
848    fn test_improved_noise_deterministic() {
849        let mut rng1 = Xoroshiro::from_seed(12345);
850        let mut rng2 = Xoroshiro::from_seed(12345);
851
852        let noise1 = ImprovedNoise::new(&mut rng1);
853        let noise2 = ImprovedNoise::new(&mut rng2);
854
855        // Same seed should produce same noise
856        #[expect(
857            clippy::float_cmp,
858            reason = "determinism test: identical seeds must produce bit-identical offsets"
859        )]
860        {
861            assert_eq!(noise1.xo, noise2.xo);
862            assert_eq!(noise1.yo, noise2.yo);
863            assert_eq!(noise1.zo, noise2.zo);
864        }
865        assert_eq!(noise1.p, noise2.p);
866
867        // Same coordinates should produce same values
868        let v1 = noise1.noise(100.0, 64.0, 100.0);
869        let v2 = noise2.noise(100.0, 64.0, 100.0);
870        assert!((v1 - v2).abs() < 1e-15);
871    }
872
873    #[test]
874    fn scalar_noise_wraps_corner_coordinates_at_i32_max() {
875        let mut rng = Xoroshiro::from_seed(42);
876        let mut noise = ImprovedNoise::new(&mut rng);
877        noise.xo = 0.0;
878        noise.yo = 0.0;
879        noise.zo = 0.0;
880
881        let _ = noise.noise(
882            f64::from(i32::MAX),
883            f64::from(i32::MAX),
884            f64::from(i32::MAX),
885        );
886    }
887
888    #[test]
889    fn test_noise_matches_zero_y_scale_path() {
890        let mut rng = Xoroshiro::from_seed(42);
891        let noise = ImprovedNoise::new(&mut rng);
892
893        for (x, y, z) in [
894            (0.0, 0.0, 0.0),
895            (1.25, 64.5, -30.75),
896            (-1000.0, -20.25, 4096.5),
897        ] {
898            assert!(
899                (noise.noise(x, y, z) - noise.noise_with_y_scale(x, y, z, 0.0, 0.0)).abs() < 1e-15
900            );
901        }
902    }
903
904    #[test]
905    fn test_zero_axis_helpers_match_full_noise() {
906        let mut rng = Xoroshiro::from_seed(12_345);
907        let noise = ImprovedNoise::new(&mut rng);
908        let samples = [
909            (0.0, 0.0),
910            (1.25, -30.75),
911            (-1000.0, 4096.5),
912            (33_554_431.5, -33_554_432.25),
913            (-0.000_000_1, 0.000_000_1),
914        ];
915
916        for &(a, b) in &samples {
917            #[expect(
918                clippy::float_cmp,
919                reason = "zero-axis helpers must be bit-identical to the full scalar path"
920            )]
921            {
922                assert_eq!(noise.noise_xz(a, b), noise.noise(a, 0.0, b));
923                assert_eq!(noise.noise_xy(a, b), noise.noise(a, b, 0.0));
924            }
925        }
926    }
927
928    #[test]
929    fn test_improved_noise_range() {
930        let mut rng = Xoroshiro::from_seed(42);
931        let noise = ImprovedNoise::new(&mut rng);
932
933        // Sample at various points and verify output is in reasonable range
934        for x in -10..10 {
935            for z in -10..10 {
936                let v = noise.noise(f64::from(x) * 10.0, 64.0, f64::from(z) * 10.0);
937                // Perlin noise should be in [-1, 1] range roughly
938                assert!(
939                    (-1.5..=1.5).contains(&v),
940                    "Noise value {v} at ({x}, {z}) out of expected range",
941                );
942            }
943        }
944    }
945
946    #[test]
947    fn test_improved_noise_spatial_variation() {
948        let mut rng = Xoroshiro::from_seed(42);
949        let noise = ImprovedNoise::new(&mut rng);
950
951        // Noise at different positions should generally be different
952        let v1 = noise.noise(0.0, 0.0, 0.0);
953        let v2 = noise.noise(100.0, 0.0, 0.0);
954        let v3 = noise.noise(0.0, 100.0, 0.0);
955        let v4 = noise.noise(0.0, 0.0, 100.0);
956
957        // At least some should be different (statistically almost certain)
958        #[expect(
959            clippy::float_cmp,
960            reason = "intentional exact equality check to detect degenerate constant noise"
961        )]
962        let all_same = v1 == v2 && v2 == v3 && v3 == v4;
963        assert!(!all_same, "All noise values are the same - unexpected");
964    }
965
966    #[test]
967    fn test_noise_with_derivative_matches_noise() {
968        let mut rng = Xoroshiro::from_seed(42);
969        let noise = ImprovedNoise::new(&mut rng);
970
971        // noise_with_derivative should return the same value as noise()
972        // (when no y_scale/y_fudge is used)
973        for &(x, y, z) in &[
974            (0.0, 0.0, 0.0),
975            (1.5, 2.3, 3.7),
976            (-5.2, 64.0, 100.3),
977            (0.25, 0.25, 0.25),
978        ] {
979            let v1 = noise.noise(x, y, z);
980            let mut deriv = [0.0; 3];
981            let v2 = noise.noise_with_derivative(x, y, z, &mut deriv);
982            assert!(
983                (v1 - v2).abs() < 1e-12,
984                "Value mismatch at ({x}, {y}, {z}): {v1} vs {v2}",
985            );
986        }
987    }
988
989    #[test]
990    fn test_noise_with_derivative_produces_derivatives() {
991        let mut rng = Xoroshiro::from_seed(42);
992        let noise = ImprovedNoise::new(&mut rng);
993
994        let mut deriv = [0.0; 3];
995        let _ = noise.noise_with_derivative(1.5, 2.3, 3.7, &mut deriv);
996
997        // At a non-grid point, at least some derivatives should be nonzero
998        let any_nonzero = deriv.iter().any(|&d| d.abs() > 1e-15);
999        assert!(any_nonzero, "All derivatives are zero: {deriv:?}");
1000    }
1001
1002    #[test]
1003    fn test_noise_with_derivative_accumulates() {
1004        let mut rng = Xoroshiro::from_seed(42);
1005        let noise = ImprovedNoise::new(&mut rng);
1006
1007        // First call
1008        let mut deriv = [0.0; 3];
1009        let _ = noise.noise_with_derivative(1.5, 2.3, 3.7, &mut deriv);
1010        let first = deriv;
1011
1012        // Second call should accumulate (+=)
1013        let _ = noise.noise_with_derivative(4.1, 5.2, 6.3, &mut deriv);
1014        let mut deriv2 = [0.0; 3];
1015        let _ = noise.noise_with_derivative(4.1, 5.2, 6.3, &mut deriv2);
1016
1017        for i in 0..3 {
1018            let expected = first[i] + deriv2[i];
1019            assert!(
1020                (deriv[i] - expected).abs() < 1e-12,
1021                "Derivative[{i}] not accumulated: {0} vs expected {expected}",
1022                deriv[i],
1023            );
1024        }
1025    }
1026}