three
Version:
JavaScript 3D library
583 lines (378 loc) • 15.4 kB
JavaScript
import { GPUInputStepMode } from './WebGPUConstants.js';
import { submit } from './WebGPUUtils.js';
import GPUBufferDescriptor from '../descriptors/GPUBufferDescriptor.js';
import GPUCommandEncoderDescriptor from '../descriptors/GPUCommandEncoderDescriptor.js';
import { Float16BufferAttribute } from '../../../core/BufferAttribute.js';
import { isTypedArray, error } from '../../../utils.js';
const _bufferDescriptor = new GPUBufferDescriptor();
const _commandEncoderDescriptor = new GPUCommandEncoderDescriptor();
const typedArraysToVertexFormatPrefix = new Map( [
[ Int8Array, [ 'sint8', 'snorm8' ]],
[ Uint8Array, [ 'uint8', 'unorm8' ]],
[ Int16Array, [ 'sint16', 'snorm16' ]],
[ Uint16Array, [ 'uint16', 'unorm16' ]],
[ Int32Array, [ 'sint32', 'snorm32' ]],
[ Uint32Array, [ 'uint32', 'unorm32' ]],
[ Float32Array, [ 'float32', ]],
] );
if ( typeof Float16Array !== 'undefined' ) {
typedArraysToVertexFormatPrefix.set( Float16Array, [ 'float16' ] );
}
const typedAttributeToVertexFormatPrefix = new Map( [
[ Float16BufferAttribute, [ 'float16', ]],
] );
const typeArraysToVertexFormatPrefixForItemSize1 = new Map( [
[ Int32Array, 'sint32' ],
[ Int16Array, 'sint32' ], // patch for INT16
[ Uint32Array, 'uint32' ],
[ Uint16Array, 'uint32' ], // patch for UINT16
[ Float32Array, 'float32' ]
] );
/**
* A WebGPU backend utility module for managing shader attributes.
*
* @private
*/
class WebGPUAttributeUtils {
/**
* Constructs a new utility object.
*
* @param {WebGPUBackend} backend - The WebGPU backend.
*/
constructor( backend ) {
/**
* A reference to the WebGPU backend.
*
* @type {WebGPUBackend}
*/
this.backend = backend;
}
/**
* Creates the GPU buffer for the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
* @param {GPUBufferUsage} usage - A flag that indicates how the buffer may be used after its creation.
*/
createAttribute( attribute, usage ) {
const bufferAttribute = this._getBufferAttribute( attribute );
const backend = this.backend;
const bufferData = backend.get( bufferAttribute );
let buffer = bufferData.buffer;
if ( buffer === undefined ) {
const device = backend.device;
let array = bufferAttribute.array;
// patch for INT16 and UINT16
if ( attribute.normalized === false ) {
if ( array.constructor === Int16Array || array.constructor === Int8Array ) {
array = new Int32Array( array );
} else if ( array.constructor === Uint16Array || array.constructor === Uint8Array ) {
array = new Uint32Array( array );
if ( usage & GPUBufferUsage.INDEX ) {
for ( let i = 0; i < array.length; i ++ ) {
if ( array[ i ] === 0xffff ) array[ i ] = 0xffffffff; // use correct primitive restart index
}
}
}
}
bufferAttribute.array = array;
let paddedItemSize;
if ( ( bufferAttribute.isStorageBufferAttribute || bufferAttribute.isStorageInstancedBufferAttribute ) && bufferAttribute.itemSize === 3 ) {
// WGSL does not support packed vec3 data in storage buffers, pad to vec4
paddedItemSize = 4;
} else if ( bufferAttribute.itemSize > 1 && ( bufferAttribute.itemSize * array.BYTES_PER_ELEMENT ) % 4 !== 0 ) {
// arrayStride must be a multiple of 4
const byteStride = bufferAttribute.itemSize * array.BYTES_PER_ELEMENT;
paddedItemSize = ( Math.floor( ( byteStride + 3 ) / 4 ) * 4 ) / array.BYTES_PER_ELEMENT;
}
if ( paddedItemSize !== undefined ) {
const itemSize = bufferAttribute.itemSize;
const paddedArray = new array.constructor( bufferAttribute.count * paddedItemSize );
for ( let i = 0; i < bufferAttribute.count; i ++ ) {
paddedArray.set( array.subarray( i * itemSize, i * itemSize + itemSize ), i * paddedItemSize );
}
if ( bufferAttribute.isStorageBufferAttribute || bufferAttribute.isStorageInstancedBufferAttribute ) {
// update the storage attribute so storage bindings access the padded layout
bufferAttribute.itemSize = paddedItemSize;
bufferAttribute.array = paddedArray;
}
array = paddedArray;
// save the original and padded item size so buffer updates can apply the same padding
bufferData._itemSize = itemSize;
bufferData._paddedItemSize = paddedItemSize;
}
// total buffer size must be a multiple of 4
const byteLength = array.byteLength;
const size = byteLength + ( ( 4 - ( byteLength % 4 ) ) % 4 );
_bufferDescriptor.label = bufferAttribute.name;
_bufferDescriptor.size = size;
_bufferDescriptor.usage = usage;
_bufferDescriptor.mappedAtCreation = true;
buffer = device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
new array.constructor( buffer.getMappedRange() ).set( array );
buffer.unmap();
bufferData.buffer = buffer;
}
}
/**
* Updates the GPU buffer of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
updateAttribute( attribute ) {
const bufferAttribute = this._getBufferAttribute( attribute );
const backend = this.backend;
const device = backend.device;
const bufferData = backend.get( bufferAttribute );
const buffer = backend.get( bufferAttribute ).buffer;
let array = bufferAttribute.array;
const itemSize = bufferData._itemSize;
const paddedItemSize = bufferData._paddedItemSize;
if ( paddedItemSize !== undefined ) {
// if the attribute data were padded on upload, apply the same padding on updates.
array = new array.constructor( bufferAttribute.count * paddedItemSize );
for ( let i = 0; i < bufferAttribute.count; i ++ ) {
array.set( bufferAttribute.array.subarray( i * itemSize, i * itemSize + itemSize ), i * paddedItemSize );
}
if ( bufferAttribute.isStorageBufferAttribute || bufferAttribute.isStorageInstancedBufferAttribute ) {
// keep the storage attribute in sync with the padded layout
bufferAttribute.array = array;
}
}
const updateRanges = bufferAttribute.updateRanges;
if ( updateRanges.length === 0 ) {
// Not using update ranges
device.queue.writeBuffer(
buffer,
0,
array,
0
);
} else {
const isTyped = isTypedArray( array );
const byteOffsetFactor = isTyped ? 1 : array.BYTES_PER_ELEMENT;
for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
const range = updateRanges[ i ];
let dataOffset, size;
if ( paddedItemSize !== undefined ) {
const vertexStart = Math.floor( range.start / itemSize );
const vertexCount = Math.ceil( ( range.start + range.count ) / itemSize ) - vertexStart;
dataOffset = vertexStart * paddedItemSize * byteOffsetFactor;
size = vertexCount * paddedItemSize * byteOffsetFactor;
} else {
dataOffset = range.start * byteOffsetFactor;
size = range.count * byteOffsetFactor;
}
const bufferOffset = dataOffset * ( isTyped ? array.BYTES_PER_ELEMENT : 1 ); // bufferOffset is always in bytes
device.queue.writeBuffer(
buffer,
bufferOffset,
array,
dataOffset,
size
);
}
bufferAttribute.clearUpdateRanges();
}
}
/**
* This method creates the vertex buffer layout data which are
* require when creating a render pipeline for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {Array<Object>} An array holding objects which describe the vertex buffer layout.
*/
createShaderVertexBuffers( renderObject ) {
const attributes = renderObject.getAttributes();
const vertexBuffers = new Map();
for ( let slot = 0; slot < attributes.length; slot ++ ) {
const geometryAttribute = attributes[ slot ];
const bytesPerElement = geometryAttribute.array.BYTES_PER_ELEMENT;
const bufferAttribute = this._getBufferAttribute( geometryAttribute );
let vertexBufferLayout = vertexBuffers.get( bufferAttribute );
if ( vertexBufferLayout === undefined ) {
let arrayStride, stepMode;
if ( geometryAttribute.isInterleavedBufferAttribute === true ) {
arrayStride = geometryAttribute.data.stride * bytesPerElement;
stepMode = geometryAttribute.data.isInstancedInterleavedBuffer ? GPUInputStepMode.Instance : GPUInputStepMode.Vertex;
} else {
arrayStride = geometryAttribute.itemSize * bytesPerElement;
stepMode = geometryAttribute.isInstancedBufferAttribute ? GPUInputStepMode.Instance : GPUInputStepMode.Vertex;
if ( geometryAttribute.itemSize > 1 && arrayStride % 4 !== 0 ) {
// packed attribute data are padded per vertex on upload
arrayStride = Math.floor( ( arrayStride + 3 ) / 4 ) * 4;
}
}
// patch for INT16 and UINT16
if ( geometryAttribute.normalized === false && ( geometryAttribute.array.constructor === Int16Array || geometryAttribute.array.constructor === Uint16Array ) ) {
arrayStride = 4;
}
vertexBufferLayout = {
arrayStride,
attributes: [],
stepMode
};
vertexBuffers.set( bufferAttribute, vertexBufferLayout );
}
const format = this._getVertexFormat( geometryAttribute );
const offset = ( geometryAttribute.isInterleavedBufferAttribute === true ) ? geometryAttribute.offset * bytesPerElement : 0;
vertexBufferLayout.attributes.push( {
shaderLocation: slot,
offset,
format
} );
}
return Array.from( vertexBuffers.values() );
}
/**
* Destroys the GPU buffer of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
destroyAttribute( attribute ) {
const backend = this.backend;
const data = backend.get( this._getBufferAttribute( attribute ) );
data.buffer.destroy();
backend.delete( attribute );
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can
* be used to retain and reuse handles to the intermediate buffers and prevent
* new allocation.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read frm.
* @param {number} count - The offset from which to start reading the
* @param {number} offset - The storage buffer attribute.
* @param {ReadbackBuffer|ArrayBuffer} target - The storage buffer attribute.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = - 1 ) {
const backend = this.backend;
const device = backend.device;
const data = backend.get( this._getBufferAttribute( attribute ) );
const bufferGPU = data.buffer;
const byteLength = count === - 1 ? bufferGPU.size - offset : count;
let readBufferGPU;
if ( target !== null && target.isReadbackBuffer ) {
const readbackInfo = backend.get( target );
if ( target._mapped === true ) {
throw new Error( 'THREE.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again.' );
}
target._mapped = true;
// initialize the GPU-side read copy buffer if it is not present
if ( readbackInfo.readBufferGPU === undefined ) {
_bufferDescriptor.label = `${ target.name }_readback`;
_bufferDescriptor.size = target.maxByteLength;
_bufferDescriptor.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
readBufferGPU = device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
// release / dispose
const releaseCallback = () => {
target.buffer = null;
target._mapped = false;
readBufferGPU.unmap();
};
const disposeCallback = () => {
target.buffer = null;
target._mapped = false;
readBufferGPU.destroy();
backend.delete( target );
target.removeEventListener( 'release', releaseCallback );
target.removeEventListener( 'dispose', disposeCallback );
};
target.addEventListener( 'release', releaseCallback );
target.addEventListener( 'dispose', disposeCallback );
// register
readbackInfo.readBufferGPU = readBufferGPU;
} else {
readBufferGPU = readbackInfo.readBufferGPU;
}
} else {
// create a new temp buffer for array buffers otherwise
_bufferDescriptor.label = `${ attribute.name }_readback`;
_bufferDescriptor.size = byteLength;
_bufferDescriptor.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
readBufferGPU = device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
}
// copy the data
_commandEncoderDescriptor.label = `readback_encoder_${ attribute.name }`;
const cmdEncoder = device.createCommandEncoder( _commandEncoderDescriptor );
_commandEncoderDescriptor.reset();
cmdEncoder.copyBufferToBuffer(
bufferGPU,
offset,
readBufferGPU,
0,
byteLength,
);
const gpuCommands = cmdEncoder.finish();
submit( device, gpuCommands );
// map the data to the CPU
await readBufferGPU.mapAsync( GPUMapMode.READ, 0, byteLength );
if ( target === null ) {
// return a new array buffer and clean up the gpu handles
const arrayBuffer = readBufferGPU.getMappedRange( 0, byteLength );
const result = arrayBuffer.slice();
readBufferGPU.destroy();
return result;
} else if ( target.isReadbackBuffer ) {
// assign the data to the read back handle
target.buffer = readBufferGPU.getMappedRange( 0, byteLength );
return target;
} else {
// copy the data into the target array buffer
const arrayBuffer = readBufferGPU.getMappedRange( 0, byteLength );
new Uint8Array( target ).set( new Uint8Array( arrayBuffer ) );
readBufferGPU.destroy();
return target;
}
}
/**
* Returns the vertex format of the given buffer attribute.
*
* @private
* @param {BufferAttribute} geometryAttribute - The buffer attribute.
* @return {string|undefined} The vertex format (e.g. 'float32x3').
*/
_getVertexFormat( geometryAttribute ) {
const { itemSize, normalized } = geometryAttribute;
const ArrayType = geometryAttribute.array.constructor;
const AttributeType = geometryAttribute.constructor;
let format;
if ( itemSize === 1 ) {
format = typeArraysToVertexFormatPrefixForItemSize1.get( ArrayType );
} else {
const prefixOptions = typedAttributeToVertexFormatPrefix.get( AttributeType ) || typedArraysToVertexFormatPrefix.get( ArrayType );
const prefix = prefixOptions[ normalized ? 1 : 0 ];
if ( prefix ) {
const bytesPerUnit = ArrayType.BYTES_PER_ELEMENT * itemSize;
const paddedBytesPerUnit = Math.floor( ( bytesPerUnit + 3 ) / 4 ) * 4;
const paddedItemSize = paddedBytesPerUnit / ArrayType.BYTES_PER_ELEMENT;
if ( paddedItemSize % 1 ) {
throw new Error( 'THREE.WebGPUAttributeUtils: Bad vertex format item size.' );
}
format = `${prefix}x${paddedItemSize}`;
}
}
if ( ! format ) {
error( 'WebGPUAttributeUtils: Vertex format not supported yet.' );
}
return format;
}
/**
* Utility method for handling interleaved buffer attributes correctly.
* To process them, their `InterleavedBuffer` is returned.
*
* @private
* @param {BufferAttribute} attribute - The attribute.
* @return {BufferAttribute|InterleavedBuffer}
*/
_getBufferAttribute( attribute ) {
if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
return attribute;
}
}
export default WebGPUAttributeUtils;