c8442c5Command-line renderer (Node WebGPU via Dawn) and gallery generatorJeremy Magland 1// Runs one simulation to completion in Node (Dawn WebGPU) and returns the pressure signal.
3import { defaultParams, type PhysParams } from "../params.ts";
4import { Simulation } from "../sim/simulation.ts";
6let devicePromise: Promise<GPUDevice> | null = null;
8export function getDevice(): Promise<GPUDevice> {
9 if (!devicePromise) {
10 devicePromise = (async () => {
d94838aLoad the Dawn binding lazily so the Chrome engine runs without itJeremy Magland 11 // Loaded lazily: the prebuilt binding needs a recent glibc and is only used by --engine dawn.
12 const { create, globals } = await import("webgpu");
13 Object.assign(globalThis, globals);
c8442c5Command-line renderer (Node WebGPU via Dawn) and gallery generatorJeremy Magland 14 const gpu = create([]);
b640535CLI: Chrome engine (Playwright) as default, Dawn optional; gallery fixesJeremy Magland 15 const adapter = await gpu.requestAdapter();
c8442c5Command-line renderer (Node WebGPU via Dawn) and gallery generatorJeremy Magland 16 if (!adapter) throw new Error("No WebGPU adapter");
b640535CLI: Chrome engine (Playwright) as default, Dawn optional; gallery fixesJeremy Magland 17 const device = await adapter.requestDevice();
18 device.addEventListener("uncapturederror", (ev) => {
19 console.error("WebGPU error:", (ev as GPUUncapturedErrorEvent).error.message);
20 });
21 return device;
23 }
24 return devicePromise;
25}
27export interface RunResult {
28 params: PhysParams;
29 audio: Float32Array;
30 fs: number;
31 steps: number;
32 seconds: number;
33 grid: { nx: number; ny: number };
34 dt: number;
35 maxContactForce: number;
36}
38export async function runSimulation(overrides: Partial<PhysParams>, log: (s: string) => void = () => {}): Promise<RunResult> {
39 const params: PhysParams = { ...defaultParams, ...overrides };
40 const device = await getDevice();
41 const sim = new Simulation(device, params);
42 const t0 = performance.now();
43 let lastLog = t0;
44 let maxContactForce = 0;
45 while (sim.phase !== "done") {
46 await sim.runBatch();
47 const st = sim.status();
48 if (st.stick) maxContactForce = Math.max(maxContactForce, st.stick.maxForce);
49 const now = performance.now();
50 if (now - lastLog > 5000) {
51 lastLog = now;
52 log(` ${st.phase} t=${st.t.toFixed(2)} s, ${(sim.totalSteps / ((now - t0) / 1000) / 1e3).toFixed(0)} k steps/s`);
53 }
54 }
b640535CLI: Chrome engine (Playwright) as default, Dawn optional; gallery fixesJeremy Magland 55 log(" loop finished");
c8442c5Command-line renderer (Node WebGPU via Dawn) and gallery generatorJeremy Magland 56 const seconds = (performance.now() - t0) / 1000;
57 const result: RunResult = {
58 params,
59 audio: sim.processedAudio().slice(),
60 fs: sim.fs,
61 steps: sim.totalSteps,
62 seconds,
63 grid: { nx: sim.grid.nx, ny: sim.grid.ny },
64 dt: sim.dt,
65 maxContactForce,
66 };
b640535CLI: Chrome engine (Playwright) as default, Dawn optional; gallery fixesJeremy Magland 67 // Buffers are left to the garbage collector: explicit destroy() aborts inside the Dawn bindings.
c8442c5Command-line renderer (Node WebGPU via Dawn) and gallery generatorJeremy Magland 68 return result;
69}