Task 4 of 5

The Buffer That Outlives the Frame

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.

the new frame gets 1/(n+1) of the vote; everything before it keeps the rest
Goal: make accumulate a running mean, and make the feedback loop legal, so that 48 traced frames melt into one clean image.

Requirements

Hint 1 — run the starter and read the error

gpu.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.

Hint 2 — where the n comes from

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.

Hint 3 — the two edits

In the kernel body:

return (old * n + now) / (n + 1);

and in its settings, beside pipeline: true:

immutable: true,

Same idea elsewhere

Every "progressive" viewport you have ever watched resolve — Cycles, Arnold's IPR, a Substance or Unreal path-traced preview — is this loop: a persistent accumulation buffer plus a sample count. On the GPU it is always two textures being ping-ponged, because a shader may not read the surface it writes; WebGPU makes you bind two storage textures and swap them, CUDA makes you swap two device pointers, and 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.

All tasks in Progressive Path Tracing: Noise Melting Into an Image

  1. Dice Every Thread Can Roll
  2. Where the Ray Hits
  3. One Path Per Pixel, and It Looks Terrible
  4. The Buffer That Outlives the Frame
  5. The Noise Falls Like 1/√n

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.