/ simulated_instruments / cymbal2
Sign in
simulated_instruments / cymbal2
cymbal2 / webapp / scripts / gpu-selftest.ts
169 lines · 6.1 KBCodeBlameHistory
65ecc03Run the time loop on the GPU in the web app when WebGPU is availableJeremy Magland 1// Runs the webapp's WebGPU time loop (src/worker/gpuSim.ts) from Node against
2// desktop WebGPU (the optional `webgpu` package, prebuilt Google Dawn) on the
3// validation suite's reduced configuration, and checks the pinned contact
4// diagnostics and output rms. This exercises exactly the code the browser
5// worker runs; a machine without the package or without a GPU reports and
6// exits 0 so CI can run it opportunistically.
7// npm i --no-save webgpu && npx tsx scripts/gpu-selftest.ts
9import { readFileSync, readdirSync } from 'node:fs';
10import { fileURLToPath } from 'node:url';
11import path from 'node:path';
12import { executeCode, RTV, type RuntimeValue, type RuntimeTensor } from 'numbl';
14const MATLAB_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../matlab');
16async function installDawn(): Promise<boolean> {
17 try {
18 const mod = await import('webgpu');
19 Object.assign(globalThis, mod.globals);
20 Object.defineProperty(globalThis, 'navigator', {
21 value: { gpu: mod.create([]) },
22 configurable: true,
23 });
24 return true;
25 } catch {
26 return false;
27 }
30function mfiles() {
31 return readdirSync(MATLAB_DIR)
32 .filter((f) => f.endsWith('.m'))
33 .sort()
34 .map((name) => ({ name, source: readFileSync(path.join(MATLAB_DIR, name), 'utf8') }));
37const scalar = (v: RuntimeValue): number => {
38 if (typeof v === 'number') return v;
39 const t = v as RuntimeTensor;
40 if (t && t.kind === 'tensor' && t.data.length === 1) return t.data[0];
41 throw new Error('expected scalar');
42};
43const f32 = (v: RuntimeValue): Float32Array => {
44 const t = v as RuntimeTensor;
45 return Float32Array.from(t.data);
46};
48async function main() {
49 if (!(await installDawn())) {
50 console.log('gpu-selftest: `webgpu` package not installed; skipping (npm i --no-save webgpu)');
51 return;
52 }
53 const { requestGpuDevice, runGpuSim } = await import('../src/worker/gpuSim');
55 const files = mfiles();
56 const P = `
57P = cymbal2_defaults();
58P.sample_rate = 44100;
59P.oversample = 1;
60P.tail_s = 1.5;
61`;
62 const r = executeCode(`${P}
63S = cymbal2_system(P);
64MB = cymbal2_mallet(P);
65G = S.G;
66o_D1 = G.D1; o_D2 = G.D2; o_F = G.F; o_Gi = G.Gi; o_Rd = G.Rd; o_R2d = G.R2d;
67o_AiB = S.AiB; o_AiC = S.AiC; o_Ai = S.Ai; o_Fi = S.Fi;
68o_ASiB = MB.ASiB; o_ASiC = MB.ASiC;
69o_vM = MB.vM; o_vR = MB.vR; o_vK = MB.vK; o_tipcol = MB.tipcol;
70K = max(1, round(P.strike_count));
71tstr = P.pre_delay_ms * 1e-3 + (0:K - 1) / max(1e-3, P.strike_rate_hz);
72vstr = zeros(1, K);
73state = P.seed * 2654435761;
74for i = 1:K
75 state = mod(state * 1664525 + 1013904223, 4294967296);
76 x = state / 4294967296;
77 vstr(i) = max(0.05, P.strike_velocity * (1 + P.velocity_spread * (2 * x - 1)));
78end
79gap = P.strike_gap_m;
80if gap <= 0
81 gap = 1.05 * max(vstr)^2 / (2 * P.strike_accel) + 0.005;
82end
83travel = gap ./ vstr + vstr / (2 * P.strike_accel);
84tlaunch = tstr - travel;
85off = -min(0, min(tlaunch));
86tlaunch = tlaunch + off;
87duration = off + (K - 1) / max(1e-3, P.strike_rate_hz) + P.tail_s;
88nsteps = floor(duration * P.sample_rate * P.oversample);
89o_tlaunch = tlaunch; o_vstr = vstr;
90c_nrt = G.nrt; c_Nr = G.Nr; c_nth = G.nth; c_hr = G.hr; c_hth = G.hth;
91c_ht = S.ht; c_Eh = S.Eh; c_Rs = S.Rs; c_rhoh = S.rhoh;
92c_fsc = S.forceScale; c_nlr0 = S.eomRow0;
93c_ls = S.ls; c_js = S.js; c_lp = S.lp; c_jp = S.jp;
94c_tipi = MB.tipi; c_nfree = MB.nfree; c_ps = MB.ps;
95c_KM = P.head_stiffness; c_alM = P.head_alpha;
96c_aH = P.strike_accel; c_aHr = P.retract_accel;
97c_holdSteps = max(1, round(P.strike_hold_ms * 1e-3 / S.ht));
98c_missDepth = gap + 0.06;
99c_gap = gap; c_K = K; c_nsteps = nsteps;
100`, { displayResults: false }, files, 'stage.m');
101 const v = r.variableValues;
102 const s = (n: string) => scalar(v[n]);
103 const t = (n: string) => f32(v[n]);
104 const setup = {
105 ops: {
106 D1: t('o_D1'), D2: t('o_D2'), F: t('o_F'), Gi: t('o_Gi'),
107 Rd: t('o_Rd'), R2d: t('o_R2d'),
108 AiB: t('o_AiB'), AiC: t('o_AiC'), Ai: t('o_Ai'), Fi: t('o_Fi'),
109 ASiB: t('o_ASiB'), ASiC: t('o_ASiC'),
110 vM: t('o_vM'), vR: t('o_vR'), vK: t('o_vK'), tipcol: t('o_tipcol'),
111 },
112 tlaunch: Array.from((v.o_tlaunch as RuntimeTensor).data),
113 vstr: Array.from((v.o_vstr as RuntimeTensor).data),
114 nrt: s('c_nrt'), Nr: s('c_Nr'), nth: s('c_nth'),
115 hr: s('c_hr'), hth: s('c_hth'),
116 ht: s('c_ht'), Eh: s('c_Eh'), Rs: s('c_Rs'), rhoh: s('c_rhoh'),
117 fsc: s('c_fsc'), nlr0: s('c_nlr0'),
118 ls: s('c_ls'), js: s('c_js'), lp: s('c_lp'), jp: s('c_jp'),
119 tipi: s('c_tipi'), nfree: s('c_nfree'), ps: s('c_ps'),
120 KM: s('c_KM'), alM: s('c_alM'), aH: s('c_aH'), aHr: s('c_aHr'),
121 holdSteps: s('c_holdSteps'), missDepth: s('c_missDepth'),
122 gap: s('c_gap'), K: s('c_K'), nsteps: s('c_nsteps'),
123 };
125 let device;
126 try {
127 device = await requestGpuDevice();
128 } catch (e) {
129 console.log(`gpu-selftest: no WebGPU device (${e instanceof Error ? e.message : e}); skipping`);
130 return;
131 }
132 const t0 = performance.now();
133 const sim = await runGpuSim(device, setup, () => {});
134 device.destroy();
135 const ms = performance.now() - t0;
137 const ro = executeCode(`${P}
138[y, peakAccel] = cymbal2_output(P, acc);
139`, { displayResults: false, initialVariableValues: { acc: RTV.tensor(Float64Array.from(sim.acc), [1, sim.acc.length]) } }, files, 'stage.m');
140 const y = f32(ro.variableValues.y);
141 let ss = 0;
142 for (let i = 0; i < y.length; i++) ss += y[i] * y[i];
143 const rms = Math.sqrt(ss / y.length);
144 let peakF = 0;
145 for (let i = 0; i < sim.fhist.length; i++) peakF = Math.max(peakF, sim.fhist[i]);
146 const firstC = sim.fhist.findIndex((x) => x > 0);
148 console.log(
149 `gpu-selftest: ${setup.nsteps} steps in ${(ms / 1000).toFixed(1)} s ` +
150 `(${((1000 * ms) / setup.nsteps).toFixed(0)} us/step); ` +
151 `peakF ${peakF.toFixed(2)} N (ref 311.14), cvel ${sim.contactVel[0].toFixed(3)} (ref -8.223), ` +
152 `first contact step ${firstC + 1} (ref 1376), rms ${rms.toFixed(4)} (ref 0.0973)`,
153 );
154 const ok =
155 Math.abs(peakF - 311.14) < 3 &&
156 Math.abs(sim.contactVel[0] + 8.223) < 0.08 &&
157 firstC + 1 === 1376 &&
158 Math.abs(rms - 0.0973) < 0.012;
159 if (!ok) {
160 console.error('gpu-selftest: FAILED');
161 process.exit(1);
162 }
163 console.log('gpu-selftest: OK');
166main().then(() => process.exit(0)).catch((e) => {
167 console.error(e);
168 process.exit(1);
169});
↑↓ move↵openescclose