diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index 4e500f10c4384a..d2037f2c78a6ef 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -365,6 +365,18 @@ class Backend { // attributes + /** + * Copies data of the given source buffer attribute to the given destination buffer attribute. + * + * @abstract + * @param {BufferAttribute} srcAttribute - The source buffer attribute. + * @param {BufferAttribute} dstAttribute - The destination buffer attribute. + * @param {number} byteLength - The number of bytes to copy. + * @param {number} [srcOffset=0] - The source offset in bytes. + * @param {number} [dstOffset=0] - The destination offset in bytes. + */ + copyBufferToBuffer( /*srcAttribute, dstAttribute, byteLength, srcOffset=0, dstOffset=0*/ ) {} + /** * Creates the GPU buffer of a shader attribute. * diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 756a2b4ed5f905..fc2876d8706ec1 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -2903,6 +2903,86 @@ class Renderer { } + /** + * Copies data of the given source buffer attribute into a destination buffer attribute. + * + * @param {BufferAttribute} srcAttribute - The source buffer attribute. + * @param {BufferAttribute} dstAttribute - The destination buffer attribute. + * @param {?number} [count=null] - The number of items to copy. If `null`, the overlapping range is copied. + * @param {number} [srcIndex=0] - The source start index (in items). + * @param {number} [dstIndex=0] - The destination start index (in items). + */ + copyBufferToBuffer( + srcAttribute, + dstAttribute, + count = null, + srcIndex = 0, + dstIndex = 0, + ) { + + // Layout must match for item-wise copy. + if ( + srcAttribute.itemSize !== dstAttribute.itemSize || + srcAttribute.array.BYTES_PER_ELEMENT !== + dstAttribute.array.BYTES_PER_ELEMENT + ) { + + error( 'Renderer.copyBufferToBuffer: Incompatible attribute layouts.' ); + return; + + } + + const stride = srcAttribute.itemSize * srcAttribute.array.BYTES_PER_ELEMENT; + + const maxCount = Math.max( + 0, + Math.min( srcAttribute.count - srcIndex, dstAttribute.count - dstIndex ), + ); + + if ( count === null ) count = maxCount; + else count = Math.max( 0, Math.min( count, maxCount ) ); + + if ( count <= 0 ) { + + error( + 'Renderer.copyBufferToBuffer: Copy would produce a zero-sized GPU buffer region.\n' + + 'This leads to invalid WebGPU bindings.\n' + + `src.count=${srcAttribute.count}, dst.count=${dstAttribute.count}, ` + + `srcIndex=${srcIndex}, dstIndex=${dstIndex}`, + ); + return; + + } + + const byteLength = count * stride; + const srcOffset = srcIndex * stride; + const dstOffset = dstIndex * stride; + + // Keep behavior consistent with WebGPU backend validation. + if ( + ( byteLength & 3 ) !== 0 || + ( srcOffset & 3 ) !== 0 || + ( dstOffset & 3 ) !== 0 + ) { + + error( + `Renderer.copyBufferToBuffer: WebGPU requires 4-byte aligned copies. Got srcOffset=${srcOffset}, dstOffset=${dstOffset}, byteLength=${byteLength}.`, + ); + return; + + } + + this.backend.copyBufferToBuffer( + srcAttribute, + dstAttribute, + byteLength, + srcOffset, + dstOffset, + ); + + } + + /** * Reads pixel data from the given render target. * diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index b00231b5ea2d13..7ad8b21b871a8a 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -2033,6 +2033,56 @@ class WebGLBackend extends Backend { } + /** + * Copies data of the given source buffer attribute to the given destination buffer attribute. + * + * Uses WebGL2 `copyBufferSubData`. + * + * @param {BufferAttribute} srcAttribute - The source buffer attribute. + * @param {BufferAttribute} dstAttribute - The destination buffer attribute. + * @param {number} byteLength - The number of bytes to copy. + * @param {number} [srcOffset=0] - The source offset in bytes. + * @param {number} [dstOffset=0] - The destination offset in bytes. + */ + copyBufferToBuffer( + srcAttribute, + dstAttribute, + byteLength, + srcOffset = 0, + dstOffset = 0, + ) { + + const gl = this.gl; + + const srcData = this.get( srcAttribute ); + const dstData = this.get( dstAttribute ); + + if ( srcData.bufferGPU === undefined ) this.createAttribute( srcAttribute ); + if ( dstData.bufferGPU === undefined ) this.createAttribute( dstAttribute ); + + const srcGPU = srcData.bufferGPU; + const dstGPU = dstData.bufferGPU; + + const prevRead = gl.getParameter( gl.COPY_READ_BUFFER_BINDING ); + const prevWrite = gl.getParameter( gl.COPY_WRITE_BUFFER_BINDING ); + + gl.bindBuffer( gl.COPY_READ_BUFFER, srcGPU ); + gl.bindBuffer( gl.COPY_WRITE_BUFFER, dstGPU ); + + gl.copyBufferSubData( + gl.COPY_READ_BUFFER, + gl.COPY_WRITE_BUFFER, + srcOffset, + dstOffset, + byteLength, + ); + + gl.bindBuffer( gl.COPY_READ_BUFFER, prevRead ); + gl.bindBuffer( gl.COPY_WRITE_BUFFER, prevWrite ); + + } + + /** * Checks if the given compatibility is supported by the backend. * diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index b23217b0d0a412..5ddcd4a22c5011 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2531,6 +2531,91 @@ class WebGPUBackend extends Backend { } + /** + * Copies data of the given source buffer attribute to the given destination buffer attribute. + * + * @param {BufferAttribute} srcAttribute - The source buffer attribute. + * @param {BufferAttribute} dstAttribute - The destination buffer attribute. + * @param {number} byteLength - The number of bytes to copy. + * @param {number} [srcOffset=0] - The source offset in bytes. + * @param {number} [dstOffset=0] - The destination offset in bytes. + */ + copyBufferToBuffer( srcAttribute, dstAttribute, byteLength, srcOffset = 0, dstOffset = 0 ) { + + if ( ( srcOffset & 3 ) !== 0 || ( dstOffset & 3 ) !== 0 || ( byteLength & 3 ) !== 0 ) { + + error( 'WebGPUBackend: copyBufferToBuffer: srcOffset, dstOffset and byteLength must be multiples of 4 bytes.' ); + return; + + } + + const srcData = this.get( srcAttribute ); + const dstData = this.get( dstAttribute ); + + // Source must exist + if ( srcData.buffer === undefined ) { + + error( 'WebGPUBackend: copyBufferToBuffer: src GPUBuffer is undefined.', { srcId: srcAttribute.id } ); + return; + + } + + // Destination may be created. + if ( dstData.buffer === undefined ) { + + if ( dstAttribute.isStorageBufferAttribute || dstAttribute.isStorageInstancedBufferAttribute ) { + + this.createStorageAttribute( dstAttribute ); + + } else { + + this.createAttribute( dstAttribute ); + + } + + } + + // Re-fetch buffers after possible create + const sourceGPU = this.get( srcAttribute ).buffer; + const destinationGPU = this.get( dstAttribute ).buffer; + + if ( sourceGPU === undefined || destinationGPU === undefined ) { + + error( 'WebGPUBackend: copyBufferToBuffer: missing GPUBuffer(s) after ensure.', { + srcId: srcAttribute.id, + dstId: dstAttribute.id, + srcHasBuffer: sourceGPU !== undefined, + dstHasBuffer: destinationGPU !== undefined, + } ); + + return; + + } + + if ( srcOffset + byteLength > sourceGPU.size || dstOffset + byteLength > destinationGPU.size ) { + + error( 'WebGPUBackend: copyBufferToBuffer: Copy region out of bounds.', { + srcSize: sourceGPU.size, + dstSize: destinationGPU.size, + srcOffset, + dstOffset, + byteLength, + } ); + + return; + + } + + const encoder = this.device.createCommandEncoder( { + label: 'copyBufferToBuffer_' + srcAttribute.id + '_' + dstAttribute.id, + } ); + + encoder.copyBufferToBuffer( sourceGPU, srcOffset, destinationGPU, dstOffset, byteLength ); + + this.device.queue.submit( [ encoder.finish() ] ); + + } + /** * Checks if the given compatibility is supported by the backend. *