WebGPURenderer: `recycleBuffer()` - reduce buffer creation overhead.
Related issue: #XXXX
As noted, the WebGPURenderer.readRenderTargetPixelsAsync() does not take a user provided buffer, but instead created a new buffer for each call. As noted this is a consequence of the WebGPU API.
https://github.com/mrdoob/three.js/pull/29320#discussion_r1743663932
This PR attempts to provide a solution to this issue by providing a simple mechanism for the user to return used buffers ( and the related WebGPUbuffer object back to the renderer.
This is demonstrated in the webgpu_multiple_rendertargets_readback example.
Brief testing has demonstrated a reduction in CPU usage.
📦 Bundle size
Full ESM build, minified and gzipped.
| Before | After | Diff | |
|---|---|---|---|
| WebGL | 685.18 169.62 |
685.18 169.62 |
+0 B +0 B |
| WebGPU | 825.96 221.44 |
826.82 221.66 |
+864 B +225 B |
| WebGPU Nodes | 825.54 221.34 |
826.4 221.57 |
+864 B +225 B |
🌳 Bundle size after tree-shaking
Minimal build including a renderer, camera, empty scene, and dependencies.
| Before | After | Diff | |
|---|---|---|---|
| WebGL | 461.96 111.46 |
461.96 111.46 |
+0 B +0 B |
| WebGPU | 525.27 141.52 |
526.13 141.76 |
+864 B +242 B |
| WebGPU Nodes | 481.93 131.34 |
482.79 131.58 |
+864 B +239 B |
I see what the complexity is with providing a raw buffer to the read pixels function. I'm wondering if it makes sense to have a three.js class that wraps the resulting buffer and provides the ability to release it whenever the user is finished so this caching doesn't have to happen internally. Something like so:
class WebGPUDataBuffer {
// handle to the underlying WebGPU resource - can be private
bufferHandle: GPUBuffer;
// the buffer of data - can be readonly
buffer: TypedArray;
// flag indicating whether the buffer is mapped and available to read
mapped: Boolean;
// unmaps the data so it can be written to by the GPU
release() : void;
// destroys the GPU resource and fully releases any memory associated with it
destroy() : void
}
Then the read pixels function can be used with the ability to reuse that buffer by passing it in as an argument like before. If it's not provided then a new resource is created. You can reuse the buffer over multiple reads like so:
let gpuData = null;
// ...
if ( ! isReading ) {
renderer
.readRenderTargetPixelsAsync( renderTarget, x, y, width, height, gpuData, /* index, faceIndex */ )
.then( result => {
gpuData = result;
// ... do something with the data
} );
}