Task 4 of 5
Nothing in this scene moves. So every frame you trace is another independent estimate of the same integral — and the way to spend them is not to throw the last one away, but to keep a running average of all of them in a buffer that survives from frame to frame. Frame 48 is not 48 times more work per frame; it is the same work, added to what you already had.
The average has to be exact, not exponential. Video Filters blends each new
frame in with a fixed alpha because its scene is moving and old frames go
stale; here nothing goes stale, so every frame gets an equal vote:
next = (previous * n + sample) / (n + 1)
And the buffer lives on the card. This is the ping-pong from Pipelines &
Textures doing real work: the kernel reads the very texture it is about to replace, so
immutable: true is what makes the loop legal — each call renders into a
fresh texture and last frame's numbers stay safe to read while this frame's are
being written. Leave it off and gpu.js stops you with the reason. (Auto runs this one on
WebGL throughout, deliberately: a graphical kernel can never be a WebGPU kernel, so
letting the maths upgrade would hand the buffer across backends once a frame — measured
here at 71 ms against 25 ms for the whole 48-frame loop.)
Then render every pass. Consecutive render() calls collapse into a
scrubber, so you get to drag the noise away by hand.
accumulate a running mean, and make the
feedback loop legal, so that 48 traced frames melt into one clean image.(old * n + now) / (n + 1) — an exact mean of every frame so far, not a blendimmutable: true to accumulate (keep pipeline: true)render(paint.canvas) inside the loop, once per frame, for the scrubbergpu.js refuses the second pass of the loop: the kernel's input is its own
output storage. WebGL names the fix in the message; WebGPU only tells you the buffer
belongs to the kernel. Either way immutable: true is it — a fresh output
texture per call, so reading last frame's is safe.
n is how many frames are already in the buffer, which is
the loop counter. At n = 0 the formula returns the new sample untouched,
which is exactly right: the zero buffer must not get a vote.
In the kernel body:
return (old * n + now) / (n + 1);
and in its settings, beside pipeline: true:
immutable: true,immutable: true is
gpu.js doing that bookkeeping for you. Real-time renderers do the same trick within a
moving scene by reprojecting the history with motion vectors — that is what the
"TA" in TAA stands for.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.