Skip to main content

steel_math/noise_math/
coordinate.rs

1use std::{
2    ops,
3    simd::{
4        Mask, Select, Simd, SimdCast, SimdElement, StdFloat,
5        cmp::{SimdPartialEq, SimdPartialOrd},
6        num::{SimdFloat, SimdInt},
7    },
8};
9
10/// Floor function that matches Java behavior.
11///
12/// In Java, `(int)v` truncates toward zero, but we need floor behavior.
13/// For negative values, we need to subtract 1 if there's a fractional part.
14///
15/// Fast Floor from Stefan Gustavson's in "Simplex Noise Demystified" 2005 paper
16///
17/// Java reference: `Mth.floor(double)`
18#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
19#[inline(always)]
20#[must_use]
21pub fn fast_floor(v: f64) -> i32 {
22    let i = v as i32;
23    if v < f64::from(i) { i - 1 } else { i }
24}
25
26/// SIMD implementation of `fast_floor`.
27#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
28#[inline(always)]
29#[must_use]
30pub fn fast_floor_simd<F, I, const N: usize>(v: Simd<F, N>) -> Simd<I, N>
31where
32    F: SimdElement + SimdCast,
33    I: SimdElement + SimdCast,
34    Simd<F, N>: SimdFloat<Cast<I> = Simd<I, N>>
35        + SimdPartialOrd
36        + SimdPartialEq<Mask = Mask<<F as SimdElement>::Mask, N>>,
37    Simd<I, N>: SimdInt<Cast<F> = Simd<F, N>> + ops::Sub<Output = Simd<I, N>>,
38{
39    let i = v.cast::<I>();
40    let b = v.simd_lt(i.cast::<F>());
41    b.select(i - Simd::splat(1).cast(), i)
42}
43
44/// Long floor function matching Java behavior.
45///
46/// Java reference: `Mth.lfloor(double)`
47#[inline]
48#[must_use]
49pub fn fast_lfloor(v: f64) -> i64 {
50    let i = v as i64;
51    if v < i as f64 { i - 1 } else { i }
52}
53
54#[cfg(test)]
55mod fast_floor_tests {
56    use super::*;
57    #[test]
58    fn test_floor() {
59        assert_eq!(fast_floor(1.5), 1);
60        assert_eq!(fast_floor(1.0), 1);
61        assert_eq!(fast_floor(0.5), 0);
62        assert_eq!(fast_floor(0.0), 0);
63        assert_eq!(fast_floor(-0.5), -1);
64        assert_eq!(fast_floor(-1.0), -1);
65        assert_eq!(fast_floor(-1.5), -2);
66    }
67}
68/// Round-off constant for coordinate wrapping to prevent precision loss.
69/// This is 2^25 = 33554432.
70const ROUND_OFF: f64 = 33_554_432.0;
71
72const HALF_ROUND_OFF: f64 = (ROUND_OFF / 2.0).next_down();
73
74/// Wrap N coordinates to prevent precision loss (N-lane SIMD version of [`wrap`]).
75///
76/// Fast path: at normal game coordinates all lanes are within `[-HALF_ROUND_OFF, HALF_ROUND_OFF)`,
77/// so the expensive `div + floor + mul` is skipped almost always.
78#[inline]
79#[must_use]
80pub fn wrap_simd<F, const N: usize>(x: Simd<F, N>) -> Simd<F, N>
81where
82    F: SimdElement + SimdCast,
83    Simd<F, N>: ops::Div<Output = Simd<F, N>>
84        + ops::Add<Output = Simd<F, N>>
85        + ops::Mul<Output = Simd<F, N>>
86        + ops::Sub<Output = Simd<F, N>>
87        + SimdPartialOrd<Mask = Mask<<F as SimdElement>::Mask, N>>
88        + StdFloat,
89{
90    let in_fast_range = x.simd_ge(Simd::splat(-HALF_ROUND_OFF).cast())
91        & x.simd_lt(Simd::splat(HALF_ROUND_OFF).cast());
92    if in_fast_range.all() {
93        return x;
94    }
95
96    let round_off = Simd::splat(ROUND_OFF).cast();
97    x - (x / round_off + Simd::splat(0.5).cast()).floor() * round_off
98}
99
100/// Wrap a coordinate to prevent precision loss at large values.
101///
102/// This wraps the coordinate to the range `[-ROUND_OFF/2, ROUND_OFF/2]` to
103/// maintain numerical precision for coordinates far from the origin.
104///
105/// Public because `BlendedNoise` calls this directly on per-octave coordinates.
106#[inline]
107#[must_use]
108pub fn wrap(x: f64) -> f64 {
109    if (-HALF_ROUND_OFF..HALF_ROUND_OFF).contains(&x) {
110        return x;
111    }
112
113    x - (x / ROUND_OFF + 0.5).floor() * ROUND_OFF
114}
115
116#[cfg(test)]
117mod wrap_tests {
118    use super::*;
119    use std::simd::f64x4;
120
121    const CASES: &[[f64; 4]] = &[
122        [0.0, -0.0, 100.0, -100.0],
123        [
124            HALF_ROUND_OFF.next_down(),
125            HALF_ROUND_OFF,
126            HALF_ROUND_OFF.next_up(),
127            HALF_ROUND_OFF.next_up().next_up(),
128        ],
129        [
130            (-HALF_ROUND_OFF).next_down(),
131            -HALF_ROUND_OFF,
132            (-HALF_ROUND_OFF).next_up(),
133            (-HALF_ROUND_OFF).next_up().next_up(),
134        ],
135        [ROUND_OFF, -ROUND_OFF, 100_000_000.0, -100_000_000.0],
136        [1.25, HALF_ROUND_OFF, -20.5, -HALF_ROUND_OFF],
137        [0.0, -0.0, HALF_ROUND_OFF, f64::NAN],
138        [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, -f64::NAN],
139        [f64::MAX, -f64::MAX, f64::MIN_POSITIVE, -f64::MIN_POSITIVE],
140    ];
141
142    fn assert_matches_vanilla(input: f64, actual: f64) {
143        // GradientNoise.wrap uses nextDown(16777216), including its fast path.
144        let bound = 16_777_216.0_f64.next_down();
145        let expected = if (-bound..bound).contains(&input) {
146            input
147        } else {
148            input - (input / 33_554_432.0 + 0.5).floor() * 33_554_432.0
149        };
150        if expected.is_nan() {
151            assert!(actual.is_nan(), "input {input:?}: {actual:?}");
152        } else {
153            assert_eq!(actual.to_bits(), expected.to_bits(), "input {input:?}");
154        }
155    }
156
157    #[test]
158    fn test_wrap() {
159        assert_eq!(
160            wrap(16_777_216.0_f64.next_down()).to_bits(),
161            (-16_777_216.0_f64).to_bits()
162        );
163        for &input in CASES.iter().flatten() {
164            assert_matches_vanilla(input, wrap(input));
165        }
166    }
167
168    #[test]
169    fn test_wrap_4x_matches_vanilla() {
170        for &case in CASES {
171            let wrapped = wrap_simd(f64x4::from_array(case)).to_array();
172            for (input, actual) in case.into_iter().zip(wrapped) {
173                assert_matches_vanilla(input, actual);
174            }
175        }
176    }
177}