makepad/platform/script/audio_aot/examples/wasm_v8.mjs
Admin 0f4c8082e3 script (audio_aot, compute): audio programs compile to wasm, bit-exact -- the wasm backend's scalar form now compiles audio I/O (in/out channels clamped into 0..n exactly as the interpreter does; audio never fuses), with the kernels' entry type run(ctx, state, shared, io, n, frame): one voice's or the effect's state as today, io = four addresses (in0, in1, out0, out1, n frames each, outputs added into), one call per voice per block; the only import is env.memory (shared in the threaded build). audio_aot::wasm builds one module for a set of shaders (run<k> = shader k), keeps a per-thread link(key, slot) registry (a table slot belongs to the thread's instance) and records compiled shaders for a build tool (record_compiled / take_recorded); AudioShader::program_key and shared_table; on wasm32 run() calls the linked slot when this thread has one, else the interpreter. The golden test had silently skipped the 11 library shaders since they moved to apps/commercial/engine/score_player/shaders; its path is fixed. Checked: cargo test --release -p makepad-script-audio-aot (24 + 1; new: wasm_renders_match_the_golden_hashes on stitch, all 18 golden hashes; wasm_host_slicing_matches_the_interpreter); examples/wasm_v8 in node, slices 128 and 1,7,128,33,64,3: all 18 bit-identical (fm2_svf 20.7 ns/frame, native 20.7, interpreter 496); -p makepad-script-compute --test wasm; wasm_diff 300 rounds both tiers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 13:10:08 +02:00

102 lines
3.9 KiB
JavaScript

// The V8 side of examples/wasm_v8.rs: renders every shader of a batch on
// its generated module, call by call (the host's ctx/state changes applied
// before each), and compares the FNV hash of both output channels with
// the interpreter's.
// Usage: node wasm_v8.mjs <batch file>
// Prints `OK <name> <ns per frame>` or `FAIL <name> <what>` per shader.
import fs from 'node:fs';
const buf = fs.readFileSync(process.argv[2]);
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
let at = 0;
const u32 = () => { const x = dv.getUint32(at, true); at += 4; return x; };
const bytes = (n) => { const b = buf.subarray(at, at + n); at += n; return b; };
const words = () => { const n = u32(); const w = new Uint32Array(n); for (let k = 0; k < n; k++) w[k] = dv.getUint32(at + 4 * k, true); at += 4 * n; return w; };
if (u32() !== 0x31415641) throw new Error('not an audio wasm batch');
const MAX = 128;
function fnv(chans) {
// 64-bit FNV-1a over the little-endian bytes of every word, in BigInt.
let h = 0xcbf29ce484222325n;
const p = 0x100000001b3n, m = (1n << 64n) - 1n;
for (const w of chans) {
for (let k = 0; k < w.length; k++) {
let x = w[k];
for (let b = 0; b < 4; b++) {
h ^= BigInt(x & 0xff);
h = (h * p) & m;
x >>>= 8;
}
}
}
return h;
}
const count = u32();
for (let s = 0; s < count; s++) {
const name = new TextDecoder().decode(bytes(u32()));
const mod = bytes(u32());
const frameWords = u32();
const shared = words(), ctx0 = words(), state0 = words(), inL = words(), inR = words();
const total = inL.length;
// Layout (byte addresses, 16-aligned): ctx, state, shared, io, 4 channels, frame.
let top = 64;
const alloc = (w) => { const a = top; top += (Math.max(w, 1) * 4 + 16 + 15) & ~15; return a; };
const ctx = alloc(ctx0.length), state = alloc(state0.length), sh = alloc(shared.length), io = alloc(4);
const ch = [alloc(MAX), alloc(MAX), alloc(MAX), alloc(MAX)];
const frame = alloc(frameWords);
const ncalls = u32();
const calls = [];
for (let c = 0; c < ncalls; c++) {
const f = u32(), n = u32(), np = u32();
const patch = [];
for (let k = 0; k < np; k++) patch.push([u32(), u32(), u32()]);
calls.push([f, n, patch]);
}
const want = BigInt(u32()) | (BigInt(u32()) << 32n);
let inst, mem;
try {
mem = new WebAssembly.Memory({ initial: Math.ceil(top / 65536) + 1 });
inst = new WebAssembly.Instance(new WebAssembly.Module(mod), { env: { memory: mem } });
} catch (e) {
console.log(`FAIL ${name} compile: ${e.message}`);
continue;
}
const run = inst.exports.run0;
const render = () => {
const I = new Uint32Array(mem.buffer);
I.fill(0);
I.set(ctx0, ctx >> 2);
I.set(state0, state >> 2);
I.set(shared, sh >> 2);
for (let k = 0; k < 4; k++) I[(io >> 2) + k] = ch[k];
const outL = new Uint32Array(total), outR = new Uint32Array(total);
for (const [f, n, patch] of calls) {
for (const [r, w, x] of patch) I[((r === 0 ? ctx : state) >> 2) + w] = x;
I.set(inL.subarray(f, f + n), ch[0] >> 2);
I.set(inR.subarray(f, f + n), ch[1] >> 2);
I.set(outL.subarray(f, f + n), ch[2] >> 2);
I.set(outR.subarray(f, f + n), ch[3] >> 2);
run(ctx, state, sh, io, n, frame);
outL.set(I.subarray(ch[2] >> 2, (ch[2] >> 2) + n), f);
outR.set(I.subarray(ch[3] >> 2, (ch[3] >> 2) + n), f);
}
return [outL, outR];
};
try {
const got = fnv(render());
if (got !== want) {
console.log(`FAIL ${name} hash ${got.toString(16)} want ${want.toString(16)}`);
continue;
}
// Warm (optimized tier), then time a few renders (copies included).
for (let k = 0; k < 3; k++) render();
const reps = 5;
const t0 = performance.now();
for (let k = 0; k < reps; k++) render();
const ns = (performance.now() - t0) * 1e6 / (reps * total);
console.log(`OK ${name} ${ns.toFixed(1)}`);
} catch (e) {
console.log(`FAIL ${name} trap ${e.message}`);
}
}