diff --git a/examples/webgpu_compute_reduce.html b/examples/webgpu_compute_reduce.html
index c8a962b5b56b3b..f1f867914b842e 100644
--- a/examples/webgpu_compute_reduce.html
+++ b/examples/webgpu_compute_reduce.html
@@ -963,9 +963,9 @@
Subgroup Reduction Explanation
functionObj[ logFunctionName ] = async() => {
const selectedBuffer = buffers[ unifiedEffectController.loggedBuffer ];
- const readbackBuffer = new THREE.ReadbackBuffer( selectedBuffer.value );
+ const readbackBuffer = new THREE.ReadbackBuffer( selectedBuffer.value.array.length );
- const result = new Uint32Array( await renderer.getArrayBufferAsync( readbackBuffer ) );
+ const result = new Uint32Array( await renderer.getArrayBufferAsync( selectedBuffer.value, readbackBuffer ) );
console.log( result );
diff --git a/src/renderers/common/Info.js b/src/renderers/common/Info.js
index 9152e540c05900..66e09662b998bf 100644
--- a/src/renderers/common/Info.js
+++ b/src/renderers/common/Info.js
@@ -340,7 +340,7 @@ class Info {
*/
createReadbackBuffer( readbackBuffer ) {
- const size = this._getAttributeMemorySize( readbackBuffer.attribute );
+ const size = readbackBuffer.size;
this.memoryMap.set( readbackBuffer, { size, type: 'readbackBuffers' } );
this.memory.readbackBuffers ++;
diff --git a/src/renderers/common/ReadbackBuffer.js b/src/renderers/common/ReadbackBuffer.js
index 239003e7fa0c7f..7c4a27c7cd4e54 100644
--- a/src/renderers/common/ReadbackBuffer.js
+++ b/src/renderers/common/ReadbackBuffer.js
@@ -1,5 +1,7 @@
import { EventDispatcher } from '../../core/EventDispatcher.js';
+let _id = 0;
+
/**
* A readback buffer is used to transfer data from the GPU to the CPU.
* It is primarily used to read back compute shader results.
@@ -11,18 +13,32 @@ class ReadbackBuffer extends EventDispatcher {
/**
* Constructs a new readback buffer.
*
- * @param {BufferAttribute} attribute - The buffer attribute.
+ * @param {BufferAttribute} size - The buffer attribute.
*/
- constructor( attribute ) {
+ constructor( size ) {
super();
/**
- * The buffer attribute.
+ * The size of the buffer in bytes.
+ *
+ * @type {number}
+ */
+ this.size = size;
+
+ /**
+ * A unique identifier for this readback buffer.
+ *
+ * @type {number}
+ */
+ this.id = _id ++;
+
+ /**
+ * A name for this readback buffer.
*
- * @type {BufferAttribute}
+ * @type {string}
*/
- this.attribute = attribute;
+ this.name = '';
/**
* This flag can be used for type testing.
@@ -35,19 +51,6 @@ class ReadbackBuffer extends EventDispatcher {
}
- /**
- * Releases the mapped buffer data so the GPU buffer can be
- * used by the GPU again.
- *
- * Note: Any `ArrayBuffer` data associated with this readback buffer
- * are removed and no longer accessible after calling this method.
- */
- release() {
-
- this.dispatchEvent( { type: 'release' } );
-
- }
-
/**
* Frees internal resources.
*/
diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js
index d4716b6ffba054..4ed0b0fd26087f 100644
--- a/src/renderers/common/Renderer.js
+++ b/src/renderers/common/Renderer.js
@@ -1919,23 +1919,26 @@ class Renderer {
* from the GPU to the CPU in context of compute shaders.
*
* @async
- * @param {StorageBufferAttribute|ReadbackBuffer} buffer - The storage buffer attribute.
+ * @param {StorageBufferAttribute} attribute - The storage buffer attribute.
+ * @param {ReadbackBuffer} [readbackBuffer=null] - The readback buffer.
+ * @param {number} [offset=0] - The offset in bytes.
+ * @param {number} [size=null] - The size in bytes.
* @return {Promise} A promise that resolves with the buffer data when the data are ready.
*/
- async getArrayBufferAsync( buffer ) {
+ async getArrayBufferAsync( attribute, readbackBuffer = null, offset = 0, size = null ) {
- let readbackBuffer = buffer;
+ if ( readbackBuffer === null ) {
- if ( readbackBuffer.isReadbackBuffer !== true ) {
-
- const attribute = buffer;
const attributeData = this.backend.get( attribute );
readbackBuffer = attributeData.readbackBuffer;
if ( readbackBuffer === undefined ) {
- readbackBuffer = new ReadbackBuffer( attribute );
+ const byteLength = attribute.array.byteLength;
+
+ readbackBuffer = new ReadbackBuffer( byteLength );
+ readbackBuffer.name = attribute.name;
const dispose = () => {
@@ -1971,9 +1974,45 @@ class Renderer {
}
- readbackBuffer.release();
+ if ( size === null ) {
+
+ size = attribute.array.byteLength - offset;
+
+ }
+
+ const readbackBufferData = this.backend.get( readbackBuffer );
+
+ if ( readbackBufferData.running ) {
+
+ warn( 'Renderer: "ReadbackBuffer" is already in use. To obtain multiple acquisitions in parallel of the same attribute, use instances of "ReadbackBuffer".' );
+
+ return new ArrayBuffer();
+
+ }
+
+ readbackBufferData.running = true;
+
+ const attributeByteLength = attribute.array.byteLength;
+
+ if ( offset + size > attributeByteLength ) {
+
+ warn( 'Renderer: The given offset and size exceed the attribute size.' );
+ size = attributeByteLength - offset;
+
+ }
+
+ if ( size > readbackBuffer.size ) {
+
+ warn( 'Renderer: "ReadbackBuffer" size is not compatible with the selected attribute range.' );
+ size = readbackBuffer.size;
+
+ }
+
+ const arrayBuffer = await this.backend.getArrayBufferAsync( attribute, readbackBuffer, offset, size );
+
+ readbackBufferData.running = false;
- return await this.backend.getArrayBufferAsync( readbackBuffer );
+ return arrayBuffer;
}
diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js
index 2103c062e539e7..9cc049c979b6b6 100644
--- a/src/renderers/webgl-fallback/WebGLBackend.js
+++ b/src/renderers/webgl-fallback/WebGLBackend.js
@@ -312,12 +312,15 @@ class WebGLBackend extends Backend {
* a storage buffer attribute from the GPU to the CPU.
*
* @async
+ * @param {StorageBufferAttribute} attribute - The storage buffer attribute.
* @param {ReadbackBuffer} readbackBuffer - The readback buffer.
+ * @param {number} offset - The offset in bytes.
+ * @param {number} size - The size in bytes.
* @return {Promise} A promise that resolves with the buffer data when the data are ready.
*/
- async getArrayBufferAsync( readbackBuffer ) {
+ async getArrayBufferAsync( attribute, readbackBuffer, offset, size ) {
- return await this.attributeUtils.getArrayBufferAsync( readbackBuffer );
+ return await this.attributeUtils.getArrayBufferAsync( attribute, readbackBuffer, offset, size );
}
diff --git a/src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js b/src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js
index cef665a7eb9876..7e44c71afa209b 100644
--- a/src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js
+++ b/src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js
@@ -257,21 +257,20 @@ class WebGLAttributeUtils {
* a storage buffer attribute from the GPU to the CPU.
*
* @async
+ * @param {StorageBufferAttribute} attribute - The storage buffer attribute.
* @param {ReadbackBuffer} readbackBuffer - The readback buffer.
+ * @param {number} offset - The offset in bytes.
+ * @param {number} size - The size in bytes.
* @return {Promise} A promise that resolves with the buffer data when the data are ready.
*/
- async getArrayBufferAsync( readbackBuffer ) {
+ async getArrayBufferAsync( attribute, readbackBuffer, offset, size ) {
const backend = this.backend;
const { gl } = backend;
- const attribute = readbackBuffer.attribute;
const bufferAttribute = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
const { bufferGPU } = backend.get( bufferAttribute );
- const array = attribute.array;
- const byteLength = array.byteLength;
-
gl.bindBuffer( gl.COPY_READ_BUFFER, bufferGPU );
const readbackBufferData = backend.get( readbackBuffer );
@@ -280,6 +279,8 @@ class WebGLAttributeUtils {
if ( writeBuffer === undefined ) {
+ const byteLength = readbackBuffer.size;
+
writeBuffer = gl.createBuffer();
gl.bindBuffer( gl.COPY_WRITE_BUFFER, writeBuffer );
@@ -309,11 +310,11 @@ class WebGLAttributeUtils {
}
- gl.copyBufferSubData( gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, byteLength );
+ gl.copyBufferSubData( gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, offset, 0, size );
await backend.utils._clientWaitAsync();
- const dstBuffer = new attribute.array.constructor( array.length );
+ const dstBuffer = new Uint8Array( size );
// Ensure the buffer is bound before reading
gl.bindBuffer( gl.COPY_WRITE_BUFFER, writeBuffer );
@@ -323,7 +324,7 @@ class WebGLAttributeUtils {
gl.bindBuffer( gl.COPY_READ_BUFFER, null );
gl.bindBuffer( gl.COPY_WRITE_BUFFER, null );
- return dstBuffer;
+ return dstBuffer.buffer;
}
diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js
index 8204da58f43ca3..849d5e4a00ee0b 100644
--- a/src/renderers/webgpu/WebGPUBackend.js
+++ b/src/renderers/webgpu/WebGPUBackend.js
@@ -328,12 +328,15 @@ class WebGPUBackend extends Backend {
* a storage buffer attribute from the GPU to the CPU.
*
* @async
+ * @param {StorageBufferAttribute} attribute - The storage buffer attribute.
* @param {ReadbackBuffer} readbackBuffer - The readback buffer.
+ * @param {number} offset - The offset in bytes.
+ * @param {number} size - The size in bytes.
* @return {Promise} A promise that resolves with the buffer data when the data are ready.
*/
- async getArrayBufferAsync( readbackBuffer ) {
+ async getArrayBufferAsync( attribute, readbackBuffer, offset, size ) {
- return await this.attributeUtils.getArrayBufferAsync( readbackBuffer );
+ return await this.attributeUtils.getArrayBufferAsync( attribute, readbackBuffer, offset, size );
}
diff --git a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
index fc9a1aac36e290..48f2384818a1cb 100644
--- a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
+++ b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
@@ -318,51 +318,49 @@ class WebGPUAttributeUtils {
* a storage buffer attribute from the GPU to the CPU.
*
* @async
+ * @param {StorageBufferAttribute} attribute - The storage buffer attribute.
* @param {ReadbackBuffer} readbackBuffer - The storage buffer attribute.
+ * @param {number} offset - The offset in bytes.
+ * @param {number} size - The size in bytes.
* @return {Promise} A promise that resolves with the buffer data when the data are ready.
*/
- async getArrayBufferAsync( readbackBuffer ) {
+ async getArrayBufferAsync( attribute, readbackBuffer, offset, size ) {
const backend = this.backend;
const device = backend.device;
- const attribute = readbackBuffer.attribute;
const data = backend.get( this._getBufferAttribute( attribute ) );
const bufferGPU = data.buffer;
- const size = bufferGPU.size;
const readbackBufferData = backend.get( readbackBuffer );
+ const name = `readback_${ readbackBuffer.id || readbackBuffer.name }`;
+
let { readBufferGPU } = readbackBufferData;
if ( readBufferGPU === undefined ) {
+ const bufferSize = readbackBuffer.size + 4; // 4 extra bytes to accommodate unaligned offsets
+ const alignedBufferSize = bufferSize + ( ( 4 - ( bufferSize % 4 ) ) % 4 );
+
readBufferGPU = device.createBuffer( {
- label: `${ attribute.name }_readback`,
- size,
+ label: `${ name }_readback`,
+ size: alignedBufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
} );
// release / dispose
- const release = () => {
-
- readBufferGPU.unmap();
-
- };
-
const dispose = () => {
readBufferGPU.destroy();
backend.delete( readbackBuffer );
- readbackBuffer.removeEventListener( 'release', release );
readbackBuffer.removeEventListener( 'dispose', dispose );
};
- readbackBuffer.addEventListener( 'release', release );
readbackBuffer.addEventListener( 'dispose', dispose );
// register
@@ -372,15 +370,19 @@ class WebGPUAttributeUtils {
}
const cmdEncoder = device.createCommandEncoder( {
- label: `readback_encoder_${ attribute.name }`
+ label: `readback_encoder_${ name }`
} );
+ const diff = offset % 4;
+ const alignedOffset = offset - diff;
+ const alignedCopySize = ( size + diff ) + ( ( 4 - ( ( size + diff ) % 4 ) ) % 4 );
+
cmdEncoder.copyBufferToBuffer(
bufferGPU,
- 0,
+ alignedOffset,
readBufferGPU,
0,
- size
+ alignedCopySize
);
const gpuCommands = cmdEncoder.finish();
@@ -388,7 +390,9 @@ class WebGPUAttributeUtils {
await readBufferGPU.mapAsync( GPUMapMode.READ );
- const arrayBuffer = readBufferGPU.getMappedRange();
+ const arrayBuffer = readBufferGPU.getMappedRange().slice( diff, diff + size );
+
+ readBufferGPU.unmap();
return arrayBuffer;