three.js icon indicating copy to clipboard operation
three.js copied to clipboard

WebGPURenderer: `recycleBuffer()` - reduce buffer creation overhead.

Open aardgoose opened this issue 1 year ago • 2 comments

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.

aardgoose avatar Sep 06 '24 11:09 aardgoose

📦 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

github-actions[bot] avatar Sep 06 '24 11:09 github-actions[bot]

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

    } );

}

gkjohnson avatar Sep 08 '24 13:09 gkjohnson