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;
71const HALF_ROUND_OFF: f64 = ROUND_OFF / 2.0;
72
73/// Wrap N coordinates to prevent precision loss (N-lane SIMD version of [`wrap`]).
74///
75/// Fast path: at normal game coordinates all lanes are within `[-HALF_ROUND_OFF, HALF_ROUND_OFF)`,
76/// so the expensive `div + floor + mul` is skipped almost always.
77#[inline]
78#[must_use]
79pub fn wrap_simd<F, const N: usize>(x: Simd<F, N>) -> Simd<F, N>
80where
81    F: SimdElement + SimdCast,
82    Simd<F, N>: ops::Div<Output = Simd<F, N>>
83        + ops::Add<Output = Simd<F, N>>
84        + ops::Mul<Output = Simd<F, N>>
85        + ops::Sub<Output = Simd<F, N>>
86        + SimdPartialOrd<Mask = Mask<<F as SimdElement>::Mask, N>>
87        + StdFloat,
88{
89    let in_fast_range = x.simd_ge(Simd::splat(-HALF_ROUND_OFF).cast())
90        & x.simd_lt(Simd::splat(HALF_ROUND_OFF).cast());
91    if in_fast_range.all() {
92        return x;
93    }
94
95    let round_off = Simd::splat(ROUND_OFF).cast();
96    x - (x / round_off + Simd::splat(0.5).cast()).floor() * round_off
97}
98
99/// Wrap a coordinate to prevent precision loss at large values.
100///
101/// This wraps the coordinate to the range `[-ROUND_OFF/2, ROUND_OFF/2]` to
102/// maintain numerical precision for coordinates far from the origin.
103///
104/// Public because `BlendedNoise` calls this directly on per-octave coordinates.
105#[inline]
106#[must_use]
107pub fn wrap(x: f64) -> f64 {
108    if (-HALF_ROUND_OFF..HALF_ROUND_OFF).contains(&x) {
109        return x;
110    }
111
112    x - (x / ROUND_OFF + 0.5).floor() * ROUND_OFF
113}
114
115#[cfg(test)]
116mod wrap_tests {
117    use super::*;
118    use std::simd::f64x4;
119    #[test]
120    fn test_wrap() {
121        fn wrap_reference(x: f64) -> f64 {
122            x - (x / ROUND_OFF + 0.5).floor() * ROUND_OFF
123        }
124
125        // Small values should be unchanged
126        assert!((wrap(100.0) - 100.0).abs() < 1e-10);
127        assert!((wrap(-100.0) - (-100.0)).abs() < 1e-10);
128
129        // Very large values should be wrapped
130        let large = 100_000_000.0;
131        let wrapped = wrap(large);
132        assert!(wrapped.abs() < ROUND_OFF);
133
134        for x in [
135            -HALF_ROUND_OFF,
136            -HALF_ROUND_OFF + 1.0,
137            0.0,
138            HALF_ROUND_OFF - 1.0,
139            HALF_ROUND_OFF,
140            ROUND_OFF,
141            -ROUND_OFF,
142            100_000_000.0,
143            -100_000_000.0,
144        ] {
145            assert!((wrap(x) - wrap_reference(x)).abs() < 1e-15);
146        }
147    }
148
149    #[test]
150    fn test_wrap_4x_matches_scalar_wrap() {
151        let cases = [
152            [0.0, 1.0, -1.0, HALF_ROUND_OFF - 1.0],
153            [
154                -HALF_ROUND_OFF,
155                -HALF_ROUND_OFF + 1.0,
156                HALF_ROUND_OFF - 1.0,
157                HALF_ROUND_OFF,
158            ],
159            [ROUND_OFF, -ROUND_OFF, 100_000_000.0, -100_000_000.0],
160            [1.25, HALF_ROUND_OFF, -20.5, -HALF_ROUND_OFF],
161        ];
162
163        for case in cases {
164            let wrapped = wrap_simd(f64x4::from_array(case)).to_array();
165            for (input, actual) in case.into_iter().zip(wrapped) {
166                #[expect(
167                    clippy::float_cmp,
168                    reason = "SIMD wrap must be bit-identical to scalar wrap per lane"
169                )]
170                {
171                    assert_eq!(actual, wrap(input));
172                }
173            }
174        }
175    }
176}