From 94dda020cf95e62ca639a56a15baa3ad4e87b5da Mon Sep 17 00:00:00 2001 From: foobar-qw Date: Tue, 17 Oct 2023 22:03:57 -0400 Subject: [PATCH 1/3] Generate random numbers using modern primitives. Seeding now uses SplitMix64. Generating now uses Xoroshiro128**. This might make a differene with spawn luck. --- CMakeLists.txt | 3 ++ README.md | 1 + include/g_local.h | 1 + include/rng.h | 9 ++++++ include/rng_gen_impl.h | 11 +++++++ include/rng_seed_impl.h | 10 +++++++ src/g_main.c | 2 +- src/g_utils.c | 7 ++++- src/rng.c | 33 +++++++++++++++++++++ src/rng_gen_impl.c | 66 +++++++++++++++++++++++++++++++++++++++++ src/rng_seed_impl.c | 29 ++++++++++++++++++ 11 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 include/rng.h create mode 100644 include/rng_gen_impl.h create mode 100644 include/rng_seed_impl.h create mode 100644 src/rng.c create mode 100644 src/rng_gen_impl.c create mode 100644 src/rng_seed_impl.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7178eeb2f..0811bb7f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,9 @@ set(SRC_COMMON "${DIR_SRC}/player.c" "${DIR_SRC}/q_shared.c" "${DIR_SRC}/race.c" + "${DIR_SRC}/rng.c" + "${DIR_SRC}/rng_gen_impl.c" + "${DIR_SRC}/rng_seed_impl.c" "${DIR_SRC}/route_calc.c" "${DIR_SRC}/route_fields.c" "${DIR_SRC}/route_lookup.c" diff --git a/README.md b/README.md index 7fa6f1443..8b3eee83e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ For the versions available, see the [tags on this repository][ktx-tags]. * **Ivan** *"qqshka"* **Bolsunov** * **Dominic** *"oldman"* **Evans** +* **Foobar** * **Anton** *"tonik"* **Gavrilov** * **Andrew** *"ult"* **Grondalski** * **Paul Klumpp** diff --git a/include/g_local.h b/include/g_local.h index 6b503a65a..3a8852165 100644 --- a/include/g_local.h +++ b/include/g_local.h @@ -238,6 +238,7 @@ typedef enum // K_SPW_0_NONRANDOM changes "Normal QW respawns" to "pre-qtest nonrandom respawns" #define K_SPW_0_NONRANDOM +void g_random_seed(int); float g_random(void); float crandom(void); int i_rnd(int from, int to); diff --git a/include/rng.h b/include/rng.h new file mode 100644 index 000000000..18d7b2254 --- /dev/null +++ b/include/rng.h @@ -0,0 +1,9 @@ +// rng.h -- High-quality random number generation. + +#ifndef __RNG_H__ +#define __RNG_H__ + +void rng_seed(int seed); +uint32_t rng_next(void); + +#endif diff --git a/include/rng_gen_impl.h b/include/rng_gen_impl.h new file mode 100644 index 000000000..cc0c13bef --- /dev/null +++ b/include/rng_gen_impl.h @@ -0,0 +1,11 @@ +// rng_gen_impl.h -- Xoroshiro128** implementation. + +#ifndef __RNG_GEN_IMPL_H__ +#define __RNG_GEN_IMPL_H__ + +#include + +void rng_gen_impl_set_initial_state(uint32_t, uint32_t, uint32_t, uint32_t); +uint32_t rng_gen_impl_next(void); + +#endif diff --git a/include/rng_seed_impl.h b/include/rng_seed_impl.h new file mode 100644 index 000000000..739847688 --- /dev/null +++ b/include/rng_seed_impl.h @@ -0,0 +1,10 @@ +// rng_seed_impl.h -- SplitMix64 implementation. + +#ifndef __RNG_SEED_IMPL_H__ +#define __RNG_SEED_IMPL_H__ + +#include + +uint64_t rng_seed_impl_next(uint64_t*); + +#endif diff --git a/src/g_main.c b/src/g_main.c index 48d733823..c36925a14 100644 --- a/src/g_main.c +++ b/src/g_main.c @@ -464,7 +464,7 @@ void G_InitGame(int levelTime, int randomSeed) { int i; - srand(randomSeed); + g_random_seed(randomSeed); framecount = 0; starttime = levelTime * 0.001; G_Printf("Init Game\n"); diff --git a/src/g_utils.c b/src/g_utils.c index cb2e1caff..ac03f5604 100644 --- a/src/g_utils.c +++ b/src/g_utils.c @@ -24,6 +24,7 @@ */ #include "g_local.h" +#include "rng.h" void Sc_Stats(float on); void race_stoprecord(qbool cancel); @@ -44,9 +45,13 @@ int NUM_FOR_EDICT(gedict_t *e) return b; } +void g_random_seed(int seed) { + rng_seed(seed); +} + float g_random() { - return ((rand() & 0x7fff) / ((float)0x8000)); + return ((rng_next() & 0x7fff) / ((float)0x8000)); } float crandom() diff --git a/src/rng.c b/src/rng.c new file mode 100644 index 000000000..5d56ae7f0 --- /dev/null +++ b/src/rng.c @@ -0,0 +1,33 @@ +#include + +#include "rng.h" + +#include "rng_gen_impl.h" +#include "rng_seed_impl.h" + +/* Seed the PRNG. + + Since the main game loop only provides 4 bytes of entropy, but the + xoroshiro128** PRNG expects 16 bytes of entropy, this function uses the + splitmix64 algorithm to expand the seed. This approach is recommended by + the authors of xoroshiro128**, as described here: https://prng.di.unimi.it . +*/ +void rng_seed(int seed) { + uint64_t x = (uint64_t)(seed); + + uint32_t a = (uint32_t)rng_seed_impl_next(&x); + uint32_t b = (uint32_t)rng_seed_impl_next(&x); + uint32_t c = (uint32_t)rng_seed_impl_next(&x); + uint32_t d = (uint32_t)rng_seed_impl_next(&x); + + rng_gen_impl_set_initial_state(a, b, c, d); +} + +/* Update the internal PRNG state and return the next value. + + Uses the xoroshiro128** algorithm. +*/ +uint32_t rng_next(void) { + return rng_gen_impl_next(); +} + diff --git a/src/rng_gen_impl.c b/src/rng_gen_impl.c new file mode 100644 index 000000000..50f66e709 --- /dev/null +++ b/src/rng_gen_impl.c @@ -0,0 +1,66 @@ +#include "rng_gen_impl.h" + +//// Start imported code. +// Code in this section copied verbatim from +// https://prng.di.unimi.it/xoshiro128starstar.c + +/* Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . */ + +#include + +/* This is xoshiro128** 1.1, one of our 32-bit all-purpose, rock-solid + generators. It has excellent speed, a state size (128 bits) that is + large enough for mild parallelism, and it passes all tests we are aware + of. + + Note that version 1.0 had mistakenly s[0] instead of s[1] as state + word passed to the scrambler. + + For generating just single-precision (i.e., 32-bit) floating-point + numbers, xoshiro128+ is even faster. + + The state must be seeded so that it is not everywhere zero. */ + + +static inline uint32_t rotl(const uint32_t x, int k) { + return (x << k) | (x >> (32 - k)); +} + + +static uint32_t s[4]; + +static inline uint32_t next(void) { + const uint32_t result = rotl(s[1] * 5, 7) * 9; + + const uint32_t t = s[1] << 9; + + s[2] ^= s[0]; + s[3] ^= s[1]; + s[1] ^= s[2]; + s[0] ^= s[3]; + + s[2] ^= t; + + s[3] = rotl(s[3], 11); + + return result; +} +//// End imported code. + +// Code in this section wraps the PRNG implementation. +void rng_gen_impl_set_initial_state(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { + s[0] = a; + s[1] = b; + s[2] = c; + s[3] = d; +} + +uint32_t rng_gen_impl_next(void) { + return next(); +} diff --git a/src/rng_seed_impl.c b/src/rng_seed_impl.c new file mode 100644 index 000000000..b8f9cb422 --- /dev/null +++ b/src/rng_seed_impl.c @@ -0,0 +1,29 @@ +//// Start imported code. +// Code in this section was copied from +// https://prng.di.unimi.it/splitmix64.c +// then lightly edited to remove a global variable and rename a function. + +/* Written in 2015 by Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . */ + +#include + +/* This is a fixed-increment version of Java 8's SplittableRandom generator + See http://dx.doi.org/10.1145/2714064.2660195 and + http://docs.oracle.com/javase/8/docs/api/java/util/SplittableRandom.html + + It is a very fast generator passing BigCrush, and it can be useful if + for some reason you absolutely want 64 bits of state. */ + +uint64_t rng_seed_impl_next(uint64_t* x) { + uint64_t z = (*x += 0x9e3779b97f4a7c15); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; + z = (z ^ (z >> 27)) * 0x94d049bb133111eb; + return z ^ (z >> 31); +} +//// End imported code. From 1ee5b076c9c986036cfc9091bf128286a4bcb0e7 Mon Sep 17 00:00:00 2001 From: foobar-qw Date: Wed, 18 Oct 2023 03:46:23 -0400 Subject: [PATCH 2/3] Add local RNGs, and use one in the VM. --- include/g_local.h | 1 + include/rng.h | 9 +++++++-- include/rng_gen_impl.h | 6 ++++-- include/rng_gen_state.h | 7 +++++++ src/bg_lib.c | 8 +++----- src/g_main.c | 1 + src/g_utils.c | 5 ++--- src/rng.c | 30 +++++++++++++++++++----------- src/rng_gen_impl.c | 31 +++++++++++-------------------- 9 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 include/rng_gen_state.h diff --git a/include/g_local.h b/include/g_local.h index 3a8852165..b173602c5 100644 --- a/include/g_local.h +++ b/include/g_local.h @@ -35,6 +35,7 @@ #include "g_consts.h" #include "g_syscalls.h" #include "player.h" +#include "rng.h" #if defined(_WIN32) #define QW_PLATFORM "Windows" diff --git a/include/rng.h b/include/rng.h index 18d7b2254..c42dd7bf2 100644 --- a/include/rng.h +++ b/include/rng.h @@ -3,7 +3,12 @@ #ifndef __RNG_H__ #define __RNG_H__ -void rng_seed(int seed); -uint32_t rng_next(void); +#include "rng_gen_state.h" + +void rng_seed(RngState*, int seed); +uint32_t rng_next(RngState*); + +void rng_seed_global(int seed); +uint32_t rng_next_global(void); #endif diff --git a/include/rng_gen_impl.h b/include/rng_gen_impl.h index cc0c13bef..1b91816f0 100644 --- a/include/rng_gen_impl.h +++ b/include/rng_gen_impl.h @@ -5,7 +5,9 @@ #include -void rng_gen_impl_set_initial_state(uint32_t, uint32_t, uint32_t, uint32_t); -uint32_t rng_gen_impl_next(void); +#include "rng_gen_state.h" + +RngState rng_gen_impl_initial_state(int seed); +uint32_t rng_gen_impl_next(RngState* state); #endif diff --git a/include/rng_gen_state.h b/include/rng_gen_state.h new file mode 100644 index 000000000..aab5f8bca --- /dev/null +++ b/include/rng_gen_state.h @@ -0,0 +1,7 @@ +#ifndef __RNG_STATE_H__ +#define __RNG_STATE_H__ + +// You must initialize this with rng_seed/rng_seed_global! +typedef struct RngState { uint32_t s[4]; } RngState; + +#endif diff --git a/src/bg_lib.c b/src/bg_lib.c index a7df3efce..6ff0822df 100644 --- a/src/bg_lib.c +++ b/src/bg_lib.c @@ -177,18 +177,16 @@ double fabs(double x) } //===================================== -static int randSeed = 0; +static RngState rng_state = { 0 }; void srand(unsigned seed) { - randSeed = seed; + rng_seed(&rng_state, (int)seed); } int rand(void) { - randSeed = (69069 * randSeed + 1); - - return (randSeed & 0x7fff); + return (int)rng_next(&rng_state); } //===================================== diff --git a/src/g_main.c b/src/g_main.c index c36925a14..c9c07dbc2 100644 --- a/src/g_main.c +++ b/src/g_main.c @@ -24,6 +24,7 @@ */ #include "g_local.h" +#include "rng.h" /* global 4 fix entity self; entity other; diff --git a/src/g_utils.c b/src/g_utils.c index ac03f5604..ebecfe3db 100644 --- a/src/g_utils.c +++ b/src/g_utils.c @@ -24,7 +24,6 @@ */ #include "g_local.h" -#include "rng.h" void Sc_Stats(float on); void race_stoprecord(qbool cancel); @@ -46,12 +45,12 @@ int NUM_FOR_EDICT(gedict_t *e) } void g_random_seed(int seed) { - rng_seed(seed); + rng_seed_global(seed); } float g_random() { - return ((rng_next() & 0x7fff) / ((float)0x8000)); + return ((rng_next_global() & 0x7fff) / ((float)0x8000)); } float crandom() diff --git a/src/rng.c b/src/rng.c index 5d56ae7f0..005ead5e4 100644 --- a/src/rng.c +++ b/src/rng.c @@ -11,23 +11,31 @@ xoroshiro128** PRNG expects 16 bytes of entropy, this function uses the splitmix64 algorithm to expand the seed. This approach is recommended by the authors of xoroshiro128**, as described here: https://prng.di.unimi.it . + + You must initialize your RngStates with rng_seed/rng_seed_global! + (The all-zeros state has bad statistics.) */ -void rng_seed(int seed) { +void rng_seed(RngState* s, int seed) { uint64_t x = (uint64_t)(seed); - uint32_t a = (uint32_t)rng_seed_impl_next(&x); - uint32_t b = (uint32_t)rng_seed_impl_next(&x); - uint32_t c = (uint32_t)rng_seed_impl_next(&x); - uint32_t d = (uint32_t)rng_seed_impl_next(&x); + s->s[0] = (uint32_t)rng_seed_impl_next(&x); + s->s[1] = (uint32_t)rng_seed_impl_next(&x); + s->s[2] = (uint32_t)rng_seed_impl_next(&x); + s->s[3] = (uint32_t)rng_seed_impl_next(&x); +} - rng_gen_impl_set_initial_state(a, b, c, d); +uint32_t rng_next(RngState* state) { + return rng_gen_impl_next(state); } -/* Update the internal PRNG state and return the next value. +// You must initialize this with rng_seed/rng_seed_global! +static RngState global_rng = { 0 }; - Uses the xoroshiro128** algorithm. -*/ -uint32_t rng_next(void) { - return rng_gen_impl_next(); +void rng_seed_global(int seed) { + return rng_seed(&global_rng, seed); +} + +uint32_t rng_next_global(void) { + return rng_next(&global_rng); } diff --git a/src/rng_gen_impl.c b/src/rng_gen_impl.c index 50f66e709..4b2e3588c 100644 --- a/src/rng_gen_impl.c +++ b/src/rng_gen_impl.c @@ -33,34 +33,25 @@ static inline uint32_t rotl(const uint32_t x, int k) { } -static uint32_t s[4]; +static inline uint32_t next(RngState* s) { + const uint32_t result = rotl(s->s[1] * 5, 7) * 9; -static inline uint32_t next(void) { - const uint32_t result = rotl(s[1] * 5, 7) * 9; + const uint32_t t = s->s[1] << 9; - const uint32_t t = s[1] << 9; + s->s[2] ^= s->s[0]; + s->s[3] ^= s->s[1]; + s->s[1] ^= s->s[2]; + s->s[0] ^= s->s[3]; - s[2] ^= s[0]; - s[3] ^= s[1]; - s[1] ^= s[2]; - s[0] ^= s[3]; + s->s[2] ^= t; - s[2] ^= t; - - s[3] = rotl(s[3], 11); + s->s[3] = rotl(s->s[3], 11); return result; } //// End imported code. // Code in this section wraps the PRNG implementation. -void rng_gen_impl_set_initial_state(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { - s[0] = a; - s[1] = b; - s[2] = c; - s[3] = d; -} - -uint32_t rng_gen_impl_next(void) { - return next(); +uint32_t rng_gen_impl_next(RngState* state) { + return next(state); } From d1a9675488a5d4ad14ab52de4215bc5797f5a8da Mon Sep 17 00:00:00 2001 From: foobar-qw Date: Wed, 18 Oct 2023 03:53:46 -0400 Subject: [PATCH 3/3] Comment --- src/rng_gen_impl.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rng_gen_impl.c b/src/rng_gen_impl.c index 4b2e3588c..b077875f1 100644 --- a/src/rng_gen_impl.c +++ b/src/rng_gen_impl.c @@ -1,8 +1,9 @@ #include "rng_gen_impl.h" //// Start imported code. -// Code in this section copied verbatim from +// Code in this section copied from // https://prng.di.unimi.it/xoshiro128starstar.c +// It was lightly edited to change a global variable to a local variable. /* Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org)