psx_fx/rng.rs
1//! Deterministic linear-congruential PRNG.
2//!
3//! Not cryptographically interesting -- perfect for sprinkling
4//! variability across particle velocity / enemy-shot cadence /
5//! any "looks random but must replay identically" effect. The
6//! constants match the venerable `glibc` LCG, which has
7//! good-enough statistical properties for game use and produces
8//! the same output on every PS1 / host / emulator.
9
10/// 32-bit integer LCG. Step once per `next()` / `signed()` call.
11#[derive(Copy, Clone, Debug)]
12#[repr(transparent)]
13pub struct LcgRng(u32);
14
15impl LcgRng {
16 /// Build with an explicit seed. Same seed → same sequence.
17 pub const fn new(seed: u32) -> Self {
18 Self(seed)
19 }
20
21 /// One LCG step. Multiplier + increment are `glibc`'s constants.
22 /// Returns the fresh internal state.
23 ///
24 /// # Take the high bits, not the low ones
25 ///
26 /// The low bits of any power-of-two LCG are very weak, and bit 0 of this
27 /// one is not random at all. The multiplier and the increment are both
28 /// odd, so `x' = x*m + c` gives `x'0 = x0 ^ 1`: bit 0 strictly alternates,
29 /// period two. Bit `k` has period at most `2^(k+1)`.
30 ///
31 /// So `next() & 1` ping-pongs, `next() & 127` cycles inside 128 draws, and
32 /// `next() % 25` leans on the same weak bits. Use [`LcgRng::next_mixed`]
33 /// for anything that masks or takes a remainder, or shift the high half
34 /// down yourself as [`LcgRng::signed`] does.
35 #[inline]
36 #[allow(clippy::should_implement_trait)]
37 pub fn next(&mut self) -> u32 {
38 self.0 = self.0.wrapping_mul(1_103_515_245).wrapping_add(12345);
39 self.0
40 }
41
42 /// One step with the strong high half folded down over the weak low half,
43 /// for callers that mask or take a remainder.
44 ///
45 /// VoXide worked this out and carried it as a private wrapper: its callers
46 /// lean on `& 1`, `% 4` and `% 120`, and the raw low bits cycle with tiny
47 /// periods. It belongs on the generator rather than in one game.
48 #[inline]
49 pub fn next_mixed(&mut self) -> u32 {
50 let x = self.next();
51 x ^ (x >> 16)
52 }
53
54 /// Uniform-ish value in `[0, max)`, or 0 when `max` is 0.
55 ///
56 /// Sourced from [`LcgRng::next_mixed`], so it is safe against the low-bit
57 /// weakness a bare `next() % max` walks into.
58 #[inline]
59 pub fn below(&mut self, max: u32) -> u32 {
60 if max == 0 {
61 return 0;
62 }
63 self.next_mixed() % max
64 }
65
66 /// Signed integer in `[-range, +range]`, symmetric about zero, sourced from
67 /// five bits of the LCG: thirty-two evenly spaced values from `-range` to
68 /// `+range`, rounded toward zero. Any `range` is fine, negative included
69 /// (the result is then mirrored); the arithmetic is 32-bit, so it does not
70 /// overflow for the whole `i16` range.
71 #[inline]
72 pub fn signed(&mut self, range: i16) -> i16 {
73 let r = self.next();
74 spread(((r >> 16) & 0x1F) as u8, range)
75 }
76
77 /// Current internal state -- useful if a caller wants to save /
78 /// restore the RNG across reset boundaries.
79 pub const fn state(self) -> u32 {
80 self.0
81 }
82}
83
84/// Map five random bits (`0..=31`) onto `[-range, +range]`: odd multiples of
85/// `range / 31` from `-31` to `+31`, so the two ends are exactly `-range` and
86/// `+range` and the mapping is odd (`spread(31 - raw) == -spread(raw)`).
87const fn spread(raw: u8, range: i16) -> i16 {
88 let steps = 2 * (raw as i32) - 31; // -31, -29, ..., 31
89 (steps * range as i32 / 31) as i16
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn same_seed_same_sequence() {
98 let mut a = LcgRng::new(0xC0DE_F00D);
99 let mut b = LcgRng::new(0xC0DE_F00D);
100 for _ in 0..100 {
101 assert_eq!(a.next(), b.next());
102 }
103 }
104
105 #[test]
106 fn signed_stays_in_range() {
107 let mut rng = LcgRng::new(0xBEEF_0042);
108 for _ in 0..10_000 {
109 let v = rng.signed(40);
110 assert!((-40..=40).contains(&v), "out of range: {v}");
111 }
112 }
113
114 #[test]
115 fn signed_spans_the_whole_range_and_is_symmetric() {
116 // The old mapping gave [-range, 15 * range / 16]: at range = 40 the
117 // maximum was 37, and every burst drifted toward -x / -y.
118 for range in [1i16, 5, 40, 100, 1000, 2047, 2048, 20_000, i16::MAX] {
119 let mut min = i16::MAX;
120 let mut max = i16::MIN;
121 let mut sum = 0i64;
122 for raw in 0..32u8 {
123 let v = spread(raw, range);
124 assert_eq!(v, -spread(31 - raw, range), "range {range} raw {raw}");
125 min = min.min(v);
126 max = max.max(v);
127 sum += v as i64;
128 }
129 assert_eq!((min, max), (-range, range), "range {range}");
130 assert_eq!(sum, 0, "no net drift at range {range}");
131 }
132 }
133
134 #[test]
135 fn signed_does_not_overflow_on_large_ranges() {
136 // (raw - 16) * range overflowed i16 above 2047: a panic in a host
137 // debug build, a flipped sign on the console.
138 let mut rng = LcgRng::new(9);
139 for _ in 0..1000 {
140 let v = rng.signed(i16::MAX);
141 assert!(i32::from(v).abs() <= i32::from(i16::MAX));
142 let w = rng.signed(i16::MIN + 1);
143 assert!(i32::from(w).abs() <= i32::from(i16::MAX));
144 }
145 }
146
147 #[test]
148 fn signed_zero_range_is_zero() {
149 let mut rng = LcgRng::new(1);
150 for _ in 0..100 {
151 assert_eq!(rng.signed(0), 0);
152 }
153 }
154}