diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index f1146aa21..2007adf3f 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -67,6 +67,7 @@ add_library(common
timer.cpp
timer.h
types.h
+ xorshift_prng.h
)
target_include_directories(common PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj
index c4c36981c..e302aab2b 100644
--- a/src/common/common.vcxproj
+++ b/src/common/common.vcxproj
@@ -50,6 +50,7 @@
+
diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters
index 09070d4e1..c67d63fad 100644
--- a/src/common/common.vcxproj.filters
+++ b/src/common/common.vcxproj.filters
@@ -53,6 +53,7 @@
+
diff --git a/src/common/xorshift_prng.h b/src/common/xorshift_prng.h
new file mode 100644
index 000000000..246687e2c
--- /dev/null
+++ b/src/common/xorshift_prng.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2025 Connor McLaughlin
+// SPDX-License-Identifier: CC0-1.0
+
+#pragma once
+
+#include "types.h"
+
+class XorShift128PlusPlus
+{
+public:
+ struct State
+ {
+ u64 s0;
+ u64 s1;
+ };
+
+ static constexpr State GetInitialState(u64 seed)
+ {
+ u64 x = seed;
+ return State{SplitMix64(x), SplitMix64(x)};
+ }
+
+ constexpr XorShift128PlusPlus() : m_state(GetInitialState(0)) {}
+ constexpr XorShift128PlusPlus(u64 seed) : m_state(GetInitialState(seed)) {}
+
+ ALWAYS_INLINE const State& GetState() const { return m_state; }
+ State* GetMutableStatePtr() { return &m_state; }
+ void SetState(State& state) { m_state = state; }
+
+ void Reset(u64 seed) { m_state = GetInitialState(seed); }
+
+ u64 Next()
+ {
+ // https://xoroshiro.di.unimi.it/xoroshiro128plusplus.c
+ u64 s0 = m_state.s0;
+ u64 s1 = m_state.s1;
+ u64 result = RotateLeft64(s0 + s1, 17) + s0;
+
+ s1 ^= s0;
+ m_state.s0 = RotateLeft64(s0, 49) ^ s1 ^ (s1 << 21); // a, b
+ m_state.s1 = RotateLeft64(s1, 28); // c
+
+ return result;
+ }
+
+ ALWAYS_INLINE u64 NextRange(u64 n)
+ {
+ // This constant should be folded.
+ const u64 max_allowed_value = (UINT64_C(0xFFFFFFFFFFFFFFFF) / n) * n;
+ for (;;)
+ {
+ const u64 x = Next();
+ if (x > max_allowed_value)
+ continue;
+
+ return x % n;
+ }
+ }
+
+ template
+ ALWAYS_INLINE T NextRange(T low, T high)
+ {
+ return low + static_cast(NextRange(static_cast(high - low)));
+ }
+
+private:
+ static constexpr u64 SplitMix64(u64& x)
+ {
+ // https://xoroshiro.di.unimi.it/splitmix64.c
+ u64 z = (x += 0x9e3779b97f4a7c15);
+ z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
+ z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
+ return z ^ (z >> 31);
+ }
+
+ static ALWAYS_INLINE u64 RotateLeft64(const u64 x, int k) { return (x << k) | (x >> (64 - k)); }
+
+ State m_state;
+};