/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
steelsim41 / src / sim / simulation.ts
634 lines · 24.5 KBCodeBlameHistory
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 1// WebGPU time-stepping driver for the von Karman plate.
2//
3// The simulation advances in batches of one movie frame (1/fps seconds). A
4// batch encodes one compute dispatch per time step (with a per-step uniform
5// addressed by dynamic offset), an optional rigid-body projection every
6// RB_INTERVAL steps, one retarded-time listener sum over the batch's audio
7// samples, and one movie-frame extraction at the end. Audio, frame, and
8// statistics are read back after every batch. One batch is kept encoded
9// ahead of the one being read back, so the GPU does not wait for the
10// readback or for the encoding. The recording starts at the impact, with the
11// plate at rest in its stress-free shape.
13import {
14 type SimParams,
15 type Plate,
16 type Grid,
17 strikePoint,
18 contactPatch,
19 mountPatch,
20 massProperties,
21 thicknessRange,
22 nodeX,
23 nodeY,
26 nodeWeights,
27 CFL,
28 HIST_MARGIN,
29 RB_INTERVAL,
30 RHO_AIR,
31 SPEED_OF_SOUND,
32} from "../params.ts";
33import { maxAngularFrequency } from "./cpuRef.ts";
34import commonWgsl from "./shaders/common.wgsl?raw";
35import stepWgsl from "./shaders/step.wgsl?raw";
36import rigidWgsl from "./shaders/rigid.wgsl?raw";
37import audioWgsl from "./shaders/audio.wgsl?raw";
38import extractWgsl from "./shaders/extract.wgsl?raw";
40export type Phase = "recording" | "done";
42export interface StickState {
43 x: number;
44 v: number;
45 force: number;
46 maxForce: number;
a46d644Update simulation, step shader, renderer and theory notesJeremy Magland 47 /** True once the contact has been released for longer than the shader's STICK_RELEASE; the stick then no longer touches the plate. */
48 withdrawn: boolean;
51export interface SimStatus {
52 phase: Phase;
53 /** Simulation time since the impact [s]. */
54 t: number;
55 totalSteps: number;
56 ke: number;
57 wmax: number;
58 samples: number;
59 frames: number;
60 peakPressure: number;
61 delayClamped: boolean;
62 stick: StickState;
65const PARAMS_LEN = 40; // floats in the Params uniform
66const STEP_LEN = 12; // floats in the StepParams uniform
67const STATS_LEN = 16;
a46d644Update simulation, step shader, renderer and theory notesJeremy Magland 68const STICK_LEN = 16; // two 8-float slots of stick state
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 69/** Nodes per workgroup along each axis in step.wgsl. */
70const TILE = 12;
72/** A batch that has been encoded and submitted but not read back. */
73interface Pending {
74 staging: GPUBuffer;
75 steps: number;
76 nAudio: number;
77 /** Which of the two stick-state slots the batch's last step wrote. */
78 stickParity: number;
81export class Simulation {
82 readonly plate: Plate;
83 readonly grid: Grid;
84 readonly dt: number;
85 /** Time steps per audio sample. */
86 readonly stepsPerSample: number;
87 readonly samplesPerBatch: number;
88 readonly stepsPerBatch: number;
89 readonly omegaMax: number;
90 /** Ring buffer length of the listener sum [samples]. */
91 readonly histLen: number;
92 /** Total mass of the plate [kg]. */
93 readonly mass: number;
95 /** Listener pressure [Pa] at fs, from the impact. */
96 audio: Float32Array = new Float32Array(0);
97 samples = 0;
98 /** Movie frames of w [m]; frame k is at t = k / fps. */
99 frames: Float32Array<ArrayBuffer>[] = [];
101 phase: Phase = "recording";
102 totalSteps = 0;
103 ke = 0;
104 wmax = 0;
105 peakPressure = 0;
106 delayClamped = false;
108 private stick: StickState;
109 private inFlight: Pending | null = null;
111 private device: GPUDevice;
112 private params: SimParams;
113 private paramsBuf: GPUBuffer;
114 private stepBuf: GPUBuffer;
115 private stepStride: number;
116 private stepData: ArrayBuffer;
117 private pos: GPUBuffer[];
118 private vel: GPUBuffer[];
119 private hist: GPUBuffer;
120 private stickBuf: GPUBuffer;
121 /** Static per-node data for the kernels, packed as (w0, weight, thickness, 0) per node. */
122 private nd: GPUBuffer;
123 /** Stress-free initial shape (f32 per node), for the renderer. */
124 readonly w0: GPUBuffer;
125 /** Largest magnitude of the initial shape [m]. */
126 readonly w0Max: number;
127 /** Node weights (f32 per node, 0 outside the plate), for the renderer. */
128 readonly nw: GPUBuffer;
129 /** Thickness (f32 per node, 0 outside the plate), for the renderer. */
130 readonly hn: GPUBuffer;
131 private lw: GPUBuffer;
132 private audioOut: GPUBuffer;
133 private frameBuf: GPUBuffer;
134 private statsBuf: GPUBuffer;
135 private staging: GPUBuffer[];
136 private stagingIdx = 0;
137 private stagingSize: number;
138 /** Index of the buffer pair holding the current state. */
139 private cur = 0;
140 /** Plain f32 array of w for the live view. */
141 readonly displayW: GPUBuffer;
143 private stepPipe: GPUComputePipeline;
144 private sumsPipe: GPUComputePipeline;
145 private applyPipe: GPUComputePipeline;
146 private audioPipe: GPUComputePipeline;
147 private extractPipe: GPUComputePipeline;
148 private stepBG: GPUBindGroup[];
149 private rbBG: GPUBindGroup[];
150 private audioBG: GPUBindGroup;
151 private extractBG: GPUBindGroup[];
152 private displayBG: GPUBindGroup[];
154 constructor(device: GPUDevice, plate: Plate, params: SimParams) {
155 this.device = device;
156 this.plate = plate;
157 this.params = { ...params };
158 const g = plate.grid;
159 const m = plate.mat;
160 this.grid = g;
162 // Stable time step, rounded so that an integer number of steps make one audio sample.
163 const hr = thicknessRange(plate);
164 this.omegaMax = maxAngularFrequency(g, m, hr.max, params.inPlaneMass);
165 const dtMax = (CFL * 2) / this.omegaMax;
166 this.stepsPerSample = Math.ceil(1 / (params.fs * dtMax));
167 this.dt = 1 / (params.fs * this.stepsPerSample);
168 this.samplesPerBatch = Math.max(1, Math.round(params.fs / params.fps));
169 this.stepsPerBatch = this.samplesPerBatch * this.stepsPerSample;
170 this.histLen = this.samplesPerBatch + HIST_MARGIN;
172 const n = g.n;
173 const f32 = (size: number, usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST) =>
174 device.createBuffer({ size: Math.max(16, size * 4), usage });
176 this.pos = [f32(4 * n), f32(4 * n)]; // [u | v | w | wLo]
177 this.vel = [f32(3 * n), f32(3 * n)]; // [du | dv | dw]
178 this.hist = f32(this.histLen * n);
179 this.stickBuf = f32(STICK_LEN);
180 this.w0 = f32(n);
181 const shape = Float32Array.from(plate.w0);
182 device.queue.writeBuffer(this.w0, 0, shape);
183 this.w0Max = shape.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0);
184 const weights = nodeWeights(g);
185 this.nw = f32(n);
186 device.queue.writeBuffer(this.nw, 0, weights);
187 const thick = Float32Array.from(plate.h);
188 this.hn = f32(n);
189 device.queue.writeBuffer(this.hn, 0, thick);
190 const packed = new Float32Array(4 * n);
191 for (let k = 0; k < n; k++) { packed[4 * k] = shape[k]; packed[4 * k + 1] = weights[k]; packed[4 * k + 2] = thick[k]; }
192 this.nd = f32(4 * n);
193 device.queue.writeBuffer(this.nd, 0, packed);
194 this.lw = f32(2 * n);
195 this.audioOut = f32(this.samplesPerBatch);
196 this.frameBuf = f32(n);
197 this.displayW = f32(n);
198 this.statsBuf = f32(STATS_LEN);
199 this.stagingSize = (this.samplesPerBatch + n + STATS_LEN + STICK_LEN) * 4;
200 this.staging = [0, 1].map(() =>
201 device.createBuffer({ size: this.stagingSize, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }),
202 );
204 // Mass and moments of inertia of the plate, the contact patch, the mount patch,
205 // and the initial stick state (position 0, moving toward the plate).
206 const { mass, Ix, Iy } = massProperties(plate);
207 this.mass = mass;
208 const cp = contactPatch(params, plate);
209 const mp = mountPatch(plate);
210 const stick = new Float32Array(STICK_LEN);
a46d644Update simulation, step shader, renderer and theory notesJeremy Magland 211 stick[1] = -params.strikeVelocity; stick[9] = -params.strikeVelocity;
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 212 device.queue.writeBuffer(this.stickBuf, 0, stick);
a46d644Update simulation, step shader, renderer and theory notesJeremy Magland 213 this.stick = { x: 0, v: -params.strikeVelocity, force: 0, maxForce: 0, withdrawn: false };
215 const [sx, sy] = strikePoint(params, plate);
216 const pf = new Float32Array(PARAMS_LEN);
217 const pu = new Uint32Array(pf.buffer);
218 pu[0] = g.nx; pu[1] = g.ny; pu[2] = n; pu[3] = this.histLen;
219 pf[4] = g.hx; pf[5] = g.hy; pf[6] = this.dt; pf[7] = m.nu;
220 pf[8] = m.E / (12 * (1 - m.nu * m.nu)); pf[9] = m.E / (1 - m.nu * m.nu); pf[10] = m.E / (2 * (1 + m.nu)); pf[11] = m.rho;
221 pf[12] = mass; pf[13] = Ix; pf[14] = Iy; pf[15] = Ix + Iy;
222 pf[16] = params.fs; pf[17] = sx; pf[18] = sy; pf[19] = params.contactRadius ** 2;
223 pf[20] = cp.mass; pu[21] = cp.nearest; pf[22] = params.contactStiffness; pf[23] = params.contactExponent;
224 pf[24] = params.stickMass; pu[25] = cp.box[0]; pu[26] = cp.box[1]; pu[27] = cp.box[2];
225 pu[28] = cp.box[3]; pf[29] = cp.center[0]; pf[30] = cp.center[1]; pf[31] = params.inPlaneMass;
226 pf[32] = 1 / params.inPlaneMass;
227 if (mp && plate.mount) {
228 pf[33] = plate.mount.x; pf[34] = plate.mount.y; pf[35] = plate.mount.radius ** 2;
229 pf[36] = plate.mount.stiffness / mp.weight; pf[37] = plate.mount.damping / mp.weight;
230 pu[38] = mp.nearest; pu[39] = 1;
231 }
232 this.paramsBuf = device.createBuffer({ size: pf.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
233 device.queue.writeBuffer(this.paramsBuf, 0, pf);
235 // Per-step parameters, one struct per step at a dynamic offset.
236 this.stepStride = Math.max(STEP_LEN * 4, device.limits.minUniformBufferOffsetAlignment);
237 this.stepData = new ArrayBuffer(this.stepStride * this.stepsPerBatch);
238 this.stepBuf = device.createBuffer({ size: this.stepData.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
240 // Pipelines.
241 const shader = (code: string) => device.createShaderModule({ code: commonWgsl + code });
242 const entry = (binding: number, type: GPUBufferBindingType, hasDynamicOffset = false): GPUBindGroupLayoutEntry => ({
243 binding,
244 visibility: GPUShaderStage.COMPUTE,
245 buffer: { type, hasDynamicOffset },
246 });
247 const pipeline = (module: GPUShaderModule, entryPoint: string, layout: GPUBindGroupLayout) =>
248 device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }), compute: { module, entryPoint } });
250 const stepLayout = device.createBindGroupLayout({
251 entries: [
252 entry(0, "uniform"),
253 entry(1, "uniform", true),
254 entry(2, "read-only-storage"),
255 entry(3, "read-only-storage"),
256 entry(4, "storage"),
257 entry(5, "storage"),
258 entry(6, "storage"),
259 entry(7, "storage"),
260 entry(8, "read-only-storage"),
261 ],
262 });
263 this.stepPipe = pipeline(shader(stepWgsl), "main", stepLayout);
265 const rigidModule = shader(rigidWgsl);
266 const rbLayout = device.createBindGroupLayout({
267 entries: [entry(0, "uniform"), entry(1, "storage"), entry(2, "storage"), entry(3, "storage"), entry(8, "read-only-storage")],
268 });
269 this.sumsPipe = pipeline(rigidModule, "sums", rbLayout);
270 this.applyPipe = pipeline(rigidModule, "apply", rbLayout);
272 const audioLayout = device.createBindGroupLayout({
273 entries: [entry(0, "uniform"), entry(1, "uniform", true), entry(2, "read-only-storage"), entry(3, "read-only-storage"), entry(4, "storage")],
274 });
275 this.audioPipe = pipeline(shader(audioWgsl), "main", audioLayout);
277 const extractLayout = device.createBindGroupLayout({
278 entries: [entry(0, "uniform"), entry(1, "read-only-storage"), entry(2, "storage")],
279 });
280 this.extractPipe = pipeline(shader(extractWgsl), "main", extractLayout);
282 // Bind groups. The state is double-buffered: bind group p reads pair p and writes pair 1 - p.
283 const sp = { buffer: this.stepBuf, size: STEP_LEN * 4 };
284 this.stepBG = [0, 1].map((p) =>
285 device.createBindGroup({
286 layout: stepLayout,
287 entries: [
288 { binding: 0, resource: { buffer: this.paramsBuf } },
289 { binding: 1, resource: sp },
290 { binding: 2, resource: { buffer: this.pos[p] } },
291 { binding: 3, resource: { buffer: this.vel[p] } },
292 { binding: 4, resource: { buffer: this.pos[1 - p] } },
293 { binding: 5, resource: { buffer: this.vel[1 - p] } },
294 { binding: 6, resource: { buffer: this.hist } },
295 { binding: 7, resource: { buffer: this.stickBuf } },
296 { binding: 8, resource: { buffer: this.nd } },
297 ],
298 }),
299 );
300 this.rbBG = [0, 1].map((p) =>
301 device.createBindGroup({
302 layout: rbLayout,
303 entries: [
304 { binding: 0, resource: { buffer: this.paramsBuf } },
305 { binding: 1, resource: { buffer: this.pos[p] } },
306 { binding: 2, resource: { buffer: this.vel[p] } },
307 { binding: 3, resource: { buffer: this.statsBuf } },
308 { binding: 8, resource: { buffer: this.nd } },
309 ],
310 }),
311 );
312 this.audioBG = device.createBindGroup({
313 layout: audioLayout,
314 entries: [
315 { binding: 0, resource: { buffer: this.paramsBuf } },
316 { binding: 1, resource: sp },
317 { binding: 2, resource: { buffer: this.hist } },
318 { binding: 3, resource: { buffer: this.lw } },
319 { binding: 4, resource: { buffer: this.audioOut } },
320 ],
321 });
322 const extractBG = (p: number, out: GPUBuffer) =>
323 device.createBindGroup({
324 layout: extractLayout,
325 entries: [
326 { binding: 0, resource: { buffer: this.paramsBuf } },
327 { binding: 1, resource: { buffer: this.pos[p] } },
328 { binding: 2, resource: { buffer: out } },
329 ],
330 });
331 this.extractBG = [0, 1].map((p) => extractBG(p, this.frameBuf));
332 this.displayBG = [0, 1].map((p) => extractBG(p, this.displayW));
334 this.setListener(params.listenerDist, params.listenerAzimuth, params.listenerElevation);
335 // Frame 0: the plate at rest when the stick arrives.
336 this.frames.push(new Float32Array(n));
337 }
339 /** Recomputes the per-node weights and delays of the listener sum. Takes effect on later samples. */
340 setListener(dist: number, azimuthDeg: number, elevationDeg: number): void {
341 const g = this.grid;
6201bf2Show the mount and the listener in the 3D view; lay the plate flatJeremy Magland 342 const L = listenerPoint(dist, azimuthDeg, elevationDeg);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 343 const data = new Float32Array(2 * g.n);
344 let dmin = Infinity;
345 for (let j = 0; j < g.ny; j++) {
346 for (let i = 0; i < g.nx; i++) {
347 const k = j * g.nx + i;
348 const rx = L[0] - nodeX(g, i), ry = L[1] - nodeY(g, j), rz = L[2];
349 const r = Math.sqrt(rx * rx + ry * ry + rz * rz);
350 const dA = g.hx * g.hy * nodeWeight(g, i, j);
351 data[2 * k] = (RHO_AIR * dA) / (2 * Math.PI * r);
352 data[2 * k + 1] = (r / SPEED_OF_SOUND) * this.params.fs;
353 dmin = Math.min(dmin, data[2 * k + 1]);
354 }
355 }
356 // Only the spread of delays across the plate is kept; the common delay is dropped.
357 this.delayClamped = false;
358 for (let k = 0; k < g.n; k++) {
359 let d = data[2 * k + 1] - dmin;
360 if (d > HIST_MARGIN - 3) {
361 d = HIST_MARGIN - 3;
362 this.delayClamped = true;
363 }
364 data[2 * k + 1] = d;
365 }
366 this.device.queue.writeBuffer(this.lw, 0, data);
367 this.params.listenerDist = dist;
368 this.params.listenerAzimuth = azimuthDeg;
369 this.params.listenerElevation = elevationDeg;
370 }
372 setDuration(duration: number): void {
373 this.params.duration = duration;
374 if (this.canContinue()) this.phase = "recording";
375 }
377 /** True when a finished or stopped run can be extended. */
378 canContinue(): boolean {
379 return this.phase === "done" && this.samples < this.targetSamples();
380 }
382 /** Ends the run; the state, audio, and movie are kept. */
383 stop(): void {
384 this.phase = "done";
385 }
387 targetSamples(): number {
388 return Math.round(this.params.duration * this.params.fs);
389 }
391 status(): SimStatus {
392 // Report the time of the state that has been read back, not the one enqueued.
393 const ahead = this.inFlight ? this.inFlight.steps * this.dt : 0;
394 return {
395 phase: this.phase,
396 t: this.totalSteps * this.dt - ahead,
397 totalSteps: this.totalSteps,
398 ke: this.ke,
399 wmax: this.wmax,
400 samples: this.samples,
401 frames: this.frames.length,
402 peakPressure: this.peakPressure,
403 delayClamped: this.delayClamped,
404 stick: this.stick,
405 };
406 }
408 /** Enqueues an extraction of the current w field into `displayW` (for the live view). */
409 encodeDisplay(encoder: GPUCommandEncoder): void {
410 const pass = encoder.beginComputePass();
411 pass.setPipeline(this.extractPipe);
412 pass.setBindGroup(0, this.displayBG[this.cur]);
413 pass.dispatchWorkgroups(Math.ceil(this.grid.n / 256));
414 pass.end();
415 }
417 private stepWorkgroups(): [number, number] {
418 return [Math.ceil(this.grid.nx / TILE), Math.ceil(this.grid.ny / TILE)];
419 }
421 /** The recorded listener pressure so far [Pa]. */
422 recordedAudio(): Float32Array {
423 return this.audio.subarray(0, this.samples);
424 }
426 private appendAudio(raw: Float32Array): void {
427 const need = this.samples + raw.length;
428 if (this.audio.length < need) {
429 const a = new Float32Array(Math.max(need, 2 * this.audio.length, this.params.fs));
430 a.set(this.audio.subarray(0, this.samples));
431 this.audio = a;
432 }
433 this.audio.set(raw, this.samples);
434 for (let k = 0; k < raw.length; k++) this.peakPressure = Math.max(this.peakPressure, Math.abs(raw[k]));
435 this.samples = need;
436 }
438 /** Writes the per-step uniform for step s of a batch. */
439 private writeStepParams(s: number, fields: { flags: number; slot: number; outIdx: number }): void {
440 const o = (s * this.stepStride) / 4;
441 const u32 = new Uint32Array(this.stepData);
442 const i32 = new Int32Array(this.stepData);
443 const f32 = new Float32Array(this.stepData);
444 const { sigma0, sigma1, sigmaIn } = this.plate.loss;
445 u32[o] = this.totalSteps + s;
446 u32[o + 1] = fields.flags;
447 i32[o + 2] = fields.slot;
448 i32[o + 3] = fields.outIdx;
449 f32[o + 4] = sigma0;
450 f32[o + 5] = sigma1;
451 f32[o + 6] = sigmaIn;
452 u32[o + 7] = (this.totalSteps + s) & 1;
453 }
455 /**
456 * Runs one batch (one movie frame of time). Resolves when its results have
457 * been read back. The following batch is encoded before this one is awaited.
458 */
459 async runBatch(): Promise<void> {
460 if (this.phase === "done") return;
461 const cur = this.inFlight ?? this.encodeBatch();
462 this.inFlight = null;
463 if (!cur) {
464 this.phase = "done";
465 return;
466 }
467 if (this.stepsLeft() > 0) this.inFlight = this.encodeBatch();
468 await this.collect(cur);
469 }
471 private stepsLeft(): number {
472 return this.targetSamples() * this.stepsPerSample - this.totalSteps;
473 }
475 /** Encodes and submits one batch; null when no steps remain. */
476 private encodeBatch(): Pending | null {
477 const p = this.params;
478 const steps = Math.min(this.stepsPerBatch, Math.max(0, this.stepsLeft()));
479 if (steps === 0) return null;
481 // Per-step parameters: the drumstick and the audio bookkeeping.
482 const audioSteps: number[] = [];
483 for (let s = 0; s < steps; s++) {
484 const local = this.totalSteps + s;
485 let slot = -1, outIdx = -1;
486 if (local % this.stepsPerSample === 0) {
487 slot = (local / this.stepsPerSample) % this.histLen;
488 outIdx = audioSteps.length;
489 audioSteps.push(s);
490 }
491 this.writeStepParams(s, { flags: 1, slot, outIdx });
492 }
493 this.device.queue.writeBuffer(this.stepBuf, 0, this.stepData, 0, steps * this.stepStride);
495 // Encode the batch in chunks so that the GPU starts while JS is still encoding.
496 const CHUNK = 1024;
497 let enc = this.device.createCommandEncoder();
498 let pass = enc.beginComputePass();
499 const [wgX, wgY] = this.stepWorkgroups();
500 const wgN = Math.ceil(this.grid.n / 256);
501 let cur = this.cur;
502 // The projection is optional and only meaningful for a free plate; the
503 // sums still run once per batch for the statistics.
504 const project = p.rigidProjection && !this.plate.mount;
505 let stepBound = false;
506 for (let s = 0; s < steps; s++) {
507 if (s > 0 && s % CHUNK === 0) {
508 pass.end();
509 this.device.queue.submit([enc.finish()]);
510 enc = this.device.createCommandEncoder();
511 pass = enc.beginComputePass();
512 stepBound = false;
513 }
514 if (!stepBound) {
515 pass.setPipeline(this.stepPipe);
516 stepBound = true;
517 }
518 pass.setBindGroup(0, this.stepBG[cur], [s * this.stepStride]);
519 pass.dispatchWorkgroups(wgX, wgY);
520 cur = 1 - cur;
521 const global = this.totalSteps + s + 1;
522 if ((project && global % RB_INTERVAL === 0) || s === steps - 1) {
523 pass.setPipeline(this.sumsPipe);
524 pass.setBindGroup(0, this.rbBG[cur]);
525 pass.dispatchWorkgroups(1);
526 if (project) {
527 pass.setPipeline(this.applyPipe);
528 pass.setBindGroup(0, this.rbBG[cur]);
529 pass.dispatchWorkgroups(wgN);
530 }
531 stepBound = false;
532 }
533 }
534 // The listener sum for every audio sample of the batch, one workgroup each;
535 // the ring buffer is long enough that no slot of the batch has been reused.
536 const nAudio = audioSteps.length;
537 if (nAudio > 0) {
538 pass.setPipeline(this.audioPipe);
539 pass.setBindGroup(0, this.audioBG, [audioSteps[0] * this.stepStride]);
540 pass.dispatchWorkgroups(nAudio);
541 }
542 pass.setPipeline(this.extractPipe);
543 pass.setBindGroup(0, this.extractBG[cur]);
544 pass.dispatchWorkgroups(wgN);
545 pass.end();
546 const staging = this.staging[this.stagingIdx];
547 this.stagingIdx = 1 - this.stagingIdx;
548 const n = this.grid.n;
549 enc.copyBufferToBuffer(this.audioOut, 0, staging, 0, Math.max(4, nAudio * 4));
550 enc.copyBufferToBuffer(this.frameBuf, 0, staging, this.samplesPerBatch * 4, n * 4);
551 enc.copyBufferToBuffer(this.statsBuf, 0, staging, (this.samplesPerBatch + n) * 4, STATS_LEN * 4);
552 enc.copyBufferToBuffer(this.stickBuf, 0, staging, (this.samplesPerBatch + n + STATS_LEN) * 4, STICK_LEN * 4);
553 this.device.queue.submit([enc.finish()]);
554 this.cur = cur;
555 this.totalSteps += steps;
556 return { staging, steps, nAudio, stickParity: this.totalSteps & 1 };
557 }
559 /** Reads back a batch and applies its results. */
560 private async collect(b: Pending): Promise<void> {
561 const n = this.grid.n;
562 await b.staging.mapAsync(GPUMapMode.READ);
563 const view = new Float32Array(b.staging.getMappedRange());
564 const stats = view.subarray(this.samplesPerBatch + n, this.samplesPerBatch + n + STATS_LEN);
565 this.ke = stats[12];
566 this.wmax = stats[13];
567 // The slot written by the last step is the one the next step reads.
a46d644Update simulation, step shader, renderer and theory notesJeremy Magland 568 const base = this.samplesPerBatch + n + STATS_LEN + 8 * b.stickParity;
569 this.stick = { x: view[base], v: view[base + 1], force: view[base + 2], maxForce: view[base + 3], withdrawn: view[base + 5] !== 0 };
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 570 this.appendAudio(view.slice(0, b.nAudio));
571 this.frames.push(view.slice(this.samplesPerBatch, this.samplesPerBatch + n));
572 b.staging.unmap();
573 if (this.samples >= this.targetSamples()) this.phase = "done";
574 }
576 // ---------- Testing support ----------
578 /**
579 * Advances the state by exactly one free step (no drumstick, no rigid-body
580 * projection, no audio) with the plate's loss parameters and its mount.
581 */
582 async stepOnce(): Promise<void> {
583 this.writeStepParams(0, { flags: 0, slot: -1, outIdx: -1 });
584 this.device.queue.writeBuffer(this.stepBuf, 0, this.stepData, 0, this.stepStride);
585 const enc = this.device.createCommandEncoder();
586 const pass = enc.beginComputePass();
587 pass.setPipeline(this.stepPipe);
588 pass.setBindGroup(0, this.stepBG[this.cur], [0]);
589 pass.dispatchWorkgroups(...this.stepWorkgroups());
590 pass.end();
591 this.device.queue.submit([enc.finish()]);
592 this.cur = 1 - this.cur;
593 this.totalSteps++;
594 await this.device.queue.onSubmittedWorkDone();
595 }
597 /** Overwrites the state: pos as [u | v | w | wLo], vel as [du | dv | dw]. */
598 writeState(pos: Float32Array<ArrayBuffer>, vel: Float32Array<ArrayBuffer>): void {
599 const n = this.grid.n;
600 if (pos.length !== 4 * n || vel.length !== 3 * n) throw new Error("state arrays have the wrong length");
601 this.device.queue.writeBuffer(this.pos[this.cur], 0, pos);
602 this.device.queue.writeBuffer(this.vel[this.cur], 0, vel);
603 }
605 /** Reads back the full state as [u | v | w | wLo] and [du | dv | dw]. */
606 async readState(): Promise<{ pos: Float32Array; vel: Float32Array }> {
607 const n = this.grid.n;
608 const stg = this.device.createBuffer({ size: 7 * n * 4, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
609 const enc = this.device.createCommandEncoder();
610 enc.copyBufferToBuffer(this.pos[this.cur], 0, stg, 0, 4 * n * 4);
611 enc.copyBufferToBuffer(this.vel[this.cur], 0, stg, 4 * n * 4, 3 * n * 4);
612 this.device.queue.submit([enc.finish()]);
613 await stg.mapAsync(GPUMapMode.READ);
614 const all = new Float32Array(stg.getMappedRange()).slice();
615 stg.unmap();
616 stg.destroy();
617 return { pos: all.subarray(0, 4 * n), vel: all.subarray(4 * n) };
618 }
620 destroy(): void {
621 this.inFlight = null;
622 for (const b of [...this.pos, ...this.vel, this.hist, this.stickBuf, this.nd, this.w0, this.nw, this.hn, this.lw, this.audioOut, this.frameBuf, this.displayW, this.statsBuf, this.paramsBuf, this.stepBuf, ...this.staging]) b.destroy();
623 }
625 get fs(): number {
626 return this.params.fs;
627 }
628 get fps(): number {
629 return this.params.fps;
630 }
631 get simParams(): SimParams {
632 return this.params;
633 }
↑↓ move↵openescclose