Skip to content

Commit b3971a3

Browse files
authored
update: refactor deterministic number generation (#40)
- added new DeterministicRandom - marked ThreadLocalRandom obsolete
1 parent 605bc87 commit b3971a3

File tree

4 files changed

+578
-86
lines changed

4 files changed

+578
-86
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,34 @@ Fixed64 sinValue = FixedTrigonometry.Sin(angle);
106106
Console.WriteLine(sinValue); // Output: ~0.707
107107
```
108108

109+
### Deterministic Random Generation
110+
111+
Use `DeterministicRandom` when you need reproducible random values across runs, worlds, or features.
112+
Streams are derived from a seed and remain deterministic regardless of threading or platform.
113+
114+
```csharp
115+
// Simple constructor-based stream:
116+
var rng = new DeterministicRandom(42UL);
117+
118+
// Deterministic integer:
119+
int value = rng.Next(1, 10); // [1,10)
120+
121+
// Deterministic Fixed64 in [0,1):
122+
Fixed64 ratio = rng.NextFixed6401();
123+
124+
// One stream per “feature” that’s stable for the same worldSeed + key:
125+
var rngOre = DeterministicRandom.FromWorldFeature(worldSeed: 123456789UL, featureKey: 0xORE);
126+
var rngRivers = DeterministicRandom.FromWorldFeature(123456789UL, 0xRIV, index: 0);
127+
128+
// Deterministic Fixed64 draws:
129+
Fixed64 h = rngOre.NextFixed64(Fixed64.One); // [0, 1)
130+
Fixed64 size = rngOre.NextFixed64(Fixed64.Zero, 5 * Fixed64.One); // [0, 5)
131+
Fixed64 posX = rngRivers.NextFixed64(-Fixed64.One, Fixed64.One); // [-1, 1)
132+
133+
// Deterministic integers:
134+
int loot = rngOre.Next(1, 5); // [1,5)
135+
```
136+
109137
---
110138
111139
## 📦 Library Structure
@@ -116,6 +144,7 @@ Console.WriteLine(sinValue); // Output: ~0.707
116144
- **`IBound` Interface:** Standard interface for bounding shapes `BoundingBox`, `BoundingArea`, and `BoundingSphere`, each offering intersection, containment, and projection logic.
117145
- **`FixedMath` Static Class:** Provides common math and trigonometric functions using fixed-point math.
118146
- **`Fixed4x4` and `Fixed3x3`:** Support matrix operations for transformations.
147+
- **`DeterministicRandom` Struct:** Seedable, allocation-free RNG for repeatable procedural generation.
119148
120149
### Fixed64 Struct
121150
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace FixedMathSharp.Utility
5+
{
6+
/// <summary>
7+
/// Fast, seedable, deterministic RNG suitable for lockstep sims and map gen.
8+
/// Uses xoroshiro128++ with splitmix64 seeding. No allocations, no time/GUID.
9+
/// </summary>
10+
public struct DeterministicRandom
11+
{
12+
// xoroshiro128++ state
13+
private ulong _s0;
14+
private ulong _s1;
15+
16+
#region Construction / Seeding
17+
18+
public DeterministicRandom(ulong seed)
19+
{
20+
// Expand a single seed into two 64-bit state words via splitmix64.
21+
_s0 = SplitMix64(ref seed);
22+
_s1 = SplitMix64(ref seed);
23+
24+
// xoroshiro requires non-zero state; repair pathological seed.
25+
if (_s0 == 0UL && _s1 == 0UL)
26+
_s1 = 0x9E3779B97F4A7C15UL;
27+
}
28+
29+
/// <summary>
30+
/// Create a stream deterministically
31+
/// Derived from (worldSeed, featureKey[,index]).
32+
/// </summary>
33+
public static DeterministicRandom FromWorldFeature(ulong worldSeed, ulong featureKey, ulong index = 0)
34+
{
35+
// Simple reversible mix (swap for a stronger mix if required).
36+
ulong seed = Mix64(worldSeed, featureKey);
37+
seed = Mix64(seed, index);
38+
return new DeterministicRandom(seed);
39+
}
40+
41+
#endregion
42+
43+
#region Core PRNG
44+
45+
/// <summary>
46+
/// xoroshiro128++ next 64 bits.
47+
/// </summary>
48+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
49+
public ulong NextU64()
50+
{
51+
ulong s0 = _s0, s1 = _s1;
52+
ulong result = RotL(s0 + s1, 17) + s0;
53+
54+
s1 ^= s0;
55+
_s0 = RotL(s0, 49) ^ s1 ^ (s1 << 21); // a,b
56+
_s1 = RotL(s1, 28); // c
57+
58+
return result;
59+
}
60+
61+
/// <summary>
62+
/// Next non-negative Int32 in [0, int.MaxValue].
63+
/// </summary>
64+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
65+
public int Next()
66+
{
67+
// Take high bits for better quality; mask to 31 bits non-negative.
68+
return (int)(NextU64() >> 33);
69+
}
70+
71+
/// <summary>
72+
/// Unbiased int in [0, maxExclusive).
73+
/// </summary>
74+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
75+
public int Next(int maxExclusive)
76+
{
77+
return maxExclusive <= 0
78+
? throw new ArgumentOutOfRangeException(nameof(maxExclusive))
79+
: (int)NextBounded((uint)maxExclusive);
80+
}
81+
82+
/// <summary>
83+
/// Unbiased int in [min, maxExclusive).
84+
/// </summary>
85+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
86+
public int Next(int minInclusive, int maxExclusive)
87+
{
88+
if (minInclusive >= maxExclusive)
89+
throw new ArgumentException("min >= max");
90+
uint range = (uint)(maxExclusive - minInclusive);
91+
return minInclusive + (int)NextBounded(range);
92+
}
93+
94+
/// <summary>
95+
/// Double in [0,1).
96+
/// </summary>
97+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
98+
public double NextDouble()
99+
{
100+
// 53 random bits -> [0,1)
101+
return (NextU64() >> 11) * (1.0 / (1UL << 53));
102+
}
103+
104+
/// <summary>
105+
/// Fill span with random bytes.
106+
/// </summary>
107+
public void NextBytes(Span<byte> buffer)
108+
{
109+
int i = 0;
110+
while (i + 8 <= buffer.Length)
111+
{
112+
ulong v = NextU64();
113+
Unsafe.WriteUnaligned(ref buffer[i], v);
114+
i += 8;
115+
}
116+
if (i < buffer.Length)
117+
{
118+
ulong v = NextU64();
119+
while (i < buffer.Length)
120+
{
121+
buffer[i++] = (byte)v;
122+
v >>= 8;
123+
}
124+
}
125+
}
126+
127+
#endregion
128+
129+
#region Fixed64 helpers
130+
131+
/// <summary>
132+
/// Random Fixed64 in [0,1).
133+
/// </summary>
134+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
135+
public Fixed64 NextFixed6401()
136+
{
137+
// Produce a raw value in [0, One.m_rawValue)
138+
ulong rawOne = (ulong)Fixed64.One.m_rawValue;
139+
ulong r = NextBounded(rawOne);
140+
return Fixed64.FromRaw((long)r);
141+
}
142+
143+
/// <summary>
144+
/// Random Fixed64 in [0, maxExclusive).
145+
/// </summary>
146+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
147+
public Fixed64 NextFixed64(Fixed64 maxExclusive)
148+
{
149+
if (maxExclusive <= Fixed64.Zero)
150+
throw new ArgumentOutOfRangeException(nameof(maxExclusive), "max must be > 0");
151+
ulong rawMax = (ulong)maxExclusive.m_rawValue;
152+
ulong r = NextBounded(rawMax);
153+
return Fixed64.FromRaw((long)r);
154+
}
155+
156+
/// <summary>
157+
/// Random Fixed64 in [minInclusive, maxExclusive).
158+
/// </summary>
159+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
160+
public Fixed64 NextFixed64(Fixed64 minInclusive, Fixed64 maxExclusive)
161+
{
162+
if (minInclusive >= maxExclusive)
163+
throw new ArgumentException("min >= max");
164+
ulong span = (ulong)(maxExclusive.m_rawValue - minInclusive.m_rawValue);
165+
ulong r = NextBounded(span);
166+
return Fixed64.FromRaw((long)r + minInclusive.m_rawValue);
167+
}
168+
169+
#endregion
170+
171+
#region Internals: unbiased range, splitmix64, mixing, rotations
172+
173+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
174+
private ulong NextBounded(ulong bound)
175+
{
176+
// Rejection to avoid modulo bias.
177+
// threshold = 2^64 % bound, but expressed as (-bound) % bound
178+
ulong threshold = unchecked((ulong)-(long)bound) % bound;
179+
while (true)
180+
{
181+
ulong r = NextU64();
182+
if (r >= threshold)
183+
return r % bound;
184+
}
185+
}
186+
187+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
188+
private static ulong RotL(ulong x, int k) => (x << k) | (x >> (64 - k));
189+
190+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
191+
private static ulong SplitMix64(ref ulong state)
192+
{
193+
ulong z = (state += 0x9E3779B97F4A7C15UL);
194+
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL;
195+
z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL;
196+
return z ^ (z >> 31);
197+
}
198+
199+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
200+
private static ulong Mix64(ulong a, ulong b)
201+
{
202+
// Simple reversible mix (variant of splitmix finalizer).
203+
ulong x = a ^ (b + 0x9E3779B97F4A7C15UL);
204+
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL;
205+
x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL;
206+
return x ^ (x >> 31);
207+
}
208+
209+
#endregion
210+
}
211+
}

0 commit comments

Comments
 (0)