Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/renderers/common/Backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
80 changes: 80 additions & 0 deletions src/renderers/common/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
50 changes: 50 additions & 0 deletions src/renderers/webgl-fallback/WebGLBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
85 changes: 85 additions & 0 deletions src/renderers/webgpu/WebGPUBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down