Skip to content

Commit 24e7b2c

Browse files
authored
Add DRNG (Deterministic RNG) and use it to do fuzzing in attribs/gl-vertex-attrib.html. (#3722)
1 parent e5b14d1 commit 24e7b2c

File tree

2 files changed

+396
-10
lines changed

2 files changed

+396
-10
lines changed

sdk/tests/js/js-test-pre.js

Lines changed: 318 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -794,16 +794,327 @@ function finishTest() {
794794
document.body.appendChild(epilogue);
795795
}
796796

797-
/// Prefer `call(() => { ... })` to `(() => { ... })()`\
798-
/// This way, it's clear up-front that we're calling not just defining.
797+
// -
798+
799+
/**
800+
* `=> { return fn(); }`
801+
*
802+
* To be clear up front that we're calling (instead of defining):
803+
* ```
804+
* call(() => {
805+
* ...
806+
* });
807+
* ```
808+
*
809+
* As opposed to:
810+
* ```
811+
* (() => {
812+
* ...
813+
* })();
814+
* ```
815+
*
816+
* @param {function(): any} fn
817+
*/
799818
function call(fn) {
800819
return fn();
801820
}
802821

803-
/// `for (const i of range(3))` => 0, 1, 2
804-
/// Don't use `for...in range(n)`, it will not work.
805-
function* range(n) {
806-
for (let i = 0; i < n; i++) {
807-
yield i;
822+
// -
823+
824+
/**
825+
* A la python:
826+
* * range(3) => [0,1,2]
827+
* * range(1,3) => [1,2]
828+
* @param {number} a
829+
* @param {number} [b]
830+
* @returns {number[]} [min(a,b), ... , max(a,b)-1]
831+
*/
832+
function range(a, b) {
833+
b = b || 0;
834+
const begin = Math.min(a, b);
835+
const end = Math.max(a, b);
836+
return new Array(end-begin).fill().map((_,i) => begin+i);
837+
}
838+
{
839+
let was;
840+
console.assert((was = range(0)).toString() == [].toString(), {was});
841+
console.assert((was = range(1)).toString() == [0].toString(), {was});
842+
console.assert((was = range(3)).toString() == [0,1,2].toString(), {was});
843+
console.assert((was = range(1,3)).toString() == [1,2].toString(), {was});
844+
}
845+
846+
// -
847+
848+
/**
849+
* `=> { throw v; }`
850+
*
851+
* Like `throw`, but usable as an expression not just a statement.\
852+
* E.g. `let found = foo.bar || throwv({foo, msg: 'foo must have .bar!'});`
853+
* @param {any} v
854+
* @throws Always throws `v`!
855+
*/
856+
function throwv(v) {
857+
throw v;
858+
}
859+
860+
// -
861+
862+
/**
863+
* @typedef {object} ConfigDict
864+
* @property {any=} key
865+
*/
866+
867+
/**
868+
* @typedef {ConfigDict[]} ConfigDictList
869+
*/
870+
871+
/**
872+
* @param {...ConfigDictList} comboDimensions
873+
* @returns {ConfigDictList} N-dim Cartesian Product of combinations of the key-value-map objects from each list.
874+
*/
875+
function crossCombine(...comboDimensions) {
876+
function crossCombine2(listA, listB) {
877+
const listC = [];
878+
for (const a of listA) {
879+
for (const b of listB) {
880+
const c = Object.assign({}, a, b);
881+
listC.push(c);
882+
}
883+
}
884+
return listC;
885+
}
886+
887+
let res = [{}];
888+
for (const i in comboDimensions) {
889+
const next = comboDimensions[i] || throwv({i, comboDimensions});
890+
res = crossCombine2(res, next);
891+
}
892+
return res;
893+
}
894+
895+
// -
896+
897+
/**
898+
* @typedef {number} U32 range: `[0, 0xffff_ffff]`
899+
* @typedef {number} I32 range: `[-0x8000_0000, 0x7fff_ffff]`, `0xffff_ffff|0 => -1`
900+
* @typedef {number} F32
901+
*/
902+
903+
/**
904+
* "SHift Right as Uint32"
905+
* @param {U32} val
906+
* @param {number} n
907+
* @returns {U32}
908+
*/
909+
function shr_u32(val, n) {
910+
val >>= n; // In JS this is shr_i32, with sign-extension for negative lhs.
911+
912+
if (n > 0) {
913+
const result_mask = (1 << (32-n)) - 1;
914+
val &= result_mask;
808915
}
916+
917+
return val;
918+
}
919+
console.assert((0xffff_ffff | 0) == -1);
920+
console.assert(0xffff_ff00 >> 4 == (0xffff_fff0 | 0));
921+
console.assert(shr_u32(0xffff_ff00, 4) != (0xffff_fff0 | 0));
922+
console.assert(shr_u32(0xffff_ff00, 4) == (0x0fff_fff0 | 0));
923+
console.assert(shr_u32(0xffff_ff00, 4) == 0x0fff_fff0);
924+
console.assert(shr_u32(0xffff_ff00|0, 4) == 0x0fff_fff0);
925+
926+
/**
927+
* @type {(val: number) => U32}
928+
*/
929+
const bitcast_u32 = call(() => {
930+
const u32View = new Uint32Array(1);
931+
return function bitcast_u32(val) {
932+
u32View[0] = val;
933+
return u32View[0];
934+
};
935+
});
936+
937+
/**
938+
* @type {(u32: U32) => F32}
939+
*/
940+
const bitcast_f32_from_u32 = call(() => {
941+
const u32 = new Uint32Array(1);
942+
const f32 = new Float32Array(u32.buffer);
943+
return function bitcast_f32_from_u32(v) {
944+
u32[0] = v;
945+
return f32[0];
946+
};
947+
});
948+
949+
// -
950+
951+
class PrngXorwow {
952+
/** @type {U32[]} */
953+
actual_seed;
954+
955+
/** @type {U32[]} */
956+
state = new Uint32Array(6); // 5 u32 shuffler + 1 u32 counter.
957+
958+
/**
959+
* @param {U32[] | U32 | undefined} seed
960+
*/
961+
constructor(seed) {
962+
if (typeof(seed) == 'string') {
963+
seed = parseInt(seed);
964+
}
965+
if (typeof(seed) == 'object' && seed.length !== undefined) {
966+
// array-ish
967+
if (!seed.length) {
968+
seed = new Uint32Array(state.length);
969+
crypto.getRandomValues(seed);
970+
} else {
971+
seed = new Uint32Array(seed);
972+
}
973+
} else {
974+
// number?
975+
if (!seed) {
976+
seed = new Uint32Array(1);
977+
crypto.getRandomValues(seed);
978+
} else {
979+
seed = new Uint32Array([seed]);
980+
}
981+
}
982+
983+
// Elide zeros from seed for compactness:
984+
while (seed[seed.length-1] == 0) {
985+
seed = seed.slice(0, seed.length-1);
986+
}
987+
this.actual_seed = seed.slice();
988+
989+
// Seed the state:
990+
const state = this.state;
991+
for (const i in state) {
992+
state[i] = this.actual_seed[i] || 0;
993+
}
994+
console.assert(state[0] || state[1] || state[2] || state[3], "The first four words of seeded state must not all be 0:", state)
995+
996+
}
997+
998+
/**
999+
* (n>=2)
1000+
* @returns {U32 | U32[n]}
1001+
*/
1002+
seed() {
1003+
const seed = this.actual_seed;
1004+
if (seed.length == 1) return seed[0];
1005+
return seed.slice();
1006+
}
1007+
1008+
// -
1009+
1010+
/**
1011+
* @returns {U32[6]}
1012+
*/
1013+
state() {
1014+
return this.state;
1015+
}
1016+
1017+
/**
1018+
* @returns {U32}
1019+
*/
1020+
next_u32() {
1021+
/* Algorithm "xorwow" from p. 5 of Marsaglia, "Xorshift RNGs" */
1022+
const state = this.state;
1023+
let t = state[4];
1024+
1025+
const s = state[0];
1026+
state[4] = state[3];
1027+
state[3] = state[2];
1028+
state[2] = state[1];
1029+
state[1] = s;
1030+
1031+
t ^= shr_u32(t, 2);
1032+
t ^= t << 1;
1033+
t ^= s ^ (s << 4);
1034+
state[0] = t;
1035+
state[5] += 362437;
1036+
1037+
let ret = state[0] + state[5];
1038+
ret = bitcast_u32(ret);
1039+
return ret;
1040+
}
1041+
1042+
/**
1043+
* @returns {number} range: [0.0, 1.0)
1044+
*/
1045+
next_unorm() {
1046+
let ret = this.next_u32();
1047+
const U32_MAX = 0xffff_ffff;
1048+
ret /= (U32_MAX + 1);
1049+
return ret; // [0,1)
1050+
}
1051+
1052+
/**
1053+
* A la crypto.getRandomValues()
1054+
* @param {ArrayBufferView} dest
1055+
* @returns {ArrayBufferView}
1056+
*/
1057+
getRandomValues(dest) {
1058+
const u8s = abv_cast(Uint8Array, dest);
1059+
const len_in_u32 = Math.floor(u8s.length / 4);
1060+
const u32s = abv_cast(Uint32Array, u8s.subarray(0, 4*len_in_u32));
1061+
for (const i in u32s) {
1062+
u32s[i] = this.next_u32();
1063+
}
1064+
for (const i of range(u32s.byteLength, u8s.byteLength)) {
1065+
u8s[i] = this.next_u32(); // Truncates u32 to u8.
1066+
}
1067+
1068+
return dest;
1069+
}
1070+
}
1071+
1072+
// -
1073+
1074+
/**
1075+
* @template {ArrayBufferView} T
1076+
* @param {T.constructor} ctor
1077+
* @param {ArrayBufferView | ArrayBuffer} abv
1078+
* @returns {T}
1079+
*/
1080+
function abv_cast(ctor, abv) {
1081+
if (abv instanceof ArrayBuffer) return new ctor(abv);
1082+
const ctor_bytes_per_element = ctor.BYTES_PER_ELEMENT || 1; // DataView doesn't have BYTES_PER_ELEMENT.
1083+
return new ctor(abv.buffer, abv.byteOffset, abv.byteLength / ctor_bytes_per_element);
1084+
}
1085+
1086+
// -
1087+
1088+
/**
1089+
* @returns {PrngXorwow}
1090+
*/
1091+
function getDrng(defaultSeed=1) {
1092+
if (globalThis._DRNG) return globalThis._DRNG;
1093+
1094+
const seedKeyName = `seed`;
1095+
1096+
const url = new URL(window.location);
1097+
let requestedSeed = url.searchParams.get(seedKeyName);
1098+
if (requestedSeed === null) {
1099+
requestedSeed = defaultSeed;
1100+
}
1101+
1102+
const drng = globalThis._DRNG = new PrngXorwow(requestedSeed);
1103+
const seed = drng.seed();
1104+
1105+
// Run it a few times to avoid seed=1 giving similar values at first.
1106+
for (const _ of range(100)) {
1107+
drng.next_u32();
1108+
}
1109+
1110+
url.searchParams.set(seedKeyName, seed);
1111+
let linkText = `Link to this run's seed: ${url}`;
1112+
if (seed != requestedSeed) {
1113+
linkText += ' (autogenerated)';
1114+
}
1115+
1116+
globalThis.debug && debug(linkText);
1117+
console.log(linkText);
1118+
1119+
return drng;
8091120
}

0 commit comments

Comments
 (0)