Skip to main content

steel_utils/random/
xoroshiro.rs

1use crate::random::{
2    PositionalRandom, Random, RandomSource, RandomSplitter, gaussian::MarsagliaPolarGaussian,
3    get_seed, name_hash::NameHash,
4};
5
6// Ratios used in the mix functions
7const GOLDEN_RATIO_64: u64 = 0x9E37_79B9_7F4A_7C15;
8const SILVER_RATIO_64: u64 = 0x6A09_E667_F3BC_C909;
9
10/// A Xoroshiro128++ random number generator.
11pub struct Xoroshiro {
12    seed_lo: u64,
13    seed_hi: u64,
14    next_gaussian: f64,
15}
16
17/// A splitter for the Xoroshiro128++ random number generator.
18#[derive(Clone)]
19pub struct XoroshiroSplitter {
20    seed_lo: u64,
21    seed_hi: u64,
22}
23
24impl Xoroshiro {
25    /// Creates a new `Xoroshiro` from a seed.
26    #[must_use]
27    pub const fn from_seed(seed: u64) -> Self {
28        // From RandomSupport
29        let (lo, hi) = Self::upgrade_seed_to_128_bit(seed);
30        let lo = mix_stafford_13(lo);
31        let hi = mix_stafford_13(hi);
32        Self::new(lo, hi)
33    }
34
35    /// Creates a new `Xoroshiro` from a seed without mixing.
36    #[must_use]
37    pub const fn from_seed_unmixed(seed: u64) -> Self {
38        // From RandomSupport and
39        let (lo, hi) = Self::upgrade_seed_to_128_bit(seed);
40        Self::new(lo, hi)
41    }
42
43    const fn new(lo: u64, hi: u64) -> Self {
44        let (lo, hi) = if (lo | hi) == 0 {
45            (GOLDEN_RATIO_64, SILVER_RATIO_64)
46        } else {
47            (lo, hi)
48        };
49        Self {
50            seed_lo: lo,
51            seed_hi: hi,
52            next_gaussian: f64::NAN,
53        }
54    }
55
56    const fn upgrade_seed_to_128_bit(seed: u64) -> (u64, u64) {
57        let lo = seed ^ SILVER_RATIO_64;
58        let hi = lo.wrapping_add(GOLDEN_RATIO_64);
59        (lo, hi)
60    }
61
62    const fn next(&mut self, bits: u64) -> u64 {
63        self.next_random() >> (64 - bits)
64    }
65
66    const fn next_random(&mut self) -> u64 {
67        let l = self.seed_lo;
68        let m = self.seed_hi;
69        let n = l.wrapping_add(m).rotate_left(17).wrapping_add(l);
70        let m = m ^ l;
71        self.seed_lo = l.rotate_left(49) ^ m ^ (m << 21);
72        self.seed_hi = m.rotate_left(28);
73        n
74    }
75
76    /// Resets this random source to vanilla's `XoroshiroRandomSource.setSeed(long)` state.
77    pub const fn set_seed(&mut self, seed: i64) {
78        *self = Self::from_seed(seed as u64);
79    }
80}
81
82impl MarsagliaPolarGaussian for Xoroshiro {
83    fn stored_next_gaussian(&self) -> Option<f64> {
84        if self.next_gaussian.is_nan() {
85            None
86        } else {
87            Some(self.next_gaussian)
88        }
89    }
90
91    fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
92        self.next_gaussian = value.unwrap_or(f64::NAN);
93    }
94}
95
96const fn mix_stafford_13(z: u64) -> u64 {
97    let z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
98    let z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
99    z ^ (z >> 31)
100}
101
102impl Random for Xoroshiro {
103    fn fork(&mut self) -> Self {
104        Self::new(self.next_random(), self.next_random())
105    }
106
107    fn next_i32(&mut self) -> i32 {
108        self.next_random() as i32
109    }
110
111    fn next_i32_bounded(&mut self, bound: i32) -> i32 {
112        let mut l = (self.next_i32() as u64) & 0xFFFF_FFFF;
113        let mut m = l.wrapping_mul(bound as u64);
114        let mut n = m & 0xFFFF_FFFF;
115        if n < bound as u64 {
116            let i = u64::from(((!bound as u32).wrapping_add(1)) % bound as u32);
117            while n < i {
118                l = (self.next_i32() as u64) & 0xFFFF_FFFF;
119                m = l.wrapping_mul(bound as u64);
120                n = m & 0xFFFF_FFFF;
121            }
122        }
123        let o = m >> 32;
124        o as i32
125    }
126
127    fn next_i64(&mut self) -> i64 {
128        self.next_random() as i64
129    }
130
131    fn next_f32(&mut self) -> f32 {
132        self.next(24) as f32 * 5.960_464_5e-8
133    }
134
135    fn next_f64(&mut self) -> f64 {
136        self.next(53) as f64 * f64::from(1.110_223e-16_f32)
137    }
138
139    fn next_bool(&mut self) -> bool {
140        (self.next_random() & 1) != 0
141    }
142
143    fn next_gaussian(&mut self) -> f64 {
144        self.calculate_gaussian()
145    }
146
147    fn next_positional(&mut self) -> RandomSplitter {
148        RandomSplitter::Xoroshiro(XoroshiroSplitter {
149            seed_lo: self.next_random(),
150            seed_hi: self.next_random(),
151        })
152    }
153}
154
155impl PositionalRandom for XoroshiroSplitter {
156    #[expect(
157        clippy::many_single_char_names,
158        reason = "matches vanilla's positional seeding math notation"
159    )]
160    fn at(&self, x: i32, y: i32, z: i32) -> RandomSource {
161        let l = get_seed(x, y, z) as u64;
162        let m = l ^ self.seed_lo;
163
164        RandomSource::Xoroshiro(Xoroshiro::new(m, self.seed_hi))
165    }
166
167    fn with_hash_of(&self, hash: &NameHash) -> RandomSource {
168        let [l, m] = hash.md5;
169        RandomSource::Xoroshiro(Xoroshiro::new(l ^ self.seed_lo, m ^ self.seed_hi))
170    }
171
172    fn with_seed(&self, seed: u64) -> RandomSource {
173        RandomSource::Xoroshiro(Xoroshiro::new(seed ^ self.seed_lo, seed ^ self.seed_hi))
174    }
175}
176
177#[cfg(test)]
178#[expect(
179    clippy::unreadable_literal,
180    clippy::cast_sign_loss,
181    clippy::float_cmp,
182    reason = "test vectors from vanilla Java; raw literals and casts are intentional"
183)]
184mod tests {
185    use super::*;
186    use crate::random::{PositionalRandom, Random};
187
188    // Values checked against results from the equivalent Java source
189
190    const MIX_STAFFORD_13_TEST_CASES: &[(u64, i64)] = &[
191        (0, 0),
192        (1, 6238072747940578789),
193        (64, -8456553050427055661),
194        (4096, -1125827887270283392),
195        (262144, -120227641678947436),
196        (16777216, 6406066033425044679),
197        (1073741824, 3143522559155490559),
198        (16, -2773008118984693571),
199        (1024, 8101005175654470197),
200        (65536, -3551754741763842827),
201        (4194304, -2737109459693184599),
202        (2, -2606959012126976886),
203        (128, -5825874238589581082),
204        (8192, 1111983794319025228),
205        (524288, -7964047577924347155),
206        (33554432, -5634612006859462257),
207        (2147483648, -1436547171018572641),
208        (137438953472, -4514638798598940860),
209        (8796093022208, -610572083552328405),
210        (562949953421312, -263574021372026223),
211        (36028797018963968, 7868130499179604987),
212        (253, -4045451768301188906),
213        (127, -6873224393826578139),
214        (8447, 6670985465942597767),
215        (524543, -6228499289678716485),
216        (33554687, 2630391896919662492),
217        (2147483903, -6879633228472053040),
218        (137438953727, -5817997684975131823),
219        (8796093022463, 2384436581894988729),
220        (562949953421567, -5076179956679497213),
221        (36028797018964223, -5993365784811617721),
222    ];
223
224    #[test]
225    fn test_mix_stafford_13() {
226        for &(input, expected) in MIX_STAFFORD_13_TEST_CASES {
227            assert_eq!(
228                mix_stafford_13(input),
229                expected as u64,
230                "mix_stafford_13({input}) failed"
231            );
232        }
233    }
234
235    #[test]
236    fn next_i32_matches_java() {
237        const EXPECTED: [i32; 10] = [
238            -160476802,
239            781697906,
240            653572596,
241            1337520923,
242            -505875771,
243            -47281585,
244            342195906,
245            1417498593,
246            -1478887443,
247            1560080270,
248        ];
249
250        let mut rng = Xoroshiro::from_seed(0);
251        for &expected in &EXPECTED {
252            assert_eq!(rng.next_i32(), expected);
253        }
254    }
255
256    #[test]
257    fn next_i32_bounded() {
258        const SMALL_EXPECTED: [i32; 10] = [9, 1, 1, 3, 8, 9, 0, 3, 6, 3];
259        const LARGE_EXPECTED: [i32; 10] = [
260            9784805, 470346, 13560642, 7320226, 14949645, 13460529, 2824352, 10938308, 14146127,
261            4549185,
262        ];
263        let mut rng = Xoroshiro::from_seed(0);
264
265        for &expected in &SMALL_EXPECTED {
266            assert_eq!(rng.next_i32_bounded(10), expected);
267        }
268
269        for &expected in &LARGE_EXPECTED {
270            assert_eq!(rng.next_i32_bounded(0xFF_FFFF), expected);
271        }
272    }
273
274    #[test]
275    fn next_i32_between_inclusive() {
276        const EXPECTED: [i32; 10] = [99, 59, 57, 65, 94, 100, 54, 66, 83, 68];
277
278        let mut rng = Xoroshiro::from_seed(0);
279        for &expected in &EXPECTED {
280            assert_eq!(rng.next_i32_between(50, 100), expected);
281        }
282    }
283
284    #[test]
285    fn next_i32_between_exclusive() {
286        const EXPECTED: [i32; 10] = [98, 59, 57, 65, 94, 99, 53, 66, 82, 68];
287
288        let mut rng = Xoroshiro::from_seed(0);
289        for &expected in &EXPECTED {
290            assert_eq!(rng.next_i32_between_exclusive(50, 100), expected);
291        }
292    }
293
294    #[test]
295    fn next_f64_matches_java() {
296        const EXPECTED: [f64; 10] = [
297            0.16474369376959186,
298            0.7997457290026366,
299            0.2511961888876212,
300            0.11712489470639631,
301            0.0997124786680137,
302            0.7566797430601416,
303            0.7723285712021574,
304            0.9420469457586381,
305            0.48056202536813664,
306            0.6099690583914598,
307        ];
308
309        let mut rng = Xoroshiro::from_seed(0);
310        for &expected in &EXPECTED {
311            assert_eq!(rng.next_f64(), expected);
312        }
313    }
314
315    #[test]
316    fn next_f32_matches_java() {
317        const EXPECTED: [f32; 10] = [
318            0.16474366,
319            0.7997457,
320            0.25119615,
321            0.117124856,
322            0.09971243,
323            0.7566797,
324            0.77232856,
325            0.94204694,
326            0.48056197,
327            0.609969,
328        ];
329
330        let mut rng = Xoroshiro::from_seed(0);
331        for &expected in &EXPECTED {
332            assert_eq!(rng.next_f32(), expected);
333        }
334    }
335
336    #[test]
337    fn next_i64_matches_java() {
338        const EXPECTED: [i64; 10] = [
339            3038984756725240190,
340            -3694039286755638414,
341            4633751808701151732,
342            2160572957309072155,
343            1839370574944072389,
344            -4488466507718817201,
345            -4199796579929588030,
346            -1069045159880208415,
347            8864804693509535725,
348            -7194800960680693874,
349        ];
350
351        let mut rng = Xoroshiro::from_seed(0);
352        for &expected in &EXPECTED {
353            assert_eq!(rng.next_i64(), expected);
354        }
355    }
356
357    #[test]
358    fn next_bool_matches_java() {
359        const EXPECTED: [bool; 10] = [
360            false, false, false, true, true, true, false, true, true, false,
361        ];
362
363        let mut rng = Xoroshiro::from_seed(0);
364        for &expected in &EXPECTED {
365            assert_eq!(rng.next_bool(), expected);
366        }
367    }
368
369    #[test]
370    fn next_gaussian_matches_java() {
371        const EXPECTED: [f64; 10] = [
372            -0.48540690699780015,
373            0.43399227545320296,
374            -0.3283265251019599,
375            -0.5052497078202575,
376            -0.3772512828630807,
377            0.2419080215945433,
378            -0.42622066207565135,
379            2.411315261138953,
380            -1.1419147030553274,
381            -0.05849758093810378,
382        ];
383
384        let mut rng = Xoroshiro::from_seed(0);
385        for &expected in &EXPECTED {
386            assert_eq!(rng.next_gaussian(), expected);
387        }
388    }
389
390    #[test]
391    fn triangle_matches_java() {
392        const EXPECTED: [f64; 10] = [
393            6.824989823834776,
394            10.670356470906125,
395            6.71516367803936,
396            9.151408127217596,
397            9.352964834883384,
398            8.291618967842293,
399            8.954549938640508,
400            11.833001837470519,
401            10.65851306020791,
402            11.684676364031647,
403        ];
404
405        let mut rng = Xoroshiro::from_seed(0);
406        for &expected in &EXPECTED {
407            assert_eq!(rng.triangle(10.0, 5.0), expected);
408        }
409    }
410
411    #[test]
412    fn fork_creates_independent_rng() {
413        let mut rng = Xoroshiro::from_seed(0);
414        let mut forked = rng.fork();
415
416        assert_eq!(forked.next_i32(), 542195535);
417        assert_eq!(rng.next_i32(), 653572596);
418    }
419
420    #[test]
421    fn positional_random_splitter() {
422        let mut rng = Xoroshiro::from_seed(0);
423        let mut forked = rng.fork();
424
425        assert_eq!(forked.next_i32(), 542195535);
426
427        let splitter = forked.next_positional();
428
429        let RandomSource::Xoroshiro(mut rand1) =
430            splitter.with_hash_of(&NameHash::new("TEST STRING"))
431        else {
432            panic!("Expected Xoroshiro variant");
433        };
434        assert_eq!(rand1.next_i32(), -641435713);
435
436        let RandomSource::Xoroshiro(mut rand2) = splitter.with_seed(42069) else {
437            panic!("Expected Xoroshiro variant");
438        };
439        assert_eq!(rand2.next_i32(), -340700677);
440
441        let RandomSource::Xoroshiro(mut rand3) = splitter.at(1337, 80085, -69420) else {
442            panic!("Expected Xoroshiro variant");
443        };
444        assert_eq!(rand3.next_i32(), 790449132);
445
446        assert_eq!(rng.next_i32(), 653572596);
447        assert_eq!(forked.next_i32(), 435917842);
448    }
449
450    #[test]
451    fn zero_seed_produces_fallback_values() {
452        let mut rng = Xoroshiro::new(0, 0);
453        assert_eq!(rng.next_i64(), 6807859099481836695);
454    }
455}