Basic idea. I have some WGSL code:
var<private> rnd_state : u32 = 2891336453u;
fn pcg(v: u32) -> u32 {
let state = v * 747796405u + 2891336453u;
let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
return (word >> 22u) ^ word;
}
fn random_uint() -> u32{
rnd_state = pcg(rnd_state);
return rnd_state;
}
fn hash_to_float01( hash:u32 ) -> f32{
return bitcast<f32>(0x3f800000 | (hash >> 9)) - 1.0f;
}
fn random() -> f32{
let h = random_uint();
return hash_to_float01( h );
}
This is a snippet from Shade, the bit that we care about here is the random function.
We test this by writing a bog standard JavaScript unit test, no WebGPU support required:
test('random() returns a finite number in [0, 1) and successive calls differ', () => {
const lib = ComputeShaderEmulator.fromCodeChunk(wgsl_source, { parser });
const a = lib.random();
const b = lib.random();
const c = lib.random();
for (const v of [a, b, c]) {
expect(typeof v).toBe('number');
expect(Number.isFinite(v)).toBe(true);
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThan(1);
}
expect(a).not.toBe(b);
expect(b).not.toBe(c);
expect(a).not.toBe(c);
});
And this works, it runs and it passes.
This is something I have been thinking of for a very long time, I even wrote on the idea here a while ago.
How does this work?
The basic idea is to translate WGSL into JavaScript, we have a compiler that takes wgsl string and spits out a JS string like this:
(wgsl) => {
let rnd_state = 2891336453;
function pcg(v) {
const state = wgsl.add(wgsl.mul(v, 747796405), 2891336453);
const word = wgsl.mul(((state >> (wgsl.add((state >> 28), 4))) ^ state), 277803737);
return (word >> 22) ^ word;
}
function random_uint() {
rnd_state = pcg(rnd_state);
return rnd_state;
}
function hash_to_float01(hash) {
return wgsl.sub(wgsl.bitcast(0x3f800000 | (hash >> 9), "f32", "u32"), 1.0);
}
function random() {
const h = random_uint();
return hash_to_float01(h);
}
const __module__ = {};
__module__.pcg = pcg;
__module__.random_uint = random_uint;
__module__.hash_to_float01 = hash_to_float01;
__module__.random = random;
Object.defineProperty(__module__, "rnd_state", { get: () => rnd_state, set: (v) => { rnd_state = v; } });
return __module__;
}
The one important point here is that the JavaScript source looks very close to WGSL. Meaning that if our tests don’t pass - we can step through the source code with the debugger.
Shade is written with a module abstraction for WGSL code, I call it CodeChunk, here’s the one for random:
import { CodeChunk } from "../../compiler/CodeChunk.js";
import { chunk_hash_to_float01 } from "./chunk_hash_to_float01.js";
import { chunk_random_uint } from "./chunk_random_uint.js";
/**
* A pseudo random number.
* Generate a random float in [0,1) range
*/
export const chunk_random = CodeChunk.from(
//language=WGSL
`
fn random() -> f32{
let h = random_uint();
return hash_to_float01( h );
}
`,
[
chunk_random_uint,
chunk_hash_to_float01,
]
)
All this, to say that the way the code is laid out lends itself well to unit testing.
Note on the example:
The random() was picked because it’s small-enough to be illustrative, but at the same time, it uses var<private> storage space, it performs chained function calls and even does bitcasting. These are all fairly complex mechanisms to faithfully replicate in JavaScript.
FAQ
What about complete shaders?
Complete shaders are supported, for now only compute though. Dispatch mechanism is per-thread. This is still a debug / testing tool primarily.
Limitations?
Surprisingly few, even with this first version:
- no
subgroupsupport - no 3d texture support
Is this a cross-compiler?
Yes, essentially, with a small runtime.