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