/ proof-of-concept / steelsim21
Sign in
proof-of-concept / steelsim21
Render lazily: no GPU or spectrogram work when nothing changed
The view and the spectrogram were redrawn every animation frame even while paused or idle. The spectrogram now caches its image and redraws only when data, options, size, or the playhead change; the displacement view renders only when the simulation advanced, the movie position or camera moved, or a view setting changed.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit e6ed3ad3541b parent 8dcc11f Browse files
2 changed files+67−12
src/audio/spectrogram.tsmodified+41−8View file
@@ -81,11 +81,30 @@ export class Spectrogram {
8181 return this.columns.length;
8282 }
8383
84- /** Draws the spectrogram; `duration` [s] fixes the time axis, `playhead` [s] draws a cursor. */
84+ private imageKey = "";
85+ private image: ImageData | null = null;
86+ private drawKey = "";
87+
88+ /** Forces the next draw to recompute everything. */
89+ invalidate(): void {
90+ this.imageKey = "";
91+ this.drawKey = "";
92+ }
93+
94+ /**
95+ * Draws the spectrogram; `duration` [s] fixes the time axis, `playhead` [s] draws a cursor.
96+ * The plot image is cached and only recomputed when the data, options, or size change,
97+ * and nothing is drawn at all when the previous frame is still current.
98+ */
8599 draw(canvas: HTMLCanvasElement, fs: number, duration: number, playhead: number | null): void {
86100 const dpr = window.devicePixelRatio || 1;
87101 const W = Math.max(1, Math.floor(canvas.clientWidth * dpr));
88102 const H = Math.max(1, Math.floor(canvas.clientHeight * dpr));
103+ const o = this.opts;
104+ const imageKey = `${W}|${H}|${this.columns.length}|${duration}|${fs}|${o.windowSize}|${o.hop}|${o.fmax}|${o.dbFloor}|${o.logFrequency}`;
105+ const drawKey = `${imageKey}|${playhead === null ? "-" : playhead.toFixed(4)}`;
106+ if (drawKey === this.drawKey) return;
107+ this.drawKey = drawKey;
89108 if (canvas.width !== W || canvas.height !== H) {
90109 canvas.width = W;
91110 canvas.height = H;
@@ -95,6 +114,23 @@ export class Spectrogram {
95114 const pw = Math.max(1, W - left - right), ph = Math.max(1, H - top - bottom);
96115 ctx.fillStyle = "#1c1e22";
97116 ctx.fillRect(0, 0, W, H);
117+ const { fmax, logFrequency } = this.opts;
118+ const fmin = logFrequency ? 20 : 0;
119+ const logMin = Math.log(fmin || 1), logMax = Math.log(fmax);
120+ if (imageKey !== this.imageKey || !this.image) {
121+ this.image = this.renderImage(ctx, pw, ph, fs, duration);
122+ this.imageKey = imageKey;
123+ }
124+ ctx.putImageData(this.image, left, top);
125+ this.drawAxes(ctx, dpr, left, top, pw, ph, fmin, fmax, logMin, logMax, logFrequency, duration);
126+ if (playhead !== null && playhead >= 0 && playhead <= duration) {
127+ const x = left + (playhead / duration) * pw;
128+ ctx.fillStyle = "rgba(255,255,255,0.8)";
129+ ctx.fillRect(x, top, Math.max(1, dpr), ph);
130+ }
131+ }
132+
133+ private renderImage(ctx: CanvasRenderingContext2D, pw: number, ph: number, fs: number, duration: number): ImageData {
98134 const { windowSize, hop, fmax, dbFloor, logFrequency } = this.opts;
99135 const binHz = fs / windowSize;
100136 const fmin = logFrequency ? 20 : 0;
@@ -122,8 +158,10 @@ export class Spectrogram {
122158 data[o] = r; data[o + 1] = g; data[o + 2] = b; data[o + 3] = 255;
123159 }
124160 }
125- ctx.putImageData(img, left, top);
126- // Axes.
161+ return img;
162+ }
163+
164+ private drawAxes(ctx: CanvasRenderingContext2D, dpr: number, left: number, top: number, pw: number, ph: number, fmin: number, fmax: number, logMin: number, logMax: number, logFrequency: boolean, duration: number): void {
127165 ctx.fillStyle = "#c8cbd0";
128166 ctx.font = `${11 * dpr}px system-ui, sans-serif`;
129167 ctx.textAlign = "right";
@@ -148,11 +186,6 @@ export class Spectrogram {
148186 ctx.textBaseline = "middle";
149187 ctx.fillText("Hz", 0, 0);
150188 ctx.restore();
151- if (playhead !== null && playhead >= 0 && playhead <= duration) {
152- const x = left + (playhead / duration) * pw;
153- ctx.fillStyle = "rgba(255,255,255,0.8)";
154- ctx.fillRect(x, top, Math.max(1, dpr), ph);
155- }
156189 }
157190 }
158191
src/main.tsmodified+26−4View file
@@ -451,6 +451,7 @@ function frame(): void {
451451 }
452452 }
453453
454+let lastRenderKey = "";
454455 function frameBody(): void {
455456 if (!device || !sim || !renderer) return;
456457 const dev = device;
@@ -463,8 +464,27 @@ function frameBody(): void {
463464 scrubFrame = Math.min(sim.frames.length - 1, Math.floor(pos * sim.fps));
464465 scrub.value = String(scrubFrame);
465466 }
466- const enc = dev.createCommandEncoder();
467467 const st = sim.status();
468+ // Render only when something visible changed, so an idle or paused page does no work.
469+ const canvas = $<HTMLCanvasElement>("view");
470+ const cam = renderer.camera;
471+ const renderKey = [
472+ renderer.mode, renderer.exaggeration, renderer.colorRange, renderer.showShape,
473+ cam.yaw.toFixed(4), cam.pitch.toFixed(4), cam.dist.toFixed(4),
474+ live, scrubFrame, sim.frames.length, st.totalSteps, st.phase,
475+ canvas.clientWidth, canvas.clientHeight, window.devicePixelRatio,
476+ ].join("|");
477+ if (renderKey !== lastRenderKey) {
478+ lastRenderKey = renderKey;
479+ renderView(dev, st);
480+ }
481+ spec.draw($("spec"), sim.fs, Math.max(duration, sim.samples / sim.fs), pos ?? (live ? null : scrubFrame / sim.fps));
482+ updateInfo(st);
483+}
484+
485+function renderView(dev: GPUDevice, st: ReturnType<Simulation["status"]>): void {
486+ if (!sim || !renderer) return;
487+ const enc = dev.createCommandEncoder();
468488 if (live || sim.frames.length === 0) {
469489 sim.encodeDisplay(enc);
470490 renderer.render(enc, sim.displayW, st.wmax);
@@ -484,8 +504,6 @@ function frameBody(): void {
484504 dev.pushErrorScope("validation");
485505 dev.queue.submit([enc.finish()]);
486506 void dev.popErrorScope().then((e) => { if (e && !frameErrorShown) { frameErrorShown = true; console.error("frame validation error:", e.message); } });
487- spec.draw($("spec"), sim.fs, Math.max(duration, sim.samples / sim.fs), pos ?? (live ? null : scrubFrame / sim.fps));
488- updateInfo(st);
489507 }
490508
491509 function updateInfo(st: ReturnType<Simulation["status"]>): void {
@@ -507,8 +525,12 @@ function updateInfo(st: ReturnType<Simulation["status"]>): void {
507525 let html = rows.map(([a, b]) => `<div class="row"><span>${a}</span><span>${b}</span></div>`).join("");
508526 if (st.delayClamped) html += `<div class="warn">Listener is so close that some propagation delays were clamped.</div>`;
509527 if (!isFinite(st.wmax) || st.wmax > 1) html += `<div class="err">The solution has diverged. Reduce the time step by increasing the grid spacing, or reduce the push.</div>`;
510- $("info").innerHTML = html;
528+ if (html !== lastInfoHtml) {
529+ lastInfoHtml = html;
530+ $("info").innerHTML = html;
531+ }
511532 }
533+let lastInfoHtml = "";
512534
513535 // ---------- Init ----------
514536
↑↓ move↵openescclose