13f4dbdexport .cymsim fileJeremy Magland 1// WebGPU time loop: packs the set-up into storage buffers, compiles the four kernels and runs
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 2// the sub-steps of a chunk of audio samples as one command buffer. Works in a browser and under
3// Node with the `webgpu` (Dawn) package, which the tests use.
4import { HOLD_HEIGHT } from "../model/exciter.ts";
5import { strainEnergies } from "../model/forces.ts";
13f4dbdexport .cymsim fileJeremy Magland 6import { DECIMATION_SPAN, DELAY_TAPS } from "../model/radiation.ts";
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 7import type { Setup } from "../model/setup.ts";
13f4dbdexport .cymsim fileJeremy Magland 8import type { BackendOptions, BackendState, ChunkResult, Diagnostics, SimulatorBackend } from "../model/simulator.ts";
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 9import { CHUNK_WORDS, ELEM_STRIDE, GATHER_ELEMS, HIT_HEADER, HITU_HEADER, NODE_STRIDE, RADCOEF_STRIDE, STATE_FLOATS, SYNC_HEADER, WORKGROUP_NODES, shaderSource } from "./shaders.ts";
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 10
11// evaluated lazily: under Node the WebGPU globals are installed after this module is imported
12const storageUsage = () => GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
13f4dbdexport .cymsim fileJeremy Magland 14/** Largest per-chunk output buffer (acceleration rows or movie frames) we allocate on the GPU. */
15const OUTPUT_BUDGET = 64e6;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 17export class GpuSimulator implements SimulatorBackend {
18 readonly kind = "gpu" as const;
19 readonly setup: Setup; readonly device: GPUDevice;
20 samplesDone = 0;
21 private step = 0;
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 22 private readonly G: number; private readonly GE: number; private readonly HF: number; private readonly HU: number;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 23 private readonly buffers: Record<string, GPUBuffer> = {};
24 private readonly pipelines: Record<string, GPUComputePipeline> = {};
25 private readonly bindGroups: Record<string, GPUBindGroup> = {};
13f4dbdexport .cymsim fileJeremy Magland 26 private readonly chunkCapacity: number; readonly movieEvery: number; readonly dispEvery: number; readonly recordAcceleration: boolean;
27 private readonly framesCapacity: number; private readonly dispCapacity: number; private readonly accRows: number;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 28 private readonly stagingPressure: GPUBuffer; private readonly stagingState: GPUBuffer; private readonly stagingHits: GPUBuffer; private readonly stagingHitsU: GPUBuffer;
13f4dbdexport .cymsim fileJeremy Magland 29 private readonly stagingMovie: GPUBuffer | null; private readonly stagingDisp: GPUBuffer | null; private readonly stagingAcc: GPUBuffer | null;
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 30 private readonly radStateFloats: number; // accumulator slots, node history and the current sample's sub-step accelerations: the part of `rad` that is state
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 31
13f4dbdexport .cymsim fileJeremy Magland 32 constructor(device: GPUDevice, setup: Setup, chunkCapacity = 8192, opts: BackendOptions | number = {}) {
33 if (typeof opts === "number") opts = { movieEvery: opts };
34 this.device = device; this.setup = setup;
35 this.movieEvery = opts.movieEvery ?? 0; this.dispEvery = opts.dispEvery ?? 0; this.recordAcceleration = opts.recordAcceleration ?? false;
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 36 const Nn = setup.mesh.n;
13f4dbdexport .cymsim fileJeremy Magland 37 // a chunk's outputs (movie frames, acceleration rows) stay under the budget
38 if (this.movieEvery > 0) chunkCapacity = Math.max(128, Math.min(chunkCapacity, Math.floor((OUTPUT_BUDGET / (4 * Nn)) * this.movieEvery)));
39 if (this.recordAcceleration) chunkCapacity = Math.max(128, Math.min(chunkCapacity, Math.floor(OUTPUT_BUDGET / (4 * Nn)) - 2));
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 40 this.chunkCapacity = chunkCapacity;
13f4dbdexport .cymsim fileJeremy Magland 41 this.framesCapacity = this.movieEvery > 0 ? Math.ceil(chunkCapacity / this.movieEvery) + 2 : 0;
42 this.dispCapacity = this.dispEvery > 0 ? Math.ceil(chunkCapacity / this.dispEvery) + 2 : 0;
43 this.accRows = this.recordAcceleration ? chunkCapacity + 2 : 0;
44 const { mesh, shell: sh, schedule: sc, radiation: rad, substeps: M } = setup;
45 const N = mesh.n, E = mesh.e, K = sc.K, P = sc.P, W = DECIMATION_SPAN, D = rad.ringLength, T = DELAY_TAPS;
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 46 this.G = Math.ceil(N / WORKGROUP_NODES); this.GE = Math.ceil(E / GATHER_ELEMS);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 47 this.HF = HIT_HEADER + 5 * P; this.HU = HITU_HEADER + 3 * P;
48 const G = this.G, HF = this.HF, HU = this.HU;
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 49 this.radStateFloats = (W + D + M) * N;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 50
51 // ---- pack the set-up
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 53 for (let e = 0; e < E; e++) {
54 const b = ELEM_STRIDE * e;
55 for (let i = 0; i < 3; i++) { elemF[b + i] = mesh.Gx[3 * e + i]; elemF[b + 3 + i] = mesh.Gy[3 * e + i]; }
56 elemF[b + 6] = mesh.area[e]; elemF[b + 7] = sh.z0x[e]; elemF[b + 8] = sh.z0y[e];
57 elemF[b + 9] = sh.Cm[3 * e]; elemF[b + 10] = sh.Cm[3 * e + 1]; elemF[b + 11] = sh.Cm[3 * e + 2];
58 elemF[b + 12] = 2 * setup.config.material.sigma1 * sh.rhoHA[e];
59 for (let i = 0; i < 81; i++) elemF[b + 13 + i] = sh.Ke[81 * e + i];
60 }
61 const elemU = new Uint32Array(3 * E); elemU.set(mesh.tri);
13f4dbdexport .cymsim fileJeremy Magland 62 const nodeF = new Float32Array(NODE_STRIDE * N + W * M);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 63 for (let n = 0; n < N; n++) {
64 const b = NODE_STRIDE * n;
65 for (let d = 0; d < 5; d++) { nodeF[b + d] = sh.Minv[5 * n + d]; nodeF[b + 5 + d] = sh.M[5 * n + d]; }
66 for (let k = 0; k < 6; k++) for (let d = 0; d < 5; d++) nodeF[b + 10 + 5 * k + d] = sh.Rm[(k * N + n) * 5 + d];
13f4dbdexport .cymsim fileJeremy Magland 67 for (let d = 0; d < 3; d++) nodeF[b + 40 + d] = sh.normalNode[3 * n + d];
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 68 }
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 70 const adj = new Uint32Array(N + 1 + sh.adjEntries.length); adj.set(sh.adjOffsets); adj.set(sh.adjEntries, N + 1);
71 const hitsF = new Float32Array(HF * K), hitsU = new Uint32Array(HU * K);
72 for (let k = 0; k < K; k++) {
73 const hf = HF * k, hu = HU * k;
74 hitsF[hf] = sc.kPoint[k]; hitsF[hf + 1] = sc.alpha[k]; hitsF[hf + 2] = sc.mass[k]; hitsF[hf + 3] = sc.gap[k];
75 hitsF[hf + 4] = sc.velocity[k]; hitsF[hf + 5] = HOLD_HEIGHT; hitsF[hf + 6] = 0; hitsF[hf + 7] = 0; hitsF[hf + 8] = 0;
76 for (let p = 0; p < P; p++) {
77 for (let i = 0; i < 3; i++) { hitsF[hf + HIT_HEADER + 3 * p + i] = sc.weights[(k * P + p) * 3 + i]; hitsU[hu + HITU_HEADER + 3 * p + i] = sc.nodes[(k * P + p) * 3 + i]; }
78 hitsF[hf + HIT_HEADER + 3 * P + p] = sc.offsets[k * P + p];
79 }
80 hitsU[hu] = sc.releaseStep[k]; hitsU[hu + 1] = 0;
81 }
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 83 for (let e = 0; e < E; e++) {
85 radCoef[b] = rad.coefF[e]; radCoef[b + 1] = rad.coefB[e]; radCoef[b + 2] = rad.i0F[e]; radCoef[b + 3] = rad.i0B[e];
86 for (let t = 0; t < T; t++) { radCoef[b + 4 + t] = rad.tapsF[e * T + t]; radCoef[b + 4 + T + t] = rad.tapsB[e * T + t]; }
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 87 }
88 const mk = (name: string, data: Float32Array | Uint32Array, usage = storageUsage()) => {
89 const buf = device.createBuffer({ label: name, size: Math.max(16, data.byteLength), usage });
90 device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
91 this.buffers[name] = buf;
92 return buf;
93 };
95 const buf = device.createBuffer({ label: name, size: Math.max(16, 4 * floats), usage: storageUsage() });
96 this.buffers[name] = buf;
97 return buf;
98 };
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 99 mk("elemF", elemF); mk("elemU", elemU); mk("nodeF", nodeF); mk("adj", adj); mk("radCoef", radCoef);
100 mk("hitsF", hitsF); mk("hitsU", hitsU);
13f4dbdexport .cymsim fileJeremy Magland 101 mkEmpty("state", STATE_FLOATS * N); mkEmpty("elemForce", 15 * E);
102 mkEmpty("sync", SYNC_HEADER + 2 * G * 12); mkEmpty("rad", this.radStateFloats + this.accRows * N); mkEmpty("elemHist", D * E);
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 103 mkEmpty("pressure", 2 * chunkCapacity * this.GE); // one partial sum per gather workgroup
13f4dbdexport .cymsim fileJeremy Magland 104 const uniform = (name: string) => { const b = device.createBuffer({ label: name, size: 4 * CHUNK_WORDS, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.buffers[name] = b; return b; };
105 uniform("chunk"); uniform("chunkD");
106 const staging = (size: number) => device.createBuffer({ size: Math.max(16, size), usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 107 this.stagingPressure = staging(8 * chunkCapacity * this.GE); this.stagingState = staging(STATE_FLOATS * N * 4);
13f4dbdexport .cymsim fileJeremy Magland 108 this.stagingHits = staging(HF * K * 4); this.stagingHitsU = staging(HU * K * 4);
109 this.stagingMovie = this.framesCapacity > 0 ? (mkEmpty("movie", this.framesCapacity * N), staging(this.framesCapacity * N * 4)) : null;
110 this.stagingDisp = this.dispCapacity > 0 ? (mkEmpty("disp", this.dispCapacity * N), staging(this.dispCapacity * N * 4)) : null;
111 this.stagingAcc = this.accRows > 0 ? staging(this.accRows * N * 4) : null;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 112
113 // ---- pipelines
13f4dbdexport .cymsim fileJeremy Magland 114 const module = device.createShaderModule({ code: shaderSource({ N, E, K, P, M, W, D, G, T, accRows: this.accRows, dt: setup.dt, sigma0: setup.config.material.sigma0, nl: sh.nl }) });
115 type Kind = "read-only-storage" | "storage" | "uniform";
116 const bgl = (entries: [number, Kind][]) => device.createBindGroupLayout({
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 117 entries: entries.map(([binding, type]) => ({ binding, visibility: GPUShaderStage.COMPUTE, buffer: { type } })),
118 });
13f4dbdexport .cymsim fileJeremy Magland 119 const L0 = bgl([[0, "read-only-storage"], [1, "read-only-storage"], [2, "read-only-storage"], [3, "storage"], [4, "storage"], [5, "read-only-storage"], [6, "storage"]]);
120 const L1 = bgl([[0, "storage"], [1, "read-only-storage"], [2, "read-only-storage"], [3, "read-only-storage"], [4, "storage"], [5, "storage"], [6, "storage"], [7, "storage"], [8, "uniform"]]);
121 const L2 = bgl([[0, "read-only-storage"], [1, "read-only-storage"], [2, "read-only-storage"], [3, "storage"], [4, "uniform"], [5, "storage"], [6, "read-only-storage"]]);
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 122 const L3 = bgl([[0, "read-only-storage"], [1, "storage"], [2, "uniform"], [3, "read-only-storage"]]);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 123 const empty = device.createBindGroupLayout({ entries: [] });
124 const mkPipe = (entryPoint: string, layouts: GPUBindGroupLayout[]) => device.createComputePipeline({
125 label: entryPoint, layout: device.createPipelineLayout({ bindGroupLayouts: layouts }), compute: { module, entryPoint },
126 });
127 this.pipelines.element = mkPipe("elementPass", [L0]);
128 this.pipelines.node = mkPipe("nodePass", [empty, L1]);
129 this.pipelines.gather = mkPipe("gatherPass", [empty, empty, L2]);
13f4dbdexport .cymsim fileJeremy Magland 130 if (this.framesCapacity > 0 || this.dispCapacity > 0) this.pipelines.capture = mkPipe("capturePass", [empty, empty, empty, L3]);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 131 const B = this.buffers;
132 const bg = (layout: GPUBindGroupLayout, bufs: GPUBuffer[]) => device.createBindGroup({ layout, entries: bufs.map((buffer, binding) => ({ binding, resource: { buffer } })) });
13f4dbdexport .cymsim fileJeremy Magland 133 this.bindGroups.element = bg(L0, [B.elemF, B.elemU, B.state, B.elemForce, B.hitsF, B.hitsU, B.sync]);
134 this.bindGroups.node = bg(L1, [B.state, B.elemForce, B.nodeF, B.adj, B.hitsF, B.hitsU, B.sync, B.rad, B.chunk]);
135 this.bindGroups.gather = bg(L2, [B.sync, B.rad, B.radCoef, B.pressure, B.chunk, B.elemHist, B.elemU]);
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 136 if (this.framesCapacity > 0) this.bindGroups.capture = bg(L3, [B.state, B.movie, B.chunk, B.sync]);
13f4dbdexport .cymsim fileJeremy Magland 137 if (this.dispCapacity > 0) this.bindGroups.captureDisp = bg(L3, [B.state, B.disp, B.chunkD, B.sync]);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 138 this.bindGroups.empty = device.createBindGroup({ layout: empty, entries: [] });
139 }
13f4dbdexport .cymsim fileJeremy Magland 141 /** Audio sample finished by the node pass of sub-step n (the acceleration of s = n - 1), or -1 when none. */
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 142 private sampleDueAt(n: number): number {
143 const M = this.setup.substeps, W = DECIMATION_SPAN;
144 if (n < 1 || (n - 1) % M !== 0) return -1;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 146 return j >= 0 ? j : -1;
147 }
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 149 get maxChunk() { return this.chunkCapacity; }
13f4dbdexport .cymsim fileJeremy Magland 151 private capture(pass: GPUComputePassEncoder, group: GPUBindGroup) {
152 pass.setPipeline(this.pipelines.capture); pass.setBindGroup(0, this.bindGroups.empty); pass.setBindGroup(1, this.bindGroups.empty);
153 pass.setBindGroup(2, this.bindGroups.empty); pass.setBindGroup(3, group); pass.dispatchWorkgroups(this.G);
154 }
156 private async readStaging(staging: GPUBuffer, bytes: number): Promise<Float32Array> {
157 await staging.mapAsync(GPUMapMode.READ, 0, bytes);
158 const data = new Float32Array(staging.getMappedRange(0, bytes).slice(0));
159 staging.unmap();
160 return data;
161 }
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 163 async runChunk(nSamples: number): Promise<ChunkResult> {
164 if (nSamples > this.chunkCapacity) throw new Error("chunk larger than the pressure buffer");
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 165 const { device, setup } = this, M = setup.substeps, E = setup.mesh.e, K = setup.schedule.K, N = setup.mesh.n;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 166 const n0 = this.step, n1 = n0 + nSamples * M;
167 let base = -1, count = 0;
168 for (let n = n0; n < n1; n++) { const j = this.sampleDueAt(n); if (j >= 0) { if (base < 0) base = j; count++; } }
13f4dbdexport .cymsim fileJeremy Magland 169 // frames captured in this chunk: the state at the start of sub-step n where n / M is a multiple of `every`
170 const framesAt = (every: number) => { const out: number[] = []; if (every > 0) for (let n = n0; n < n1; n++) if (n % (M * every) === 0) out.push(n / M); return out; };
171 const frameSamples = framesAt(this.movieEvery), dispSamples = framesAt(this.dispEvery);
172 const frameBase = frameSamples.length ? frameSamples[0] / this.movieEvery : 0, dispBase = dispSamples.length ? dispSamples[0] / this.dispEvery : 0;
173 const accBase = base < 0 ? 0 : base, accRows = this.recordAcceleration ? Math.max(count, 0) : 0;
174 device.queue.writeBuffer(this.buffers.chunk, 0, new Int32Array([accBase, Math.max(count, 1), frameBase, Math.max(this.movieEvery, 1), accBase, accRows, 0, 0]));
175 device.queue.writeBuffer(this.buffers.chunkD, 0, new Int32Array([accBase, Math.max(count, 1), dispBase, Math.max(this.dispEvery, 1), accBase, accRows, 0, 0]));
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 176 const enc = device.createCommandEncoder();
177 enc.clearBuffer(this.buffers.pressure);
178 const pass = enc.beginComputePass();
179 const wgE = Math.ceil((E + K) / 64), wgN = this.G;
180 for (let n = n0; n < n1; n++) {
13f4dbdexport .cymsim fileJeremy Magland 181 if (this.movieEvery > 0 && n % (M * this.movieEvery) === 0) this.capture(pass, this.bindGroups.capture);
182 if (this.dispEvery > 0 && n % (M * this.dispEvery) === 0) this.capture(pass, this.bindGroups.captureDisp);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 183 pass.setPipeline(this.pipelines.element); pass.setBindGroup(0, this.bindGroups.element); pass.dispatchWorkgroups(wgE);
13f4dbdexport .cymsim fileJeremy Magland 184 pass.setPipeline(this.pipelines.node); pass.setBindGroup(0, this.bindGroups.empty); pass.setBindGroup(1, this.bindGroups.node); pass.dispatchWorkgroups(wgN);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 185 if (this.sampleDueAt(n) >= 0) {
186 pass.setPipeline(this.pipelines.gather); pass.setBindGroup(0, this.bindGroups.empty); pass.setBindGroup(1, this.bindGroups.empty);
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 187 pass.setBindGroup(2, this.bindGroups.gather); pass.dispatchWorkgroups(this.GE);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 188 }
189 }
190 pass.end();
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 191 const GE = this.GE;
192 enc.copyBufferToBuffer(this.buffers.pressure, 0, this.stagingPressure, 0, 8 * this.chunkCapacity * GE);
13f4dbdexport .cymsim fileJeremy Magland 193 const nFrames = frameSamples.length, nDisp = dispSamples.length;
3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 194 if (nFrames > 0) enc.copyBufferToBuffer(this.buffers.movie, 0, this.stagingMovie!, 0, nFrames * N * 4);
13f4dbdexport .cymsim fileJeremy Magland 195 if (nDisp > 0) enc.copyBufferToBuffer(this.buffers.disp, 0, this.stagingDisp!, 0, nDisp * N * 4);
196 if (accRows > 0) enc.copyBufferToBuffer(this.buffers.rad, this.radStateFloats * 4, this.stagingAcc!, 0, accRows * N * 4);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 197 device.queue.submit([enc.finish()]);
198 this.step = n1; this.samplesDone += nSamples;
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 199 const partial = await this.readStaging(this.stagingPressure, 8 * this.chunkCapacity * GE);
200 const all = new Float32Array(2 * this.chunkCapacity);
201 for (let i = 0; i < 2 * Math.max(count, 1); i++) { let s = 0; for (let g = 0; g < GE; g++) s += partial[i * GE + g]; all[i] = s; }
13f4dbdexport .cymsim fileJeremy Magland 202 const split = (data: Float32Array, k: number) => Array.from({ length: k }, (_, f) => data.slice(f * N, (f + 1) * N));
203 const frames = nFrames > 0 ? split(await this.readStaging(this.stagingMovie!, nFrames * N * 4), nFrames) : [];
204 const dispFrames = nDisp > 0 ? split(await this.readStaging(this.stagingDisp!, nDisp * N * 4), nDisp) : [];
205 const acceleration = this.recordAcceleration ? (accRows > 0 ? await this.readStaging(this.stagingAcc!, accRows * N * 4) : new Float32Array(0)) : null;
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 206 const size = Math.max(count, 1);
13f4dbdexport .cymsim fileJeremy Magland 207 return { firstSample: base < 0 ? this.samplesDone : base, pressureFront: all.slice(0, count), pressureBack: all.slice(size, size + count),
208 frames, frameSamples, acceleration, dispFrames, dispSamples };
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 209 }
211 private async read(buffer: GPUBuffer, staging: GPUBuffer, bytes: number): Promise<ArrayBuffer> {
212 const enc = this.device.createCommandEncoder();
213 enc.copyBufferToBuffer(buffer, 0, staging, 0, bytes);
214 this.device.queue.submit([enc.finish()]);
216 const out = staging.getMappedRange(0, bytes).slice(0);
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 217 staging.unmap();
218 return out;
219 }
13f4dbdexport .cymsim fileJeremy Magland 221 /** Read a whole buffer through a temporary staging buffer. */
222 private async readWhole(buffer: GPUBuffer, bytes: number): Promise<ArrayBuffer> {
223 const staging = this.device.createBuffer({ size: Math.max(16, bytes), usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
224 try { return await this.read(buffer, staging, bytes); } finally { staging.destroy(); }
225 }
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 227 async diagnostics(): Promise<Diagnostics> {
228 const { setup } = this, sh = setup.shell, N = setup.mesh.n, sc = setup.schedule, HF = this.HF, HU = this.HU;
13f4dbdexport .cymsim fileJeremy Magland 229 const st = new Float32Array(await this.read(this.buffers.state, this.stagingState, STATE_FLOATS * N * 4));
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 230 const hf = new Float32Array(await this.read(this.buffers.hitsF, this.stagingHits, Math.max(16, HF * sc.K * 4)));
231 const hu = new Uint32Array(await this.read(this.buffers.hitsU, this.stagingHitsU, Math.max(16, HU * sc.K * 4)));
232 let ke = 0, woh = 0;
233 for (let i = 0; i < 5 * N; i++) ke += 0.5 * sh.M[i] * st[5 * N + i] * st[5 * N + i];
234 for (let n = 0; n < N; n++) woh = Math.max(woh, Math.abs(st[5 * n + 2] / sh.hNode[n]));
235 const { membrane, bending } = strainEnergies(sh, st.subarray(0, 5 * N));
236 let ex = 0;
237 const hits = [];
238 for (let k = 0; k < sc.K; k++) {
239 ex += 0.5 * hf[HF * k + 2] * hf[HF * k + 6] ** 2;
240 hits.push({ peakForce: hf[HF * k + 8], contactTime: hu[HU * k + 1] * setup.dt, velocity: hf[HF * k + 6], height: hf[HF * k + 5] });
241 }
242 return { time: this.step * setup.dt, kinetic: ke, membrane, bending, exciters: ex, maxWoverH: woh, hits };
243 }
245 async displacement(): Promise<Float32Array> {
246 const N = this.setup.mesh.n;
13f4dbdexport .cymsim fileJeremy Magland 247 const st = new Float32Array(await this.read(this.buffers.state, this.stagingState, STATE_FLOATS * N * 4));
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 248 const w = new Float32Array(N);
249 for (let n = 0; n < N; n++) w[n] = st[5 * n + 2];
250 return w;
251 }
254 const { setup } = this, N = setup.mesh.n, E = setup.mesh.e, K = setup.schedule.K, D = setup.radiation.ringLength;
255 const B = this.buffers;
256 return { kind: "gpu", step: this.step, samplesDone: this.samplesDone, arrays: {
257 state: new Float32Array(await this.readWhole(B.state, STATE_FLOATS * N * 4)),
258 sync: new Float32Array(await this.readWhole(B.sync, (SYNC_HEADER + 2 * this.G * 12) * 4)),
259 hitsF: new Float32Array(await this.readWhole(B.hitsF, Math.max(16, this.HF * K * 4))).subarray(0, this.HF * K),
260 hitsU: new Uint32Array(await this.readWhole(B.hitsU, Math.max(16, this.HU * K * 4))).subarray(0, this.HU * K),
261 rad: new Float32Array(await this.readWhole(B.rad, this.radStateFloats * 4)),
262 elemHist: new Float32Array(await this.readWhole(B.elemHist, D * E * 4)),
263 } };
264 }
266 async importState(st: BackendState): Promise<void> {
267 if (st.kind !== "gpu") throw new Error("state was written by another backend");
268 const { setup } = this, N = setup.mesh.n, E = setup.mesh.e, K = setup.schedule.K, D = setup.radiation.ringLength;
269 const expect: Record<string, number> = { state: STATE_FLOATS * N, sync: SYNC_HEADER + 2 * this.G * 12, hitsF: this.HF * K, hitsU: this.HU * K, rad: this.radStateFloats, elemHist: D * E };
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 270 // states written before the sub-step accelerations were kept per sample lack them; their accumulator
271 // slots already hold the sample's earlier sub-steps, so zero accelerations continue them exactly
272 const legacyRad = this.radStateFloats - setup.substeps * N;
56d33ddGPU: decimate once per audio sample and spread the listener sum over all elementsJeremy Magland 274 let a = st.arrays[name];
275 if (name === "rad" && a?.length === legacyRad) { const full = new Float32Array(len); full.set(a); a = full; }
13f4dbdexport .cymsim fileJeremy Magland 276 if (!a || a.length !== len) throw new Error(`state array ${name} is missing or has the wrong size`);
277 this.device.queue.writeBuffer(this.buffers[name], 0, a.buffer, a.byteOffset, a.byteLength);
278 }
279 this.step = st.step; this.samplesDone = st.samplesDone;
280 }
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 282 /** The state buffer (q at offset 0, 5 floats per node), for a renderer that reads w directly. */
283 get stateBuffer(): GPUBuffer { return this.buffers.state; }
285 dispose() {
286 for (const b of Object.values(this.buffers)) b.destroy();
13f4dbdexport .cymsim fileJeremy Magland 287 this.stagingPressure.destroy(); this.stagingState.destroy(); this.stagingHits.destroy(); this.stagingHitsU.destroy();
288 this.stagingMovie?.destroy(); this.stagingDisp?.destroy(); this.stagingAcc?.destroy();
1e29264cymsimweb: WebGPU companion of cymsim with YAML/form editor, live spectrogram and playbackJeremy Magland 289 }
290}