1// CPU vs WebGPU solver on every preset (dev only: open /bench.html).
2// Query: ?dur=0.05&only=Flat&cpu=0&rings=20 (rings: cymbal mesh override)
3import { simulate } from '../src/sim/solver';
4import { simulateGPU } from '../src/sim/gpu';
5import type { BeadSystem, Excitation, SimOptions, SimResult } from '../src/sim/types';
6import { CYMBAL_PRESETS } from '../src/instruments/bead-cymbal/presets';
7import { buildCymbal, buildCymbalExcitation, buildMic, deriveCymbal } from '../src/instruments/bead-cymbal/model';
8import { DRUM_PRESETS } from '../src/instruments/bead-drum/presets';
9import { buildDrum, buildDrumExcitation, buildDrumPickup, deriveDrum } from '../src/instruments/bead-drum/model';
10import { PRESETS as STRING_PRESETS } from '../src/instruments/bead-string/presets';
11import { buildExcitation, buildPickup, buildSystem, derive } from '../src/instruments/bead-string/model';
13const qs = new URLSearchParams(location.search);
14const dur = Number(qs.get('dur') ?? 0.05);
15const only = qs.get('only');
16const runCpu = qs.get('cpu') !== '0';
17const rings = qs.get('rings') ? Number(qs.get('rings')) : undefined;
18const out = document.getElementById('out')!;
19const lines: string[] = [];
20const log = (s: string) => {
21 lines.push(s);
22 out.textContent = lines.join('\n');
23};
25interface Case { name: string; sys: BeadSystem; exc: Excitation; opt: SimOptions }
26const cases: Case[] = [];
27for (const pr of CYMBAL_PRESETS) {
28 const p = rings ? { ...pr.params, rings } : pr.params;
29 const d = deriveCymbal(p);
30 const mic = buildMic(p, d);
31 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 } });
32}
33for (const pr of DRUM_PRESETS) {
34 const p = pr.params;
35 const d = deriveDrum(p);
36 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) } });
37}
38for (const pr of STRING_PRESETS) {
39 const p = pr.params;
40 const d = derive(p);
41 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) } });
42}
44const maxAbs = (a: ArrayLike<number>) => {
45 let m = 0;
46 for (let i = 0; i < a.length; i++) m = Math.max(m, Math.abs(a[i]));
47 return m;
48};
49// relative RMS difference of b against a over [0, len)
50function relDiff(a: ArrayLike<number>, b: ArrayLike<number>, len: number) {
51 let d = 0, s = 0;
52 for (let i = 0; i < len; i++) {
53 d += (a[i] - b[i]) ** 2;
54 s += a[i] ** 2;
55 }
56 return Math.sqrt(d / (s || 1));
57}
58const balErr = (r: SimResult) => maxAbs(r.balance) / (maxAbs(r.energy) || 1);
60(async () => {
61 const results: unknown[] = [];
62 for (const c of cases) {
63 if (only && !c.name.includes(only)) continue;
64 try {
65 const g = await simulateGPU(c.sys, c.exc, c.opt);
66 const row: Record<string, unknown> = { 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 };
67 if (runCpu) {
68 const r = simulate(c.sys, c.exc, c.opt);
69 const L = r.audio.length;
70 Object.assign(row, {
71 cpuWall: r.wallSeconds,
72 speedup: r.wallSeconds / g.wallSeconds,
73 cpuBal: balErr(r),
74 audio10ms: relDiff(r.audio, g.audio, Math.min(L, Math.round(0.01 * r.fs))),
75 audioAll: relDiff(r.audio, g.audio, L),
76 frames: relDiff(r.frames, g.frames, r.frames.length),
77 energyEnd: g.energy[g.energy.length - 1] / (r.energy[r.energy.length - 1] || 1),
78 stretch: [r.maxStretch, g.maxStretch],
79 contact: r.contactForce ? relDiff(r.contactForce, g.contactForce!, L) : undefined,
80 });
81 }
82 results.push(row);
83 log(JSON.stringify(row, (_, v) => (typeof v === 'number' ? Number(v.toPrecision(4)) : v)));
84 } catch (e) {
85 log(`${c.name}: ERROR ${(e as Error).message}`);
86 }
87 }
88 log('DONE');
89 (window as unknown as { benchResults: unknown }).benchResults = results;
90})();