// Runs one simulation to completion in Node (Dawn WebGPU) and returns the pressure signal. import { create, globals } from "webgpu"; import { defaultParams, type PhysParams } from "../params.ts"; import { Simulation } from "../sim/simulation.ts"; let devicePromise: Promise | null = null; export function getDevice(): Promise { if (!devicePromise) { Object.assign(globalThis, globals); devicePromise = (async () => { const gpu = create([]); const adapter = await gpu.requestAdapter(); if (!adapter) throw new Error("No WebGPU adapter"); const device = await adapter.requestDevice(); device.addEventListener("uncapturederror", (ev) => { console.error("WebGPU error:", (ev as GPUUncapturedErrorEvent).error.message); }); return device; })(); } return devicePromise; } export interface RunResult { params: PhysParams; audio: Float32Array; fs: number; steps: number; seconds: number; grid: { nx: number; ny: number }; dt: number; maxContactForce: number; } export async function runSimulation(overrides: Partial, log: (s: string) => void = () => {}): Promise { const params: PhysParams = { ...defaultParams, ...overrides }; const device = await getDevice(); const sim = new Simulation(device, params); const t0 = performance.now(); let lastLog = t0; let maxContactForce = 0; while (sim.phase !== "done") { await sim.runBatch(); const st = sim.status(); if (st.stick) maxContactForce = Math.max(maxContactForce, st.stick.maxForce); const now = performance.now(); if (now - lastLog > 5000) { lastLog = now; log(` ${st.phase} t=${st.t.toFixed(2)} s, ${(sim.totalSteps / ((now - t0) / 1000) / 1e3).toFixed(0)} k steps/s`); } } log(" loop finished"); const seconds = (performance.now() - t0) / 1000; const result: RunResult = { params, audio: sim.processedAudio().slice(), fs: sim.fs, steps: sim.totalSteps, seconds, grid: { nx: sim.grid.nx, ny: sim.grid.ny }, dt: sim.dt, maxContactForce, }; // Buffers are left to the garbage collector: explicit destroy() aborts inside the Dawn bindings. return result; }