// CPU vs WebGPU solver on every preset (dev only: open /bench.html). // Query: ?dur=0.05&only=Flat&cpu=0&rings=20 (rings: cymbal mesh override) import { simulate } from '../src/sim/solver'; import { simulateGPU } from '../src/sim/gpu'; import type { BeadSystem, Excitation, SimOptions, SimResult } from '../src/sim/types'; import { CYMBAL_PRESETS } from '../src/instruments/bead-cymbal/presets'; import { buildCymbal, buildCymbalExcitation, buildMic, deriveCymbal } from '../src/instruments/bead-cymbal/model'; import { DRUM_PRESETS } from '../src/instruments/bead-drum/presets'; import { buildDrum, buildDrumExcitation, buildDrumPickup, deriveDrum } from '../src/instruments/bead-drum/model'; import { PRESETS as STRING_PRESETS } from '../src/instruments/bead-string/presets'; import { buildExcitation, buildPickup, buildSystem, derive } from '../src/instruments/bead-string/model'; const qs = new URLSearchParams(location.search); const dur = Number(qs.get('dur') ?? 0.05); const only = qs.get('only'); const runCpu = qs.get('cpu') !== '0'; const rings = qs.get('rings') ? Number(qs.get('rings')) : undefined; const out = document.getElementById('out')!; const lines: string[] = []; const log = (s: string) => { lines.push(s); out.textContent = lines.join('\n'); }; interface Case { name: string; sys: BeadSystem; exc: Excitation; opt: SimOptions } const cases: Case[] = []; for (const pr of CYMBAL_PRESETS) { const p = rings ? { ...pr.params, rings } : pr.params; const d = deriveCymbal(p); const mic = buildMic(p, d); cases.push({ name: `cymbal: ${pr.name}`, sys: buildCymbal(d), exc: buildCymbalExcitation(p, d), opt: { duration: dur, fs: p.fs, cfl: p.cfl, frameRate: p.frameRate, pickup: mic.weights, pickupDelay: mic.delay } }); } for (const pr of DRUM_PRESETS) { const p = pr.params; const d = deriveDrum(p); cases.push({ name: `drum: ${pr.name}`, sys: buildDrum(d), exc: buildDrumExcitation(p, d), opt: { duration: dur, fs: p.fs, cfl: p.cfl, frameRate: p.frameRate, pickup: buildDrumPickup(p, d) } }); } for (const pr of STRING_PRESETS) { const p = pr.params; const d = derive(p); cases.push({ name: `string: ${pr.name}`, sys: buildSystem(p, d), exc: buildExcitation(p), opt: { duration: dur, fs: p.fs, cfl: p.cfl, frameRate: p.frameRate, pickup: buildPickup(p) } }); } const maxAbs = (a: ArrayLike) => { let m = 0; for (let i = 0; i < a.length; i++) m = Math.max(m, Math.abs(a[i])); return m; }; // relative RMS difference of b against a over [0, len) function relDiff(a: ArrayLike, b: ArrayLike, len: number) { let d = 0, s = 0; for (let i = 0; i < len; i++) { d += (a[i] - b[i]) ** 2; s += a[i] ** 2; } return Math.sqrt(d / (s || 1)); } const balErr = (r: SimResult) => maxAbs(r.balance) / (maxAbs(r.energy) || 1); (async () => { const results: unknown[] = []; for (const c of cases) { if (only && !c.name.includes(only)) continue; try { const g = await simulateGPU(c.sys, c.exc, c.opt); const row: Record = { name: c.name, n: c.sys.n, spp: g.stepsPerSample, gpuWall: g.wallSeconds, gpuLoop: g.loopSeconds, usPerStep: (g.loopSeconds * 1e6) / (g.audio.length * g.stepsPerSample), gpuSlow: g.loopSeconds / dur, gpuBal: balErr(g), device: g.device }; if (runCpu) { const r = simulate(c.sys, c.exc, c.opt); const L = r.audio.length; Object.assign(row, { cpuWall: r.wallSeconds, speedup: r.wallSeconds / g.wallSeconds, cpuBal: balErr(r), audio10ms: relDiff(r.audio, g.audio, Math.min(L, Math.round(0.01 * r.fs))), audioAll: relDiff(r.audio, g.audio, L), frames: relDiff(r.frames, g.frames, r.frames.length), energyEnd: g.energy[g.energy.length - 1] / (r.energy[r.energy.length - 1] || 1), stretch: [r.maxStretch, g.maxStretch], contact: r.contactForce ? relDiff(r.contactForce, g.contactForce!, L) : undefined, }); } results.push(row); log(JSON.stringify(row, (_, v) => (typeof v === 'number' ? Number(v.toPrecision(4)) : v))); } catch (e) { log(`${c.name}: ERROR ${(e as Error).message}`); } } log('DONE'); (window as unknown as { benchResults: unknown }).benchResults = results; })();