From 492c068cdddafb3d22c1ad1c3959bf757b862eed Mon Sep 17 00:00:00 2001 From: Sally Coolatta Date: Sat, 12 Nov 2022 09:48:59 -0500 Subject: [PATCH] Try some things to reduce rng biases - Don't clamp all RNG calls to [0, FRACUNIT-1]. Only does this for P_RandomFixed now. - Use rejection sampling for any clamped RNG calls, to remove modulo bias. - Because of this, P_RandomRange ranges >= FRACUNIT are no longer undefined behavior. - Added P_Random/M_Random to grab RNG output directly. - Shuffle M_Random's RNG as well, since OS rand can be [0,INT32_MAX] instead of [0,UINT32_MAX]. --- src/lua_baselib.c | 9 +--- src/m_random.c | 126 +++++++++++++++++++++++++++++++++++----------- src/m_random.h | 16 +++--- src/st_stuff.c | 4 +- 4 files changed, 110 insertions(+), 45 deletions(-) diff --git a/src/lua_baselib.c b/src/lua_baselib.c index d73e7b073..fcdf86624 100644 --- a/src/lua_baselib.c +++ b/src/lua_baselib.c @@ -402,10 +402,7 @@ static int lib_pRandomByte(lua_State *L) static int lib_pRandomKey(lua_State *L) { INT32 a = (INT32)luaL_checkinteger(L, 1); - NOHUD - if (a > 65536) - LUA_UsageWarning(L, "P_RandomKey: range > 65536 is undefined behavior"); lua_pushinteger(L, P_RandomKey(PR_UNDEFINED, a)); demo_writerng = 2; return 1; @@ -415,15 +412,13 @@ static int lib_pRandomRange(lua_State *L) { INT32 a = (INT32)luaL_checkinteger(L, 1); INT32 b = (INT32)luaL_checkinteger(L, 2); - NOHUD - if (b < a) { + if (b < a) + { INT32 c = a; a = b; b = c; } - if ((b-a+1) > 65536) - LUA_UsageWarning(L, "P_RandomRange: range > 65536 is undefined behavior"); lua_pushinteger(L, P_RandomRange(PR_UNDEFINED, a, b)); demo_writerng = 2; return 1; diff --git a/src/m_random.c b/src/m_random.c index 166c6bf2e..6d8beeaa3 100644 --- a/src/m_random.c +++ b/src/m_random.c @@ -25,20 +25,56 @@ // RNG functions (not synched) // --------------------------- +ATTRINLINE static UINT32 FUNCINLINE __external_prng__(void) +{ + UINT32 rnd = rand(); + +#if RAND_MAX < 65535 + // Compensate for especially bad randomness. + UINT32 rndv = (rand() & 1) << 15; + rnd ^= rndv; +#endif + + // Shuffle like we do for our own PRNG, since RAND_MAX + // tends to be [0, INT32_MAX] instead of [0, UINT32_MAX]. + rnd ^= rnd >> 13; + rnd ^= rnd >> 11; + rnd ^= rnd << 21; + return (rnd * 36548569); +} + +ATTRINLINE static UINT32 FUNCINLINE __external_prng_bound__(UINT32 bound) +{ + // Do rejection sampling to remove the modulo bias. + UINT32 threshold = -bound % bound; + for (;;) + { + UINT32 r = __external_prng__(); + if (r >= threshold) + { + return r % bound; + } + } +} + +/** Provides a random 32-bit number. Distribution is uniform. + * As with all M_Random functions, not synched in netgames. + * + * \return A random 32-bit number. + */ +UINT32 M_Random(void) +{ + return __external_prng__(); +} + /** Provides a random fixed point number. Distribution is uniform. * As with all M_Random functions, not synched in netgames. * - * \return A random fixed point number from [0,1). + * \return A random fixed point number from [0,1]. */ fixed_t M_RandomFixed(void) { -#if RAND_MAX < 65535 - // Compensate for insufficient randomness. - fixed_t rndv = (rand()&1)<<15; - return rand()^rndv; -#else - return (rand() & 0xFFFF); -#endif + return (fixed_t)(__external_prng_bound__(FRACUNIT)); } /** Provides a random byte. Distribution is uniform. @@ -48,7 +84,7 @@ fixed_t M_RandomFixed(void) */ UINT8 M_RandomByte(void) { - return (rand() & 0xFF); + return (UINT8)(__external_prng_bound__(UINT8_MAX)); } /** Provides a random integer for picking random elements from an array. @@ -58,9 +94,9 @@ UINT8 M_RandomByte(void) * \param a Number of items in array. * \return A random integer from [0,a). */ -INT32 M_RandomKey(INT32 a) +UINT32 M_RandomKey(UINT32 a) { - return (INT32)((rand()/((unsigned)RAND_MAX+1.0f))*a); + return __external_prng_bound__(a); } /** Provides a random integer in a given range. @@ -73,7 +109,7 @@ INT32 M_RandomKey(INT32 a) */ INT32 M_RandomRange(INT32 a, INT32 b) { - return (INT32)((rand()/((unsigned)RAND_MAX+1.0f))*(b-a+1))+a; + return (INT32)(__external_prng_bound__((b - a) + 1)) + a; } @@ -92,23 +128,56 @@ typedef struct static rng_t rng; // The entire PRNG state -/** Provides a random fixed point number. +/** Provides a random 32 bit integer. * This is a variant of an xorshift PRNG; state fits in a 32 bit integer structure. * - * \return A random fixed point number from [0,1). + * \return A random, uniformly distributed number from [0,UINT32_MAX]. */ -ATTRINLINE static fixed_t FUNCINLINE __internal_prng__(pr_class_t pr_class) +ATTRINLINE static UINT32 FUNCINLINE __internal_prng__(pr_class_t pr_class) { rng.seed[pr_class] ^= rng.seed[pr_class] >> 13; rng.seed[pr_class] ^= rng.seed[pr_class] >> 11; rng.seed[pr_class] ^= rng.seed[pr_class] << 21; - return ( (rng.seed[pr_class] * 36548569) >> 4) & (FRACUNIT-1); + return (rng.seed[pr_class] * 36548569); +} + +/** Provides a random number within a specified range. + * + * \return A random, uniformly distributed number from [0,bound]. + */ +ATTRINLINE static UINT32 FUNCINLINE __internal_prng_bound__(pr_class_t pr_class, UINT32 bound) +{ + // Do rejection sampling to remove the modulo bias. + UINT32 threshold = -bound % bound; + for (;;) + { + UINT32 r = __internal_prng__(pr_class); + if (r >= threshold) + { + return r % bound; + } + } } /** Provides a random fixed point number. Distribution is uniform. * Literally a wrapper for the internal PRNG function. * - * \return A random fixed point number from [0,1). + * \return A random fixed point number from [0,UINT32_MAX]. + */ +#ifndef DEBUGRANDOM +UINT32 P_Random(pr_class_t pr_class) +{ +#else +UINT32 P_RandomD(const char *rfile, INT32 rline, pr_class_t pr_class) +{ + CONS_Printf("P_Random(%u) at: %sp %d\n", pr_class, rfile, rline); +#endif + return __internal_prng__(pr_class); +} + +/** Provides a random fixed point number. Distribution is uniform. + * + * \return A random fixed point number from [0,1]. */ #ifndef DEBUGRANDOM fixed_t P_RandomFixed(pr_class_t pr_class) @@ -118,14 +187,14 @@ fixed_t P_RandomFixedD(const char *rfile, INT32 rline, pr_class_t pr_class) { CONS_Printf("P_RandomFixed(%u) at: %sp %d\n", pr_class, rfile, rline); #endif - return __internal_prng__(pr_class); + return (fixed_t)(__internal_prng_bound__(pr_class, FRACUNIT)); } /** Provides a random byte. Distribution is uniform. * If you're curious, (&0xFF00) >> 8 gives the same result * as a fixed point multiplication by 256. * - * \return Random integer from [0, 255]. + * \return Random integer from [0,255]. * \sa __internal_prng__ */ #ifndef DEBUGRANDOM @@ -136,7 +205,7 @@ UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class) { CONS_Printf("P_RandomByte(%u) at: %sp %d\n", pr_class, rfile, rline); #endif - return (UINT8)((__internal_prng__(pr_class) & 0xFF00) >> 8); + return (UINT8)(__internal_prng_bound__(pr_class, UINT8_MAX)); } /** Provides a random integer for picking random elements from an array. @@ -144,23 +213,22 @@ UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class) * NOTE: Maximum range is 65536. * * \param a Number of items in array. - * \return A random integer from [0,a). + * \return A random integer from [0,a]. * \sa __internal_prng__ */ #ifndef DEBUGRANDOM -INT32 P_RandomKey(pr_class_t pr_class, INT32 a) +UINT32 P_RandomKey(pr_class_t pr_class, UINT32 a) { #else -INT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a) +UINT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, UINT32 a) { CONS_Printf("P_RandomKey(%u) at: %sp %d\n", pr_class, rfile, rline); #endif - return (INT32)(((INT64)__internal_prng__(pr_class) * a) >> FRACBITS); + return __internal_prng_bound__(pr_class, a); } /** Provides a random integer in a given range. * Distribution is uniform. - * NOTE: Maximum range is 65536. * * \param a Lower bound. * \param b Upper bound. @@ -175,7 +243,7 @@ INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 { CONS_Printf("P_RandomRange(%u) at: %sp %d\n", pr_class, rfile, rline); #endif - return (INT32)(((INT64)__internal_prng__(pr_class) * (b - a + 1)) >> FRACBITS) + a; + return (INT32)(__internal_prng_bound__(pr_class, (b - a) + 1)) + a; } @@ -187,13 +255,13 @@ INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 /** Peeks to see what the next result from the PRNG will be. * Used for debugging. * - * \return A 'random' fixed point number from [0,1). + * \return A 'random' number from [0,UINT32_MAX] * \sa __internal_prng__ */ -fixed_t P_RandomPeek(pr_class_t pr_class) +UINT32 P_RandomPeek(pr_class_t pr_class) { UINT32 r = rng.seed[pr_class]; - fixed_t ret = __internal_prng__(pr_class); + UINT32 ret = __internal_prng__(pr_class); rng.seed[pr_class] = r; return ret; } diff --git a/src/m_random.h b/src/m_random.h index c08e32993..5bb5d4435 100644 --- a/src/m_random.h +++ b/src/m_random.h @@ -69,37 +69,41 @@ typedef enum // P_Random functions pulls random bytes from a PRNG that is network synced. // RNG functions +UINT32 M_Random(void); fixed_t M_RandomFixed(void); UINT8 M_RandomByte(void); -INT32 M_RandomKey(INT32 a); +UINT32 M_RandomKey(UINT32 a); INT32 M_RandomRange(INT32 a, INT32 b); // PRNG functions #ifdef DEBUGRANDOM +#define P_Random(c) P_RandomD(__FILE__, __LINE__, c) #define P_RandomFixed(c) P_RandomFixedD(__FILE__, __LINE__, c) #define P_RandomByte(c) P_RandomByteD(__FILE__, __LINE__, c) #define P_RandomKey(c, d) P_RandomKeyD(__FILE__, __LINE__, c, d) #define P_RandomRange(c, d, e) P_RandomRangeD(__FILE__, __LINE__, c, d, e) +UINT32 P_RandomD(const char *rfile, INT32 rline, pr_class_t pr_class); fixed_t P_RandomFixedD(const char *rfile, INT32 rline, pr_class_t pr_class); UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class); -INT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a); +UINT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, UINT32 a); INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a, INT32 b); #else +UINT32 P_Random(pr_class_t pr_class); fixed_t P_RandomFixed(pr_class_t pr_class); UINT8 P_RandomByte(pr_class_t pr_class); -INT32 P_RandomKey(pr_class_t pr_class, INT32 a); +UINT32 P_RandomKey(pr_class_t pr_class, UINT32 a); INT32 P_RandomRange(pr_class_t pr_class, INT32 a, INT32 b); #endif // Macros for other functions -#define M_SignedRandom() ((INT32)M_RandomByte() - 128) // [-128, 127] signed byte, originally a -#define P_SignedRandom(pr) ((INT32)P_RandomByte(pr) - 128) // function of its own, moved to a macro +#define M_SignedRandom() ((INT32)M_RandomByte() + INT8_MIN) // [-128, 127] signed byte, originally a +#define P_SignedRandom(pr) ((INT32)P_RandomByte(pr) + INT8_MIN) // function of its own, moved to a macro #define M_RandomChance(p) (M_RandomFixed() < p) // given fixed point probability, p, between 0 (0%) #define P_RandomChance(pr, p) (P_RandomFixed(pr) < p) // and FRACUNIT (100%), returns true p% of the time // Debugging -fixed_t P_RandomPeek(pr_class_t pr_class); +UINT32 P_RandomPeek(pr_class_t pr_class); // Working with the seed for PRNG #ifdef DEBUGRANDOM diff --git a/src/st_stuff.c b/src/st_stuff.c index 676c3992a..5c2def212 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -411,12 +411,10 @@ static void ST_drawDebugInfo(void) // Figure out some other way to display all of the RNG classes. fixed_t peekres = P_RandomPeek(PR_UNDEFINED); - peekres *= 10000; // Change from fixed point - peekres >>= FRACBITS; // to displayable decimal V_DrawRightAlignedString(320, height - 16, V_MONOSPACE, va("Init: %08x", P_GetInitSeed(PR_UNDEFINED))); V_DrawRightAlignedString(320, height - 8, V_MONOSPACE, va("Seed: %08x", P_GetRandSeed(PR_UNDEFINED))); - V_DrawRightAlignedString(320, height, V_MONOSPACE, va("== : .%04d", peekres)); + V_DrawRightAlignedString(320, height, V_MONOSPACE, va("== : %08x", peekres)); height -= 32; }