WebGPU frame time: the uploads you did not ask for

· 2 min read · three.js, webgpu, tsl, performance, game-dev

A rainy neon street in a racing game, a scene where uniform and buffer uploads cost more than the shading

Most of the wins in Threejs-Punk's frame budget were not "fewer effects". They were work the engine did without being asked: uploads per draw, render targets rendered once per reader, and a second scene render nobody needed.

The numbers #

Same harness, before and after, on an M2 Max in headless Chrome:

Before After
2880x1620, driving 7.9 ms 5.3 ms
3840x2160, driving 12.4 ms 7.9 ms
CPU per frame 7.4 ms 2.4 ms
queue.writeBuffer per frame 2,315 calls, 4.1 MB about 240 calls, about 40 KB
Draw calls about 475 about 270

Three kinds of waste #

Instance matrices. An InstancedMesh with up to 1,024 instances keeps its matrices in a uniform buffer that is re-uploaded in full on every draw of every pass (view, mirror, shadow, rain height), even when nothing moved. Wrapping it so the matrices live in a storage attribute uploads only on needsUpdate, with update ranges. This works on the WebGPU backend only.

DynamicDrawUsage. three re-uploads a dynamic attribute on every use. Attributes that already flag needsUpdate must stay static usage.

The shared render group. Camera, lights and shadow uniforms live in one buffer shared by every material with the same uniform set. A uniform created inside a TSL Fn body is a new node for every material using that Fn, and that one private node splits the group per material. three's PCF-soft shadow filter did exactly that, giving around 390 render-group buffers rewritten every frame. The fix is the same filter with the uniform created once. The rule: create render-group uniforms outside Fn bodies.

Two more #

  • Render targets that render once. An rtt() node re-renders for every pass that reads it. When more than one pass reads it, set its update type to once per frame and sample the texture.
  • Images, not video. Six VideoTexture ad screens became posters on one GPU-compressed atlas with a slideshow in the shader.

How to find them #

Switching one subsystem off and comparing frame time beats reading pass timings. The measuring method is in the GPU timing post. The game is on its project page.

Related projects