diff --git a/Sources/Rendering/OpenGL/ShaderCache/index.js b/Sources/Rendering/OpenGL/ShaderCache/index.js index 836ca58cba7..e683d65a770 100644 --- a/Sources/Rendering/OpenGL/ShaderCache/index.js +++ b/Sources/Rendering/OpenGL/ShaderCache/index.js @@ -239,8 +239,6 @@ export function extend(publicAPI, model, initialValues = {}) { // Object methods vtkShaderCache(publicAPI, model); - - return Object.freeze(publicAPI); } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/BufferManager/index.js b/Sources/Rendering/WebGPU/BufferManager/index.js index c63c854c969..c667abaea95 100644 --- a/Sources/Rendering/WebGPU/BufferManager/index.js +++ b/Sources/Rendering/WebGPU/BufferManager/index.js @@ -1,6 +1,7 @@ import * as macro from 'vtk.js/Sources/macro'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkWebGPUBuffer from 'vtk.js/Sources/Rendering/WebGPU/Buffer'; +import vtkWebGPUTypes from 'vtk.js/Sources/Rendering/WebGPU/Types'; import vtkProperty from 'vtk.js/Sources/Rendering/Core/Property'; import Constants from './Constants'; @@ -362,36 +363,6 @@ function generateNormals(cellArray, primType, representation, inArray) { return packedVBO; } -function getStrideFromFormat(format) { - if (!format) return 0; - let numComp = 1; - if (format.substring(format.length - 2) === 'x4') numComp = 4; - if (format.substring(format.length - 2) === 'x3') numComp = 3; - if (format.substring(format.length - 2) === 'x2') numComp = 2; - - let typeSize = 4; - if (numComp > 1) { - if (format.substring(format.length - 3, format.length - 1) === '8x') { - typeSize = 1; - } - if (format.substring(format.length - 4, format.length - 1) === '16x') { - typeSize = 2; - } - } else { - if (format.substring(format.length - 1) === '8') typeSize = 1; - if (format.substring(format.length - 2) === '16') typeSize = 2; - } - return numComp * typeSize; -} - -function getArrayTypeFromFormat(format) { - if (!format) return null; - if (format.substring(0, 7) === 'float32') return 'Float32Array'; - if (format.substring(0, 6) === 'snorm8') return 'Int8Array'; - if (format.substring(0, 6) === 'unorm8') return 'Uint8Array'; - return ''; -} - // ---------------------------------------------------------------------------- // vtkWebGPUBufferManager methods // ---------------------------------------------------------------------------- @@ -426,8 +397,8 @@ function vtkWebGPUBufferManager(publicAPI, model) { const buffer = vtkWebGPUBuffer.newInstance(); buffer.setDevice(model.device); - const stride = getStrideFromFormat(req.format); - const arrayType = getArrayTypeFromFormat(req.format); + const stride = vtkWebGPUTypes.getByteStrideFromBufferFormat(req.format); + const arrayType = vtkWebGPUTypes.getNativeTypeFromBufferFormat(req.format); let gpuUsage = null; // handle uniform buffers diff --git a/Sources/Rendering/WebGPU/Device/index.js b/Sources/Rendering/WebGPU/Device/index.js index 4f2e27a8116..e205d3a16f9 100644 --- a/Sources/Rendering/WebGPU/Device/index.js +++ b/Sources/Rendering/WebGPU/Device/index.js @@ -187,4 +187,7 @@ export function extend(publicAPI, model, initialValues = {}) { export const newInstance = macro.newInstance(extend, 'vtkWebGPUDevice'); // ---------------------------------------------------------------------------- -export default { newInstance, extend }; +export default { + newInstance, + extend, +}; diff --git a/Sources/Rendering/WebGPU/PolyDataMapper/index.js b/Sources/Rendering/WebGPU/PolyDataMapper/index.js index 771d3aca712..85d8e47e0a8 100644 --- a/Sources/Rendering/WebGPU/PolyDataMapper/index.js +++ b/Sources/Rendering/WebGPU/PolyDataMapper/index.js @@ -11,6 +11,7 @@ import vtkWebGPUPipeline from 'vtk.js/Sources/Rendering/WebGPU/Pipeline'; import vtkWebGPUSampler from 'vtk.js/Sources/Rendering/WebGPU/Sampler'; import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache'; import vtkWebGPUShaderDescription from 'vtk.js/Sources/Rendering/WebGPU/ShaderDescription'; +import vtkWebGPUUniformBuffer from 'vtk.js/Sources/Rendering/WebGPU/UniformBuffer'; import vtkWebGPUVertexInput from 'vtk.js/Sources/Rendering/WebGPU/VertexInput'; import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode'; @@ -103,28 +104,6 @@ fn main() -> void } `; -const vtkWebGPUPolyDataUBOCode = ` -[[block]] struct mapperVals -{ - [[offset(0)]] MCVCMatrix: mat4x4; - [[offset(64)]] normalMatrix: mat4x4; - [[offset(128)]] AmbientColor: vec4; - [[offset(144)]] DiffuseColor: vec4; - [[offset(160)]] SpecularColor: vec4; - [[offset(176)]] AmbientIntensity: f32; - [[offset(180)]] DiffuseIntensity: f32; - [[offset(184)]] SpecularIntensity: f32; - [[offset(188)]] Opacity: f32; - [[offset(192)]] Metallic: f32; - [[offset(196)]] Roughness: f32; - [[offset(200)]] EmissiveFactor: f32; - [[offset(204)]] SpecularPower: f32; -}; -[[binding(0), group(1)]] var mapperUBO : mapperVals; -`; - -const vtkWebGPUPolyDataMapperUBOSize = 208 / 4; - // ---------------------------------------------------------------------------- // vtkWebGPUPolyDataMapper methods // ---------------------------------------------------------------------------- @@ -158,76 +137,44 @@ function vtkWebGPUPolyDataMapper(publicAPI, model) { }; publicAPI.updateUBO = () => { - let needSend = false; - // make sure the data is up to date const actor = model.WebGPUActor.getRenderable(); const ppty = actor.getProperty(); - const utime = model.UBOUpdateTime.getMTime(); + const utime = model.UBO.getSendTime(); if ( publicAPI.getMTime() > utime || ppty.getMTime() > utime || model.renderable.getMTime() > utime ) { let aColor = ppty.getAmbientColorByReference(); - model.UBOData[32] = aColor[0]; - model.UBOData[33] = aColor[1]; - model.UBOData[34] = aColor[2]; - model.UBOData[35] = 1.0; + model.UBO.setValue('AmbientIntensity', ppty.getAmbient()); + model.UBO.setArray('AmbientColor', [ + aColor[0], + aColor[1], + aColor[2], + 1.0, + ]); + model.UBO.setValue('DiffuseIntensity', ppty.getDiffuse()); aColor = ppty.getDiffuseColorByReference(); - model.UBOData[36] = aColor[0]; - model.UBOData[37] = aColor[1]; - model.UBOData[38] = aColor[2]; - model.UBOData[39] = 1.0; + model.UBO.setArray('DiffuseColor', [ + aColor[0], + aColor[1], + aColor[2], + 1.0, + ]); + model.UBO.setValue('SpecularIntensity', ppty.getSpecular()); + model.UBO.setValue('SpecularPower', ppty.getSpecularPower()); aColor = ppty.getSpecularColorByReference(); - model.UBOData[40] = aColor[0]; - model.UBOData[41] = aColor[1]; - model.UBOData[42] = aColor[2]; - model.UBOData[43] = 1.0; - model.UBOData[44] = ppty.getAmbient(); - model.UBOData[45] = ppty.getDiffuse(); - model.UBOData[46] = ppty.getSpecular(); - model.UBOData[47] = ppty.getOpacity(); - model.UBOData[51] = ppty.getSpecularPower(); - - model.UBOUpdateTime.modified(); - needSend = true; - } + model.UBO.setArray('SpecularColor', [ + aColor[0], + aColor[1], + aColor[2], + 1.0, + ]); + model.UBO.setValue('Opacity', ppty.getOpacity()); - // make sure the buffer is created - if (!model.UBO) { - const req = { - address: model.UBOData, - time: 0, - usage: BufferUsage.UniformArray, - }; const device = model.WebGPURenderWindow.getDevice(); - model.UBO = device.getBufferManager().getBuffer(req); - model.UBOBindGroup = device.getHandle().createBindGroup({ - layout: device.getMapperBindGroupLayout(), - entries: [ - { - binding: 0, - resource: { - buffer: model.UBO.getHandle(), - }, - }, - ], - }); - needSend = false; - } - - // send data down if needed - if (needSend) { - model.WebGPURenderWindow.getDevice() - .getHandle() - .queue.writeBuffer( - model.UBO.getHandle(), - 0, - model.UBOData.buffer, - model.UBOData.byteOffset, - model.UBOData.byteLength - ); + model.UBO.sendIfNeeded(device, device.getMapperBindGroupLayout()); } }; @@ -254,7 +201,7 @@ function vtkWebGPUPolyDataMapper(publicAPI, model) { model.WebGPURenderer.getUBOCode(), ]).result; code = vtkWebGPUShaderCache.substitute(code, '//VTK::Mapper::UBO', [ - vtkWebGPUPolyDataUBOCode, + model.UBO.getShaderCode(), ]).result; code = vtkWebGPUShaderCache.substitute(code, '//VTK::VertexInput', [ vertexInput.getShaderCode(), @@ -270,7 +217,7 @@ function vtkWebGPUPolyDataMapper(publicAPI, model) { model.WebGPURenderer.getUBOCode(), ]).result; code = vtkWebGPUShaderCache.substitute(code, '//VTK::Mapper::UBO', [ - vtkWebGPUPolyDataUBOCode, + model.UBO.getShaderCode(), ]).result; const fDesc = vtkWebGPUShaderDescription.newInstance({ @@ -795,9 +742,26 @@ export function extend(publicAPI, model, initialValues = {}) { model.tmpMat3 = mat3.identity(new Float64Array(9)); model.tmpMat4 = mat4.identity(new Float64Array(16)); - model.UBOData = new Float32Array(vtkWebGPUPolyDataMapperUBOSize); - model.UBOUpdateTime = {}; - macro.obj(model.UBOUpdateTime); + + model.UBO = vtkWebGPUUniformBuffer.newInstance(); + model.UBO.setBinding(0); + model.UBO.setGroup(1); + model.UBO.setName('mapperUBO'); + model.UBO.addEntry('MCVCMatrix', 'mat4x4'); + model.UBO.addEntry('AmbientColor', 'vec4'); + model.UBO.addEntry('DiffuseColor', 'vec4'); + model.UBO.addEntry('AmbientIntensity', 'f32'); + model.UBO.addEntry('DiffuseIntensity', 'f32'); + model.UBO.addEntry('SpecularColor', 'vec4'); + model.UBO.addEntry('SpecularIntensity', 'f32'); + model.UBO.addEntry('Opacity', 'f32'); + model.UBO.addEntry('SpecularPower', 'f32'); + + // [[offset(0)]] MCVCMatrix: mat4x4; + // [[offset(64)]] normalMatrix: mat4x4; + // [[offset(192)]] Metallic: f32; + // [[offset(196)]] Roughness: f32; + // [[offset(200)]] EmissiveFactor: f32; model.samplers = []; model.textures = []; @@ -813,7 +777,7 @@ export function extend(publicAPI, model, initialValues = {}) { const renderEncoder = model.WebGPURenderer.getRenderEncoder(); // bind the mapper UBO - renderEncoder.setBindGroup(1, model.UBOBindGroup); + renderEncoder.setBindGroup(1, model.UBO.getBindGroup()); // bind any textures and samplers for (let t = 0; t < model.textures.length; t++) { @@ -834,7 +798,7 @@ export function extend(publicAPI, model, initialValues = {}) { } // Build VTK API - macro.setGet(publicAPI, model, ['context', 'renderEncoder']); + macro.setGet(publicAPI, model, ['context', 'renderEncoder', 'UBO']); model.VBOBuildTime = {}; macro.obj(model.VBOBuildTime, { mtime: 0 }); diff --git a/Sources/Rendering/WebGPU/README.md b/Sources/Rendering/WebGPU/README.md index c714c14969b..561a3b3006d 100644 --- a/Sources/Rendering/WebGPU/README.md +++ b/Sources/Rendering/WebGPU/README.md @@ -29,11 +29,11 @@ ToDo - PBR lighting to replace the simple model currently coded - image display - volume rendering -- transparency -- actor matrix support +- actor matrix support with auto shift - sphere/stick mappers -- post render operations/framebuffers - hardware selector +- eventually switch to using IBOs and flat interpolation +- cropping planes for polydata mapper Developer Notes ============ diff --git a/Sources/Rendering/WebGPU/Renderer/index.js b/Sources/Rendering/WebGPU/Renderer/index.js index 9130cde06cb..9732dbbb64a 100644 --- a/Sources/Rendering/WebGPU/Renderer/index.js +++ b/Sources/Rendering/WebGPU/Renderer/index.js @@ -1,23 +1,10 @@ import macro from 'vtk.js/Sources/macro'; import { mat4 } from 'gl-matrix'; -import vtkWebGPUBufferManager from 'vtk.js/Sources/Rendering/WebGPU/BufferManager'; +import vtkWebGPUUniformBuffer from 'vtk.js/Sources/Rendering/WebGPU/UniformBuffer'; import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; const { vtkDebugMacro } = macro; -const { BufferUsage } = vtkWebGPUBufferManager; - -const vtkWebGPURendererUBOCode = ` -[[block]] struct renderVals -{ - [[offset(0)]] WCDCMatrix : mat4x4; - [[offset(64)]] WCVCMatrix : mat4x4; - [[offset(128)]] VCDCMatrix : mat4x4; - [[offset(192)]] WCVCNormals : mat4x4; -}; -[[binding(0), group(0)]] var rendererUBO : renderVals; -`; -const vtkWebGPURenderUBOSize = 256 / 4; // ---------------------------------------------------------------------------- // vtkWebGPURenderer methods @@ -47,7 +34,7 @@ function vtkWebGPURenderer(publicAPI, model) { } }; - publicAPI.getUBOCode = () => vtkWebGPURendererUBOCode; + publicAPI.getUBOCode = () => model.UBO.getShaderCode(); publicAPI.updateLights = () => { let count = 0; @@ -81,13 +68,10 @@ function vtkWebGPURenderer(publicAPI, model) { }; publicAPI.updateUBO = () => { - const device = model.parent.getDevice(); - let needSend = false; - // make sure the data is up to date // has the camera changed? const cam = model.renderable.getActiveCamera(); - const utime = model.UBOUpdateTime.getMTime(); + const utime = model.UBO.getSendTime(); if ( model.parent.getMTime() > utime || publicAPI.getMTime() > utime || @@ -96,30 +80,26 @@ function vtkWebGPURenderer(publicAPI, model) { ) { const aspectRatio = publicAPI.getAspectRatio(); const mat = cam.getCompositeProjectionMatrix(aspectRatio, -1, 1); - for (let i = 0; i < 4; i++) { - for (let j = 0; j < 4; j++) { - model.UBOData[i * 4 + j] = mat[j * 4 + i]; - } - } - model.UBOData[0] = mat[0]; - model.UBOData[4] = mat[1]; - model.UBOData[8] = mat[2]; - model.UBOData[12] = mat[3]; - - model.UBOData[1] = mat[4]; - model.UBOData[5] = mat[5]; - model.UBOData[9] = mat[6]; - model.UBOData[13] = mat[7]; - - model.UBOData[2] = 0.5 * mat[8] + 0.5 * mat[12]; - model.UBOData[6] = 0.5 * mat[9] + 0.5 * mat[13]; - model.UBOData[10] = 0.5 * mat[10] + 0.5 * mat[14]; - model.UBOData[14] = 0.5 * mat[11] + 0.5 * mat[15]; - - model.UBOData[3] = mat[12]; - model.UBOData[7] = mat[13]; - model.UBOData[11] = mat[14]; - model.UBOData[15] = mat[15]; + const uboData = model.UBO.getNativeView('WCDCMatrix'); + uboData[0] = mat[0]; + uboData[4] = mat[1]; + uboData[8] = mat[2]; + uboData[12] = mat[3]; + + uboData[1] = mat[4]; + uboData[5] = mat[5]; + uboData[9] = mat[6]; + uboData[13] = mat[7]; + + uboData[2] = 0.5 * mat[8] + 0.5 * mat[12]; + uboData[6] = 0.5 * mat[9] + 0.5 * mat[13]; + uboData[10] = 0.5 * mat[10] + 0.5 * mat[14]; + uboData[14] = 0.5 * mat[11] + 0.5 * mat[15]; + + uboData[3] = mat[12]; + uboData[7] = mat[13]; + uboData[11] = mat[14]; + uboData[15] = mat[15]; mat4.copy(model.tmpMat4, cam.getViewMatrix()); // zero out translation @@ -128,46 +108,10 @@ function vtkWebGPURenderer(publicAPI, model) { model.tmpMat4[11] = 0.0; mat4.invert(model.tmpMat4, model.tmpMat4); - model.UBOData.set(model.tmpMat4, 48); - // console.log(JSON.stringify(model.UBOData)); - - model.UBOUpdateTime.modified(); - needSend = true; - } - - // make sure the buffer is created - if (!model.UBO) { - const req = { - address: model.UBOData, - time: 0, - usage: BufferUsage.UniformArray, - }; - model.UBO = device.getBufferManager().getBuffer(req); - model.UBOBindGroup = device.getHandle().createBindGroup({ - layout: device.getRendererBindGroupLayout(), - entries: [ - { - binding: 0, - resource: { - buffer: model.UBO.getHandle(), - }, - }, - ], - }); - needSend = false; - } + model.UBO.setArray('WCVCNormals', model.tmpMat4); - // send data down if needed - if (needSend) { - device - .getHandle() - .queue.writeBuffer( - model.UBO.getHandle(), - 0, - model.UBOData.buffer, - model.UBOData.byteOffset, - model.UBOData.byteLength - ); + const device = model.parent.getDevice(); + model.UBO.sendIfNeeded(device, device.getRendererBindGroupLayout()); } }; @@ -201,7 +145,7 @@ function vtkWebGPURenderer(publicAPI, model) { const pl = pStruct.pipeline; pl.bind(model.renderEncoder); - model.renderEncoder.setBindGroup(0, model.UBOBindGroup); + model.renderEncoder.setBindGroup(0, model.UBO.getBindGroup()); // bind our BindGroup // set viewport const tsize = publicAPI.getTiledSizeAndOrigin(); @@ -249,7 +193,7 @@ function vtkWebGPURenderer(publicAPI, model) { const pl = pStruct.pipeline; pl.bind(model.renderEncoder); - model.renderEncoder.setBindGroup(0, model.UBOBindGroup); + model.renderEncoder.setBindGroup(0, model.UBO.getBindGroup()); // bind our BindGroup // set viewport const tsize = publicAPI.getTiledSizeAndOrigin(); @@ -359,7 +303,6 @@ function vtkWebGPURenderer(publicAPI, model) { const DEFAULT_VALUES = { context: null, selector: null, - UBOData: null, renderEncoder: null, }; @@ -371,14 +314,19 @@ export function extend(publicAPI, model, initialValues = {}) { // Inheritance vtkViewNode.extend(publicAPI, model, initialValues); - model.UBOData = new Float32Array(vtkWebGPURenderUBOSize); - model.UBOUpdateTime = {}; - macro.obj(model.UBOUpdateTime); + model.UBO = vtkWebGPUUniformBuffer.newInstance(); + model.UBO.setBinding(0); + model.UBO.setGroup(0); + model.UBO.setName('rendererUBO'); + model.UBO.addEntry('WCDCMatrix', 'mat4x4'); + model.UBO.addEntry('WCVCMatrix', 'mat4x4'); + model.UBO.addEntry('VCDCMatrix', 'mat4x4'); + model.UBO.addEntry('WCVCNormals', 'mat4x4'); model.tmpMat4 = mat4.identity(new Float64Array(16)); // Build VTK API - macro.setGet(publicAPI, model, ['renderEncoder', 'selector']); + macro.setGet(publicAPI, model, ['renderEncoder', 'selector', 'UBO']); // Object methods vtkWebGPURenderer(publicAPI, model); diff --git a/Sources/Rendering/WebGPU/ShaderCache/index.js b/Sources/Rendering/WebGPU/ShaderCache/index.js index 81c3868a8ff..9612ec7bbeb 100644 --- a/Sources/Rendering/WebGPU/ShaderCache/index.js +++ b/Sources/Rendering/WebGPU/ShaderCache/index.js @@ -72,8 +72,6 @@ export function extend(publicAPI, model, initialValues = {}) { // Object methods vtkWebGPUShaderCache(publicAPI, model); - - return Object.freeze(publicAPI); } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/ShaderDescription/index.js b/Sources/Rendering/WebGPU/ShaderDescription/index.js index 258ddcb28a0..a33d0e8fd34 100644 --- a/Sources/Rendering/WebGPU/ShaderDescription/index.js +++ b/Sources/Rendering/WebGPU/ShaderDescription/index.js @@ -56,8 +56,6 @@ export function extend(publicAPI, model, initialValues = {}) { // Object methods vtkWebGPUShaderDescription(publicAPI, model); - - return Object.freeze(publicAPI); } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/Types/index.js b/Sources/Rendering/WebGPU/Types/index.js new file mode 100644 index 00000000000..22716124d7a --- /dev/null +++ b/Sources/Rendering/WebGPU/Types/index.js @@ -0,0 +1,74 @@ +// ---------------------------------------------------------------------------- +// vtkWebGPUDevice static functions +// +// WebGPU uses types in a many places and calls, and often those types +// need to be associated with byte sizes, alignments, native arrays etc. +// The folowing methods are designed to help vtk.js introspect those types. +// WebGPU currently tends to use multiple type formats: +// - buffer types such as float32x4 +// - shader types suchs as vec4 +// - texture types such as rgba32float +// ---------------------------------------------------------------------------- + +function getByteStrideFromBufferFormat(format) { + if (!format) return 0; + let numComp = 1; + if (format.substring(format.length - 2) === 'x4') numComp = 4; + if (format.substring(format.length - 2) === 'x3') numComp = 3; + if (format.substring(format.length - 2) === 'x2') numComp = 2; + + let typeSize = 4; + if (numComp > 1) { + if (format.substring(format.length - 3, format.length - 1) === '8x') { + typeSize = 1; + } + if (format.substring(format.length - 4, format.length - 1) === '16x') { + typeSize = 2; + } + } else { + if (format.substring(format.length - 1) === '8') typeSize = 1; + if (format.substring(format.length - 2) === '16') typeSize = 2; + } + return numComp * typeSize; +} + +function getNativeTypeFromBufferFormat(format) { + if (!format) return null; + if (format.substring(0, 7) === 'float32') return 'Float32Array'; + if (format.substring(0, 6) === 'snorm8') return 'Int8Array'; + if (format.substring(0, 6) === 'unorm8') return 'Uint8Array'; + return ''; +} + +function getByteStrideFromShaderFormat(format) { + if (!format) return 0; + let numComp = 1; + + if (format.substring(0, 3) === 'vec') { + numComp = format[3]; + } else if (format.substring(0, 3) === 'mat') { + numComp = format[3] * format[5]; + } + + const typeSize = 4; + return numComp * typeSize; +} + +function getNativeTypeFromShaderFormat(format) { + if (!format) return null; + let type = format.substring(format.length - 3, format.length); + if (format[format.length - 1] === '>') { + type = format.substring(format.length - 4, format.length - 1); + } + if (type === 'f32') return 'Float32Array'; + if (type === 'i32') return 'Int32Array'; + if (type === 'u32') return 'Uint32Array'; + return ''; +} + +export default { + getByteStrideFromBufferFormat, + getNativeTypeFromBufferFormat, + getByteStrideFromShaderFormat, + getNativeTypeFromShaderFormat, +}; diff --git a/Sources/Rendering/WebGPU/UniformBuffer/index.js b/Sources/Rendering/WebGPU/UniformBuffer/index.js new file mode 100644 index 00000000000..a7a6d2df195 --- /dev/null +++ b/Sources/Rendering/WebGPU/UniformBuffer/index.js @@ -0,0 +1,338 @@ +import macro from 'vtk.js/Sources/macro'; +import vtkWebGPUBufferManager from 'vtk.js/Sources/Rendering/WebGPU/BufferManager'; +import vtkWebGPUTypes from 'vtk.js/Sources/Rendering/WebGPU/Types'; + +const { BufferUsage } = vtkWebGPUBufferManager; + +const { vtkErrorMacro } = macro; + +// ---------------------------------------------------------------------------- +// vtkWebGPUUniformBuffer methods +// ---------------------------------------------------------------------------- + +function vtkWebGPUUniformBuffer(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkWebGPUUniformBuffer'); + + publicAPI.addEntry = (name, type) => { + if (model.bufferEntryNames.has(name)) { + vtkErrorMacro(`entry named ${name} already exists`); + return; + } + model.sortDirty = true; + model.bufferEntryNames.set(name, model.bufferEntries.length); + model.bufferEntries.push({ + name, + type, + sizeInBytes: vtkWebGPUTypes.getByteStrideFromShaderFormat(type), + offset: -1, + nativeType: vtkWebGPUTypes.getNativeTypeFromShaderFormat(type), + packed: false, + }); + }; + + // UBOs have layout rules in terms of how memory is aligned so we + // have to be careful how we order the entries. For example a vec4 + // must be aligned on a 16 byte offset, etc. See + // https://gpuweb.github.io/gpuweb/wgsl/#memory-layouts + // for more details. + publicAPI.sortBufferEntries = () => { + if (!model.sortDirty) { + return; + } + + let currOffset = 0; + const newEntries = []; + + // pack anything whose size is a multiple of 16 bytes first + // this includes a couple types that don't require 16 byte alignment + // such as mat2x2 but that is OK + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + if (entry.packed === false && entry.sizeInBytes % 16 === 0) { + entry.packed = true; + entry.offset = currOffset; + newEntries.push(entry); + currOffset += entry.sizeInBytes; + } + } + + // now it gets tough, we have the following common types (f32, i32, u32) + // - vec2 8 byte size, 8 byte alignment + // - vec3 12 byte size, 16 byte alignment + // - f32 4 byte size, 4 byte alignment + + // try adding 12 byte, 4 byte pairs + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + if (entry.packed === false && entry.sizeInBytes === 12) { + for (let i2 = 0; i2 < model.bufferEntries.length; i2++) { + const entry2 = model.bufferEntries[i2]; + if (entry2.packed === false && entry2.sizeInBytes === 4) { + entry.packed = true; + entry.offset = currOffset; + newEntries.push(entry); + currOffset += entry.sizeInBytes; + entry2.packed = true; + entry2.offset = currOffset; + newEntries.push(entry2); + currOffset += entry2.sizeInBytes; + break; + } + } + } + } + + // try adding 8 byte, 8 byte pairs + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + if (!entry.packed && entry.sizeInBytes % 8 === 0) { + for (let i2 = i + 1; i2 < model.bufferEntries.length; i2++) { + const entry2 = model.bufferEntries[i2]; + if (!entry2.packed && entry2.sizeInBytes % 8 === 0) { + entry.packed = true; + entry.offset = currOffset; + newEntries.push(entry); + currOffset += entry.sizeInBytes; + entry2.packed = true; + entry2.offset = currOffset; + newEntries.push(entry2); + currOffset += entry2.sizeInBytes; + break; + } + } + } + } + + // try adding 8 byte, 4 byte 4 byte triplets + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + if (!entry.packed && entry.sizeInBytes % 8 === 0) { + let found = false; + for (let i2 = 0; !found && i2 < model.bufferEntries.length; i2++) { + const entry2 = model.bufferEntries[i2]; + if (!entry2.packed && entry2.sizeInBytes === 4) { + for (let i3 = i2 + 1; i3 < model.bufferEntries.length; i3++) { + const entry3 = model.bufferEntries[i3]; + if (!entry3.packed && entry3.sizeInBytes === 4) { + entry.packed = true; + entry.offset = currOffset; + newEntries.push(entry); + currOffset += entry.sizeInBytes; + entry2.packed = true; + entry2.offset = currOffset; + newEntries.push(entry2); + currOffset += entry2.sizeInBytes; + entry3.packed = true; + entry3.offset = currOffset; + newEntries.push(entry3); + currOffset += entry3.sizeInBytes; + found = true; + break; + } + } + } + } + } + } + + // Add anything remaining that is larger than 4 bytes and hope we get lucky. + // Likely if there is more than one item added here it will result + // in a failed UBO + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + if (!entry.packed && entry.sizeInBytes > 4) { + entry.packed = true; + entry.offset = currOffset; + newEntries.push(entry); + currOffset += entry.sizeInBytes; + } + } + + // finally add remaining 4 byte items + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + if (!entry.packed) { + entry.packed = true; + entry.offset = currOffset; + newEntries.push(entry); + currOffset += entry.sizeInBytes; + } + } + + // update entries and entryNames + model.bufferEntries = newEntries; + model.bufferEntryNames.clear(); + for (let i = 0; i < model.bufferEntries.length; i++) { + model.bufferEntryNames.set(model.bufferEntries[i].name, i); + } + model.sizeInBytes = currOffset; + model.sortDirty = false; + }; + + publicAPI.sendIfNeeded = (device, layout) => { + if (!model.UBO) { + const req = { + address: model.Float32Array, + time: 0, + usage: BufferUsage.UniformArray, + }; + model.UBO = device.getBufferManager().getBuffer(req); + model.bindGroup = device.getHandle().createBindGroup({ + layout, + entries: [ + { + binding: model.binding, + resource: { + buffer: model.UBO.getHandle(), + }, + }, + ], + }); + model.sendTime.modified(); + model.sendDirty = false; + } + + // send data down if needed + if (model.sendDirty) { + device + .getHandle() + .queue.writeBuffer( + model.UBO.getHandle(), + 0, + model.arrayBuffer, + 0, + model.sizeInBytes + ); + model.sendTime.modified(); + model.sendDirty = false; + } + }; + + publicAPI.createView = (type) => { + if (type in model === false) { + if (!model.arrayBuffer) { + model.arrayBuffer = new ArrayBuffer(model.sizeInBytes); + } + model[type] = new window[type](model.arrayBuffer); + } + }; + + publicAPI.setValue = (name, val) => { + publicAPI.sortBufferEntries(); + const idx = model.bufferEntryNames.get(name); + if (idx === undefined) { + vtkErrorMacro(`entry named ${name} not found in UBO`); + return; + } + const entry = model.bufferEntries[idx]; + publicAPI.createView(entry.nativeType); + const view = model[entry.nativeType]; + if (view[entry.offset / view.BYTES_PER_ELEMENT] !== val) { + view[entry.offset / view.BYTES_PER_ELEMENT] = val; + model.sendDirty = true; + } + }; + + publicAPI.setArray = (name, arr) => { + publicAPI.sortBufferEntries(); + const idx = model.bufferEntryNames.get(name); + if (idx === undefined) { + vtkErrorMacro(`entry named ${name} not found in UBO`); + return; + } + const entry = model.bufferEntries[idx]; + publicAPI.createView(entry.nativeType); + const view = model[entry.nativeType]; + for (let i = 0; i < arr.length; i++) { + if (view[entry.offset / view.BYTES_PER_ELEMENT + i] !== arr[i]) { + view[entry.offset / view.BYTES_PER_ELEMENT + i] = arr[i]; + model.sendDirty = true; + } + } + }; + + publicAPI.getNativeView = (name) => { + publicAPI.sortBufferEntries(); + const idx = model.bufferEntryNames.get(name); + if (idx === undefined) { + vtkErrorMacro(`entry named ${name} not found in UBO`); + return undefined; + } + const entry = model.bufferEntries[idx]; + publicAPI.createView(entry.nativeType); + const bpe = model[entry.nativeType].BYTES_PER_ELEMENT; + model.sendDirty = true; + return new window[entry.nativeType]( + model.arrayBuffer, + entry.offset, + entry.sizeInBytes / bpe + ); + }; + + publicAPI.getSendTime = () => model.sendTime.getMTime(); + publicAPI.getShaderCode = () => { + // sort the entries + publicAPI.sortBufferEntries(); + + let result = `[[block]] struct ${model.name}Struct\n{\n`; + for (let i = 0; i < model.bufferEntries.length; i++) { + const entry = model.bufferEntries[i]; + result += `[[offset(${entry.offset})]] ${entry.name}: ${entry.type};\n`; + } + result += `};\n[[binding(${model.binding}), group(${model.group})]] var ${model.name}: ${model.name}Struct;`; + return result; + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + bufferEntries: null, + bufferEntryNames: null, + sizeInBytes: 0, + name: null, + binding: 0, + group: 0, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Build VTK API + macro.obj(publicAPI, model); + + // Internal objects + model.bufferEntryNames = new Map(); + model.bufferEntries = []; + + model.sendTime = {}; + macro.obj(model.sendTime, { mtime: 0 }); + + model.sendDirty = true; + model.sortDirty = true; + + macro.get(publicAPI, model, ['bindGroup']); + macro.setGet(publicAPI, model, [ + 'binding', + 'device', + 'group', + 'name', + 'sizeInBytes', + ]); + + // Object methods + vtkWebGPUUniformBuffer(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkWebGPUUniformBuffer'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend };