videoskilletdecisions Open the app ↗

0007 — The FIR passes are not ALU-bound, so ablate before optimizing

Status: accepted, 2026-08-17.

Context#

Six passes in the signal path are FIR filters 33 to 55 taps wide, and reading one is an invitation to make its arithmetic cheaper. The inner loop is a multiply-accumulate over a kernel, sometimes with a phasor walked alongside it — every instinct a graphics programmer has says the wins are in there.

They are not, on this hardware. Three attempts, all built, all measured, all reverted for no separable difference:

What survives is the verdict and not the arms. All three were measured in worktrees that never landed, so there are no per-arm numbers in this repo to reproduce — which is a defect in the record, and the reason to re-derive an ablation upper bound rather than cite this paragraph as a measurement.

What is numbered points the same way, because three optimizations to these same passes landed on 2026-08-08 and none of them made arithmetic cheaper (dev box's WX 3200, Firefox Nightly / Linux, best-of interleaved per ../DEVELOPMENT.md › Measuring performance):

The reading that fits all of it is that these passes have idle issue slots. Arithmetic saved inside the tap loop rides in them and never reaches the frame time; work removed from the loop entirely — a fetch, a transcendental, a dispatch — does.

The same shape caught a fourth attempt before it was written, which is the one with numbers preserved. The Engine constructor's blocking createComputePipeline calls look like an obvious createComputePipelineAsync job; timed in place first, at 22 pipelines:

PLBUILD n=22 sync=9.0ms syncWarm=2.0ms asyncParallel=396.0ms

9 ms is the entire upper bound, so the refactor could not have been worth it whatever the async path did — and the async path was 44× worse anyway. One browser run, no code written.

Decision#

Measure an ablation upper bound before building an optimization here. Delete the work, run the frame without it, and read the delta: that number is the ceiling on any optimization of it, and it is routinely smaller than the code suggests.

Treat arithmetic inside the FIR tap loops as free until an ablation says otherwise. The levers that have actually paid are all of the form do less, not do it cheaper: don't dispatch the pass (when() predicates), don't fetch the sample (tap-count tiering), don't recompute what is constant across a workgroup or across the build (hoisting, tabulating).

Consequences#

The techniques this rule produced, and what each of them measured, are in ../OPTIMIZATIONS.md.

Addendum, 2026-08-21 — the arms can be recorded now, and one of them moved#

scripts/gpuprof times every pass on the GPU's own counter, headless, under Deno's wgpu on the same card (DEVELOPMENT.md › Measuring performance). It resolves a tenth of a millisecond per pass, where the batch harness measured whole frames through a bimodal ~0.8 ms of neighbour noise. Re-measured with it, the third arm above was never flat:

crt_face, stock, WX 3200            grain hashed per frame   grain baked once
  pass GPU time                            0.568 ms             0.453 ms
  CRT face vs previous shader                  —                 max 0/255

The verdict holds where it was drawn. The two FIR arms were arithmetic inside tap loops that have idle issue slots; crt_face is a per-pixel pass with no such slack, and sixteen hashes a pixel were its cost. "Ablate first" is unchanged — what changed is that the ablation is now a two-second command with a per-pass answer, and it should be the first thing run, not the last.

One more number from the same afternoon belongs here because it is the rule in the other direction. sync.wgsl's single lane was staged into workgroup memory on the theory that its 525-line walk was paying a global load per line: 0.174 → 0.166 ms, and a prefetch did nothing more. The pass is bound by one lane's issue latency on a dependent recurrence, and memory was never it.