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;