import { assert } from '../../common/util/util.js'; import { kValue } from './constants.js'; /** * Seed-able deterministic pseudo random generator for the WebGPU CTS * * This generator requires setting a seed value and the sequence of values * generated is deterministic based on the seed. * * This generator is intended to be a replacement for Math.random(). * * This generator is not cryptographically secure, though nothing in the CTS * should be needing cryptographic security. * * The current implementation is based on TinyMT * (https://github.com/MersenneTwister-Lab/TinyMT), which is a version of * Mersenne Twister that has reduced the internal state size at the cost of * shortening the period length of the generated sequence. The period is still * 2^127 - 1 entries long, so should be sufficient for use in the CTS, but it is * less costly to create multiple instances of the class. */ export class PRNG { // Storing variables for temper() as members, so they don't need to be // reallocated per call to temper() private readonly t_vars: Uint32Array; // Storing variables for next() as members, so they don't need to be // reallocated per call to next() private readonly n_vars: Uint32Array; // Generator internal state private readonly state: Uint32Array; // Default tuning parameters for TinyMT. // These are tested to not generate an all zero initial state. private static readonly kMat1: number = 0x8f7011ee; private static readonly kMat2: number = 0xfc78ff1f; private static readonly kTMat: number = 0x3793fdff; // TinyMT algorithm internal magic numbers private static readonly kMask = 0x7fffffff; private static readonly kMinLoop = 8; private static readonly kPreLoop = 8; private static readonly kSH0 = 1; private static readonly kSH1 = 10; private static readonly kSH8 = 8; // u32.max + 1, used to scale the u32 value from temper() to [0, 1). private static readonly kRandomDivisor = 4294967296.0; /** * constructor * * @param seed value used to initialize random number sequence. Results are * guaranteed to be deterministic based on this. * This value must be in the range of unsigned 32-bit integers. * Non-integers will be rounded. */ constructor(seed: number) { assert(seed >= 0 && seed <= kValue.u32.max, 'seed to PRNG needs to a u32'); this.t_vars = new Uint32Array(2); this.n_vars = new Uint32Array(2); this.state = new Uint32Array([Math.round(seed), PRNG.kMat1, PRNG.kMat2, PRNG.kTMat]); for (let i = 1; i < PRNG.kMinLoop; i++) { this.state[i & 3] ^= i + Math.imul(1812433253, this.state[(i - 1) & 3] ^ (this.state[(i - 1) & 3] >>> 30)); } // Check that the initial state isn't all 0s, since the algorithm assumes // that this never occurs assert( (this.state[0] & PRNG.kMask) !== 0 || this.state[1] !== 0 || this.state[2] !== 0 || this.state[2] !== 0, 'Initialization of PRNG unexpectedly generated all 0s initial state, this means the tuning parameters are bad' ); for (let i = 0; i < PRNG.kPreLoop; i++) { this.next(); } } /** Advances the internal state to the next values */ private next() { this.n_vars[0] = (this.state[0] & PRNG.kMask) ^ this.state[1] ^ this.state[2]; this.n_vars[1] = this.state[3]; this.n_vars[0] ^= this.n_vars[0] << PRNG.kSH0; this.n_vars[1] ^= (this.n_vars[1] >>> PRNG.kSH0) ^ this.n_vars[0]; this.state[0] = this.state[1]; this.state[1] = this.state[2]; this.state[2] = this.n_vars[0] ^ (this.n_vars[1] << PRNG.kSH1); this.state[3] = this.n_vars[1]; if ((this.n_vars[1] & 1) !== 0) { this.state[1] ^= PRNG.kMat1; this.state[2] ^= PRNG.kMat2; } } /** @returns a 32-bit unsigned integer based on the current state */ private temper(): number { this.t_vars[0] = this.state[3]; this.t_vars[1] = this.state[0] + (this.state[2] >>> PRNG.kSH8); this.t_vars[0] ^= this.t_vars[1]; if ((this.t_vars[1] & 1) !== 0) { this.t_vars[0] ^= PRNG.kTMat; } return this.t_vars[0]; } /** @returns a value on the range of [0, 1) and advances the state */ public random(): number { this.next(); return this.temper() / PRNG.kRandomDivisor; } /** @returns a 32-bit unsigned integer value and advances the state */ public randomU32(): number { this.next(); return this.temper(); } /** @returns a uniformly selected integer in [0, N-1]. N must be at least 1 and at most 2**32. */ public uniformInt(N: number) { const upperBound = (1 << 16) * (1 << 16); assert(N === Math.trunc(N)); // It's an integer assert(N > 0, `${N} must be positive`); assert(N <= upperBound, `${N} is too big, should be at most ${upperBound}`); // Use a method described in The Stanford GraphBase, // Donald E. Knuth (New York: ACM Press, 1994), viii+576pp. // Co-published by Addison-Wesley Publishing Company. // See GB_FLIP, section 12, Uniform Integers. // A naive algorithm would take a random u32 value X, and then // return (X % N). But this is biased toward smaller values // when N is not a power of 2. As Knuth writes, if N is // (2**32) / 3, this naive algorithm would return values // less than N/2 about 2/3 of the time. // Instead, we eliminate the bias by discarding samples when // they would have been biased. The "keep zone", so to speak, // must be a multiple of N. We make the algorithm efficient // by maximizing the size of the keep zone: Find the largest // multiple of N that fits within a u32. // On average, this algorithm will discard 2 or fewer samples. // Find the largest multiple of N that fits below upperBound. const keepZoneSize = upperBound - (upperBound % N); assert(keepZoneSize % N === 0); // It covers a big chunk of the whole u32 range. assert(keepZoneSize >= upperBound / 2); // Draw u32 values until we find one in the keep zone. let candidate: number; do { candidate = this.randomU32(); } while (candidate >= keepZoneSize); // Now return the candidate, but folding away multiples of N. return candidate % N; } }