makepad/platform/script/audio_aot/examples/wasm_v8.rs

201 lines
8.6 KiB
Rust

//! The generated wasm of every example and shipped library audio shader in
//! V8 (the engine of Chrome and node): each renders the golden test's
//! scripted performance (two notes, a param change, releases; effects on
//! a chirp with clicks) in node, call by call as a host makes them, and
//! must give the interpreter's bits (an FNV hash of both channels, the
//! golden test's). Prints V8's cost per frame next to the interpreter's.
//!
//! ```text
//! cargo run --release -p makepad-script-audio-aot --example wasm_v8 -- [--dir <scratch>] [--slices 128,7,1]
//! ```
//!
//! Needs `node` on the PATH. The batch node reads (see wasm_v8.mjs), per
//! shader: name, module, memory layout, shared tables, initial ctx and
//! state, both inputs, then per call its first frame, n and the ctx/state
//! words the host changed since the previous call (note on/off, params,
//! the frame counter), then the expected hash.
use makepad_script_audio_aot::{compile_with, AudioShaderRef, Backend, Kind, CTX_FRAME, MAX_FRAMES};
const RATE: f32 = 48000.0;
const TOTAL: usize = 30000;
fn sources() -> Vec<(String, String)> {
let root = env!("CARGO_MANIFEST_DIR");
let mut out: Vec<(String, String)> = ["saw_svf", "fm4", "pluck", "wavetable_pad", "fm2_svf", "allpass_reverb", "waveshaper4x"]
.iter()
.map(|n| (n.to_string(), std::fs::read_to_string(format!("{}/shaders/{}.splash", root, n)).unwrap()))
.collect();
if let Ok(dir) = std::fs::read_dir(format!("{}/../../../libs/score_player/shaders", root)) {
let mut names: Vec<_> = dir.filter_map(|e| e.ok()).map(|e| e.path()).collect();
names.sort();
for p in names {
out.push((format!("lib/{}", p.file_stem().unwrap().to_string_lossy()), std::fs::read_to_string(&p).unwrap()));
}
}
out
}
/// The golden test's input for effects.
fn test_input(n: usize) -> (Vec<f32>, Vec<f32>) {
let mut l = vec![0.0; n];
let mut r = vec![0.0; n];
for i in 0..n {
let t = i as f32 / RATE;
let env = (-(t % 0.5) * 6.0).exp();
l[i] = (t * 220.0 * (1.0 + t) * std::f32::consts::TAU).sin() * env * 0.7;
r[i] = if i % 9000 == 0 { 0.9 } else { l[i] * 0.5 };
}
(l, r)
}
fn fnv(bits: impl Iterator<Item = u32>) -> u64 {
let mut h = 0xcbf2_9ce4_8422_2325u64;
for b in bits {
for byte in b.to_le_bytes() {
h ^= byte as u64;
h = h.wrapping_mul(0x100_0000_01b3);
}
}
h
}
struct Out(Vec<u8>);
impl Out {
fn u32(&mut self, x: u32) {
self.0.extend_from_slice(&x.to_le_bytes());
}
fn bytes(&mut self, b: &[u8]) {
self.u32(b.len() as u32);
self.0.extend_from_slice(b);
}
fn words(&mut self, w: impl ExactSizeIterator<Item = u32>) {
self.u32(w.len() as u32);
for x in w {
self.u32(x);
}
}
}
/// Renders the performance on the interpreter, writing the batch entry;
/// returns (hash, interpreter ns per frame).
fn record(out: &mut Out, name: &str, shader: &AudioShaderRef, slices: &[usize]) -> (u64, f64) {
let module = makepad_script_audio_aot::wasm::module(&[shader], Default::default()).expect("compiles to wasm");
out.bytes(name.as_bytes());
out.bytes(&module);
out.u32(makepad_script_audio_aot::wasm::frame_words(shader) as u32);
out.words(shader.shared_table().iter().copied());
let mut ctx = vec![0u32; shader.ctx_words()];
shader.init_ctx(&mut ctx, RATE);
let mut state = vec![0u32; shader.state_words()];
shader.reset_state(&mut state);
out.words(ctx.iter().copied());
out.words(state.iter().copied());
let (in_l, in_r) = if shader.kind == Kind::Effect { test_input(TOTAL) } else { (vec![0.0; TOTAL], vec![0.0; TOTAL]) };
out.words(in_l.iter().map(|x| x.to_bits()));
out.words(in_r.iter().map(|x| x.to_bits()));
let mut scratch = vec![0u32; shader.scratch_words()];
let (mut out_l, mut out_r) = (vec![0.0f32; TOTAL], vec![0.0f32; TOTAL]);
let events = [0usize, 12345, 24000, 30000, 52000];
let (mut prev_ctx, mut prev_state) = (ctx.clone(), state.clone());
let mut calls = Out(Vec::new());
let mut ncalls = 0;
let (mut frame, mut k) = (0usize, 0usize);
let mut secs = 0.0;
while frame < TOTAL {
for (e, at) in events.iter().enumerate() {
if *at == frame {
match e {
0 => shader.note_on(&mut state, 57.0, 0.9, frame as u32, 7),
1 => {
if !shader.params().is_empty() {
let p = &shader.params()[0];
shader.set_param(&mut ctx, 0, p.min + (p.max - p.min) * 0.3);
}
}
2 => shader.note_off(&mut state),
3 => shader.note_on(&mut state, 64.5, 0.6, frame as u32, 11),
_ => shader.note_off(&mut state),
}
}
}
let next_event = events.iter().copied().filter(|e| *e > frame).min().unwrap_or(TOTAL);
let n = slices[k % slices.len()].min(next_event - frame).min(TOTAL - frame).min(MAX_FRAMES as usize);
k += 1;
ctx[CTX_FRAME as usize] = frame as u32;
// The words the host changed since the program last left them.
let mut patch = Vec::new();
for (region, (now, was)) in [(&ctx, &prev_ctx), (&state, &prev_state)].into_iter().enumerate() {
for (w, (a, b)) in now.iter().zip(was.iter()).enumerate() {
if a != b {
patch.push((region as u32, w as u32, *a));
}
}
}
calls.u32(frame as u32);
calls.u32(n as u32);
calls.u32(patch.len() as u32);
for (r, w, x) in patch {
calls.u32(r);
calls.u32(w);
calls.u32(x);
}
ncalls += 1;
let t0 = std::time::Instant::now();
shader.run_interp(&mut ctx, &mut state, &mut scratch, &in_l[frame..frame + n], &in_r[frame..frame + n], &mut out_l[frame..frame + n], &mut out_r[frame..frame + n], n);
secs += t0.elapsed().as_secs_f64();
prev_ctx.copy_from_slice(&ctx);
prev_state.copy_from_slice(&state);
frame += n;
}
out.u32(ncalls);
out.0.extend_from_slice(&calls.0);
let hash = fnv(out_l.iter().chain(out_r.iter()).map(|x| x.to_bits()));
out.u32(hash as u32);
out.u32((hash >> 32) as u32);
(hash, secs * 1e9 / TOTAL as f64)
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let arg = |k: &str| args.iter().position(|a| a == k).and_then(|i| args.get(i + 1)).cloned();
let dir = std::path::PathBuf::from(arg("--dir").unwrap_or_else(|| std::env::temp_dir().join("audio_wasm_v8").to_string_lossy().into_owned()));
let slices: Vec<usize> = arg("--slices").map(|s| s.split(',').filter_map(|x| x.parse().ok()).filter(|x| *x > 0).collect()).unwrap_or_else(|| vec![128]);
std::fs::create_dir_all(&dir).unwrap();
let mut out = Out(Vec::new());
out.u32(0x3141_5641); // "AVA1"
let srcs = sources();
out.u32(srcs.len() as u32);
let mut interp_ns = Vec::new();
for (name, src) in &srcs {
let shader = compile_with(src, Backend::Interp).unwrap_or_else(|e| panic!("{}: {}", name, e[0].message));
let (_, ns) = record(&mut out, name, &shader, &slices);
interp_ns.push(ns);
}
let path = dir.join("audio_batch.bin");
std::fs::write(&path, &out.0).unwrap();
let script = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/wasm_v8.mjs");
let run = std::process::Command::new("node").arg(script).arg(&path).output().expect("run node (is it on the PATH?)");
let text = String::from_utf8_lossy(&run.stdout);
let _ = std::fs::remove_file(&path);
if !run.status.success() {
panic!("node failed: {}\n{}", text, String::from_utf8_lossy(&run.stderr));
}
let mut fails = 0;
println!("{:22} {:>8} {:>12} {:>12}", "shader", "", "v8 ns/f", "interp ns/f");
for (line, ns) in text.lines().filter(|l| l.starts_with("OK ") || l.starts_with("FAIL ")).zip(&interp_ns) {
let mut it = line.split_whitespace();
let (verdict, name, v8) = (it.next().unwrap(), it.next().unwrap(), it.next().unwrap_or("-"));
fails += (verdict == "FAIL") as usize;
println!("{:22} {:>8} {:>12} {:>12.1}", name, verdict, v8, ns);
}
for l in text.lines().filter(|l| !l.starts_with("OK ") && !l.starts_with("FAIL ")) {
println!("{}", l);
}
if fails > 0 {
eprintln!("{} of {} shaders differ in V8", fails, srcs.len());
std::process::exit(1);
}
println!("all {} shaders bit-identical in V8", srcs.len());
}