diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index aee25e75e19df6..b854ea1f26a3ad 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -650,6 +650,16 @@ class Renderer { */ this._compilationPromises = null; + /** + * A reusable single-element array used by `compute()` when called with + * a single compute node. Avoids allocating a fresh `[ computeNodes ]` + * array on every call, which would otherwise churn the GC each frame. + * + * @private + * @type {Array} + */ + this._computeSingleton = [ null ]; + /** * Whether the renderer should render transparent render objects or not. * @@ -2724,7 +2734,18 @@ class Renderer { const bindings = this._bindings; const nodes = this._nodes; - const computeList = Array.isArray( computeNodes ) ? computeNodes : [ computeNodes ]; + let computeList; + + if ( Array.isArray( computeNodes ) ) { + + computeList = computeNodes; + + } else { + + this._computeSingleton[ 0 ] = computeNodes; + computeList = this._computeSingleton; + + } if ( computeList[ 0 ] === undefined || computeList[ 0 ].isComputeNode !== true ) { diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 0320d99d03572c..1daa68779b7fd0 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -13,6 +13,7 @@ import WebGPUBindingUtils from './utils/WebGPUBindingUtils.js'; import WebGPUCapabilities from './utils/WebGPUCapabilities.js'; import WebGPUPipelineUtils from './utils/WebGPUPipelineUtils.js'; import WebGPUTextureUtils from './utils/WebGPUTextureUtils.js'; +import TexelCopyTextureInfo from './utils/TexelCopyTextureInfo.js'; import { WebGPUCoordinateSystem, TimestampQuery, REVISION, HalfFloatType, Compatibility } from '../../constants.js'; import WebGPUTimestampQueryPool from './utils/WebGPUTimestampQueryPool.js'; @@ -20,6 +21,51 @@ import { error } from '../../utils.js'; const _clearValue = { r: 0, g: 0, b: 0, a: 1 }; +// Pre-allocated objects to avoid per-frame GC pressure. +// WebGPU descriptors are consumed synchronously, so reuse is safe. + +const _submitArray = [ null ]; + +const _copySrc = new TexelCopyTextureInfo(); +const _copyDst = new TexelCopyTextureInfo(); +const _copySize = [ 0, 0 ]; + +const _texCopyEncoderOptions = { label: '' }; +const _texCopySrc = new TexelCopyTextureInfo(); +const _texCopyDst = new TexelCopyTextureInfo(); +const _texCopySize = [ 0, 0, 0 ]; + +const _clearEncoderOptions = { label: 'clear' }; +const _clearPassDescriptor = { colorAttachments: null, depthStencilAttachment: null }; +const _clearConfig = { loadOp: null, clearValue: null, depthLoadOp: undefined, depthClearValue: undefined, depthStoreOp: undefined, stencilLoadOp: undefined, stencilClearValue: undefined, stencilStoreOp: undefined }; + +const _timestampWrites = { querySet: null, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 0 }; + +const _indirectOffsets = [ 0 ]; +const _bundles = []; +const _singleBundleArray = [ null ]; + +const _viewDescriptor = { label: '', baseMipLevel: 0, mipLevelCount: 1, baseArrayLayer: 0, arrayLayerCount: 1, dimension: null, depthOrArrayLayers: undefined }; +const _depthViewOptions = { dimension: undefined, arrayLayerCount: undefined, baseArrayLayer: undefined }; + +function _resetCurrentSets( sets ) { + + sets.attributes.length = 0; + sets.bindingGroups.length = 0; + sets.pipeline = null; + sets.index = null; + return sets; + +} + +function _submit( queue, commandBuffer ) { + + _submitArray[ 0 ] = commandBuffer; + queue.submit( _submitArray ); + _submitArray[ 0 ] = null; + +} + /** * A backend implementation targeting WebGPU. * @@ -405,6 +451,10 @@ class WebGPUBackend extends Backend { } + // Clear properties that may have been set externally (e.g. by beginRender) + descriptor.occlusionQuerySet = undefined; + descriptor.timestampWrites = undefined; + const colorAttachment = descriptor.colorAttachments[ 0 ]; if ( samples > 0 ) { @@ -478,22 +528,21 @@ class WebGPUBackend extends Backend { const textureData = this.get( textures[ i ] ); - const viewDescriptor = { - label: `colorAttachment_${ i }`, - baseMipLevel: renderContext.activeMipmapLevel, - mipLevelCount: 1, - baseArrayLayer: renderContext.activeCubeFace, - arrayLayerCount: 1, - dimension: GPUTextureViewDimension.TwoD - }; + _viewDescriptor.label = 'colorAttachment_' + i; + _viewDescriptor.baseMipLevel = renderContext.activeMipmapLevel; + _viewDescriptor.mipLevelCount = 1; + _viewDescriptor.baseArrayLayer = renderContext.activeCubeFace; + _viewDescriptor.arrayLayerCount = 1; + _viewDescriptor.dimension = GPUTextureViewDimension.TwoD; + _viewDescriptor.depthOrArrayLayers = undefined; if ( renderTarget.isRenderTarget3D ) { sliceIndex = renderContext.activeCubeFace; - viewDescriptor.baseArrayLayer = 0; - viewDescriptor.dimension = GPUTextureViewDimension.ThreeD; - viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth; + _viewDescriptor.baseArrayLayer = 0; + _viewDescriptor.dimension = GPUTextureViewDimension.ThreeD; + _viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth; } else if ( renderTarget.isRenderTarget && textures[ i ].image.depth > 1 ) { @@ -502,13 +551,12 @@ class WebGPUBackend extends Backend { const cameras = renderContext.camera.cameras; for ( let layer = 0; layer < cameras.length; layer ++ ) { - const layerViewDescriptor = { - ...viewDescriptor, - baseArrayLayer: layer, - arrayLayerCount: 1, - dimension: GPUTextureViewDimension.TwoD - }; - const textureView = textureData.texture.createView( layerViewDescriptor ); + _viewDescriptor.baseArrayLayer = layer; + _viewDescriptor.arrayLayerCount = 1; + _viewDescriptor.dimension = GPUTextureViewDimension.TwoD; + _viewDescriptor.depthOrArrayLayers = undefined; + + const textureView = textureData.texture.createView( _viewDescriptor ); textureViews.push( { view: textureView, resolveTarget: undefined, @@ -519,8 +567,8 @@ class WebGPUBackend extends Backend { } else { - viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray; - viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth; + _viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray; + _viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth; } @@ -528,7 +576,7 @@ class WebGPUBackend extends Backend { if ( isRenderCameraDepthArray !== true ) { - const textureView = textureData.texture.createView( viewDescriptor ); + const textureView = textureData.texture.createView( _viewDescriptor ); let view, resolveTarget; @@ -559,16 +607,22 @@ class WebGPUBackend extends Backend { if ( renderContext.depth ) { const depthTextureData = this.get( renderContext.depthTexture ); - const options = {}; + + // Reset then apply only when needed so createView() sees undefined for + // unset fields rather than leftovers from a prior call. + _depthViewOptions.dimension = undefined; + _depthViewOptions.arrayLayerCount = undefined; + _depthViewOptions.baseArrayLayer = undefined; + if ( renderContext.depthTexture.isArrayTexture || renderContext.depthTexture.isCubeTexture ) { - options.dimension = GPUTextureViewDimension.TwoD; - options.arrayLayerCount = 1; - options.baseArrayLayer = renderContext.activeCubeFace; + _depthViewOptions.dimension = GPUTextureViewDimension.TwoD; + _depthViewOptions.arrayLayerCount = 1; + _depthViewOptions.baseArrayLayer = renderContext.activeCubeFace; } - descriptorBase.depthStencilView = depthTextureData.texture.createView( options ); + descriptorBase.depthStencilView = depthTextureData.texture.createView( _depthViewOptions ); } @@ -582,38 +636,58 @@ class WebGPUBackend extends Backend { } - const descriptor = { - colorAttachments: [] - }; + // Reuse or create the descriptor object and its color attachment objects + let descriptor = descriptorBase.descriptor; + + if ( descriptor === undefined ) { + + descriptor = { colorAttachments: [] }; + + if ( descriptorBase.depthStencilView ) { + + descriptor.depthStencilAttachment = { view: null }; + + } + + descriptorBase.descriptor = descriptor; + + } + + // Clear properties that may have been set externally (e.g. by beginRender) + descriptor.occlusionQuerySet = undefined; + descriptor.timestampWrites = undefined; + + const colorAttachments = descriptor.colorAttachments; + const textureViews = descriptorBase.textureViews; // Apply dynamic properties to cached views - for ( let i = 0; i < descriptorBase.textureViews.length; i ++ ) { + for ( let i = 0; i < textureViews.length; i ++ ) { + + const viewInfo = textureViews[ i ]; - const viewInfo = descriptorBase.textureViews[ i ]; + let attachment = colorAttachments[ i ]; - let clearValue = { r: 0, g: 0, b: 0, a: 1 }; - if ( i === 0 && colorAttachmentsConfig.clearValue ) { + if ( attachment === undefined ) { - clearValue = colorAttachmentsConfig.clearValue; + attachment = { view: null, depthSlice: undefined, resolveTarget: undefined, loadOp: GPULoadOp.Load, storeOp: GPUStoreOp.Store, clearValue: _clearValue }; + colorAttachments[ i ] = attachment; } - descriptor.colorAttachments.push( { - view: viewInfo.view, - depthSlice: viewInfo.depthSlice, - resolveTarget: viewInfo.resolveTarget, - loadOp: colorAttachmentsConfig.loadOp || GPULoadOp.Load, - storeOp: colorAttachmentsConfig.storeOp || GPUStoreOp.Store, - clearValue: clearValue - } ); + attachment.view = viewInfo.view; + attachment.depthSlice = viewInfo.depthSlice; + attachment.resolveTarget = viewInfo.resolveTarget; + attachment.loadOp = colorAttachmentsConfig.loadOp || GPULoadOp.Load; + attachment.storeOp = colorAttachmentsConfig.storeOp || GPUStoreOp.Store; + attachment.clearValue = ( i === 0 && colorAttachmentsConfig.clearValue ) ? colorAttachmentsConfig.clearValue : _clearValue; } + colorAttachments.length = textureViews.length; + if ( descriptorBase.depthStencilView ) { - descriptor.depthStencilAttachment = { - view: descriptorBase.depthStencilView - }; + descriptor.depthStencilAttachment.view = descriptorBase.depthStencilView; } @@ -651,7 +725,14 @@ class WebGPUBackend extends Backend { // - occlusionQuerySet = device.createQuerySet( { type: 'occlusion', count: occlusionQueryCount, label: `occlusionQuerySet_${ renderContext.id }` } ); + if ( renderContextData.occlusionQuerySetDescriptor === undefined ) { + + renderContextData.occlusionQuerySetDescriptor = { type: 'occlusion', count: 0, label: 'occlusionQuerySet_' + renderContext.id }; + + } + + renderContextData.occlusionQuerySetDescriptor.count = occlusionQueryCount; + occlusionQuerySet = device.createQuerySet( renderContextData.occlusionQuerySetDescriptor ); renderContextData.occlusionQuerySet = occlusionQuerySet; renderContextData.occlusionQueryIndex = 0; @@ -773,7 +854,13 @@ class WebGPUBackend extends Backend { // - const encoder = device.createCommandEncoder( { label: 'renderContext_' + renderContext.id } ); + if ( renderContextData.encoderOptions === undefined ) { + + renderContextData.encoderOptions = { label: 'renderContext_' + renderContext.id }; + + } + + const encoder = device.createCommandEncoder( renderContextData.encoderOptions ); // shadow arrays - prepare bundle encoders for each camera in an array camera @@ -792,8 +879,17 @@ class WebGPUBackend extends Backend { } // Create bundle encoders for each layer - renderContextData.bundleEncoders = []; - renderContextData.bundleSets = []; + + if ( renderContextData.bundleEncoders === undefined ) { + + renderContextData.bundleEncoders = []; + renderContextData.bundleSets = []; + + } else { + + renderContextData.bundleEncoders.length = 0; + + } // Create separate bundle encoders for each camera in the array for ( let i = 0; i < cameras.length; i ++ ) { @@ -803,19 +899,26 @@ class WebGPUBackend extends Backend { 'renderBundleArrayCamera_' + i ); - // Initialize state tracking for this bundle - const bundleSets = { - attributes: {}, - bindingGroups: [], - pipeline: null, - index: null - }; + // Reuse or create state tracking for this bundle + let bundleSets = renderContextData.bundleSets[ i ]; + + if ( bundleSets !== undefined ) { + + _resetCurrentSets( bundleSets ); + + } else { + + bundleSets = { attributes: [], bindingGroups: [], pipeline: null, index: null }; + renderContextData.bundleSets[ i ] = bundleSets; + + } renderContextData.bundleEncoders.push( bundleEncoder ); - renderContextData.bundleSets.push( bundleSets ); } + renderContextData.bundleSets.length = cameras.length; + // We'll complete the bundles in finishRender renderContextData.currentPass = null; @@ -842,8 +945,26 @@ class WebGPUBackend extends Backend { renderContextData.descriptor = descriptor; renderContextData.encoder = encoder; - renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null }; - renderContextData.renderBundles = []; + + if ( renderContextData.currentSets !== undefined ) { + + _resetCurrentSets( renderContextData.currentSets ); + + } else { + + renderContextData.currentSets = { attributes: [], bindingGroups: [], pipeline: null, index: null }; + + } + + if ( renderContextData.renderBundles !== undefined ) { + + renderContextData.renderBundles.length = 0; + + } else { + + renderContextData.renderBundles = []; + + } } @@ -861,7 +982,15 @@ class WebGPUBackend extends Backend { _createDepthLayerDescriptors( renderContext, renderContextData, descriptor, cameras ) { const depthStencilAttachment = descriptor.depthStencilAttachment; - renderContextData.layerDescriptors = []; + const baseColorAttachment = descriptor.colorAttachments[ 0 ]; + + if ( renderContextData.layerDescriptors === undefined ) { + + renderContextData.layerDescriptors = []; + + } + + const layerDescriptors = renderContextData.layerDescriptors; const depthTextureData = this.get( renderContext.depthTexture ); if ( ! depthTextureData.viewCache ) { @@ -872,53 +1001,94 @@ class WebGPUBackend extends Backend { for ( let i = 0; i < cameras.length; i ++ ) { - const layerDescriptor = { - ...descriptor, - colorAttachments: [ { - ...descriptor.colorAttachments[ 0 ], - view: descriptor.colorAttachments[ i ].view - } ] - }; + let layerDescriptor = layerDescriptors[ i ]; - if ( descriptor.depthStencilAttachment ) { + if ( layerDescriptor === undefined ) { - const layerIndex = i; + layerDescriptor = { colorAttachments: [ {} ] }; + layerDescriptors[ i ] = layerDescriptor; - if ( ! depthTextureData.viewCache[ layerIndex ] ) { + } - depthTextureData.viewCache[ layerIndex ] = depthTextureData.texture.createView( { - dimension: GPUTextureViewDimension.TwoD, - baseArrayLayer: i, - arrayLayerCount: 1 - } ); + // Propagate base descriptor fields that aren't color/depth-stencil. + layerDescriptor.occlusionQuerySet = descriptor.occlusionQuerySet; + layerDescriptor.timestampWrites = descriptor.timestampWrites; + + // Single color attachment per layer, with this layer's view. + const colorAttachment = layerDescriptor.colorAttachments[ 0 ]; + colorAttachment.view = descriptor.colorAttachments[ i ].view; + colorAttachment.depthSlice = baseColorAttachment.depthSlice; + colorAttachment.resolveTarget = baseColorAttachment.resolveTarget; + colorAttachment.loadOp = baseColorAttachment.loadOp; + colorAttachment.storeOp = baseColorAttachment.storeOp; + colorAttachment.clearValue = baseColorAttachment.clearValue; + + if ( depthStencilAttachment ) { + + if ( ! depthTextureData.viewCache[ i ] ) { + + _depthViewOptions.dimension = GPUTextureViewDimension.TwoD; + _depthViewOptions.baseArrayLayer = i; + _depthViewOptions.arrayLayerCount = 1; + + depthTextureData.viewCache[ i ] = depthTextureData.texture.createView( _depthViewOptions ); } - layerDescriptor.depthStencilAttachment = { - view: depthTextureData.viewCache[ layerIndex ], - depthLoadOp: depthStencilAttachment.depthLoadOp || GPULoadOp.Clear, - depthStoreOp: depthStencilAttachment.depthStoreOp || GPUStoreOp.Store, - depthClearValue: depthStencilAttachment.depthClearValue || 1.0 - }; + let dsa = layerDescriptor.depthStencilAttachment; + + if ( dsa === undefined ) { + + dsa = {}; + layerDescriptor.depthStencilAttachment = dsa; + + } + + dsa.view = depthTextureData.viewCache[ i ]; + dsa.depthLoadOp = depthStencilAttachment.depthLoadOp || GPULoadOp.Clear; + dsa.depthStoreOp = depthStencilAttachment.depthStoreOp || GPUStoreOp.Store; + dsa.depthClearValue = depthStencilAttachment.depthClearValue || 1.0; if ( renderContext.stencil ) { - layerDescriptor.depthStencilAttachment.stencilLoadOp = depthStencilAttachment.stencilLoadOp; - layerDescriptor.depthStencilAttachment.stencilStoreOp = depthStencilAttachment.stencilStoreOp; - layerDescriptor.depthStencilAttachment.stencilClearValue = depthStencilAttachment.stencilClearValue; + dsa.stencilLoadOp = depthStencilAttachment.stencilLoadOp; + dsa.stencilStoreOp = depthStencilAttachment.stencilStoreOp; + dsa.stencilClearValue = depthStencilAttachment.stencilClearValue; + + } else { + + dsa.stencilLoadOp = undefined; + dsa.stencilStoreOp = undefined; + dsa.stencilClearValue = undefined; } } else { - layerDescriptor.depthStencilAttachment = { ...depthStencilAttachment }; + // Preserve prior behavior of spreading `depthStencilAttachment` when falsy (results in an empty object). + let dsa = layerDescriptor.depthStencilAttachment; - } + if ( dsa === undefined ) { + + dsa = {}; + layerDescriptor.depthStencilAttachment = dsa; + + } else { + + for ( const key in dsa ) delete dsa[ key ]; + + } - renderContextData.layerDescriptors.push( layerDescriptor ); + } } + // Drop stale entries if cameras.length shrank. + if ( layerDescriptors.length > cameras.length ) layerDescriptors.length = cameras.length; + + // Also drop stale cached depth views beyond current camera count. + if ( depthTextureData.viewCache.length > cameras.length ) depthTextureData.viewCache.length = cameras.length; + } /** @@ -1005,18 +1175,18 @@ class WebGPUBackend extends Backend { if ( this._isRenderCameraDepthArray( renderContext ) === true ) { - const bundles = []; + _bundles.length = 0; for ( let i = 0; i < renderContextData.bundleEncoders.length; i ++ ) { const bundleEncoder = renderContextData.bundleEncoders[ i ]; - bundles.push( bundleEncoder.finish() ); + _bundles.push( bundleEncoder.finish() ); } for ( let i = 0; i < renderContextData.layerDescriptors.length; i ++ ) { - if ( i < bundles.length ) { + if ( i < _bundles.length ) { const layerDescriptor = renderContextData.layerDescriptors[ i ]; const renderPass = encoder.beginRenderPass( layerDescriptor ); @@ -1035,7 +1205,9 @@ class WebGPUBackend extends Backend { } - renderPass.executeBundles( [ bundles[ i ] ] ); + _singleBundleArray[ 0 ] = _bundles[ i ]; + renderPass.executeBundles( _singleBundleArray ); + _singleBundleArray[ 0 ] = null; renderPass.end(); @@ -1091,7 +1263,7 @@ class WebGPUBackend extends Backend { } - this.device.queue.submit( [ renderContextData.encoder.finish() ] ); + _submit( this.device.queue, renderContextData.encoder.finish() ); // @@ -1243,7 +1415,7 @@ class WebGPUBackend extends Backend { const device = this.device; const renderer = this.renderer; - let colorAttachments = []; + let colorAttachments; let depthStencilAttachment; let supportsDepth; @@ -1290,28 +1462,32 @@ class WebGPUBackend extends Backend { supportsDepth = renderTargetContext.depth; supportsStencil = renderTargetContext.stencil; - const clearConfig = { - loadOp: color ? GPULoadOp.Clear : GPULoadOp.Load, - clearValue: color ? _clearValue : undefined - }; + _clearConfig.loadOp = color ? GPULoadOp.Clear : GPULoadOp.Load; + _clearConfig.clearValue = color ? _clearValue : undefined; + _clearConfig.depthLoadOp = undefined; + _clearConfig.depthClearValue = undefined; + _clearConfig.depthStoreOp = undefined; + _clearConfig.stencilLoadOp = undefined; + _clearConfig.stencilClearValue = undefined; + _clearConfig.stencilStoreOp = undefined; if ( supportsDepth ) { - clearConfig.depthLoadOp = depth ? GPULoadOp.Clear : GPULoadOp.Load; - clearConfig.depthClearValue = depth ? renderer.getClearDepth() : undefined; - clearConfig.depthStoreOp = GPUStoreOp.Store; + _clearConfig.depthLoadOp = depth ? GPULoadOp.Clear : GPULoadOp.Load; + _clearConfig.depthClearValue = depth ? renderer.getClearDepth() : undefined; + _clearConfig.depthStoreOp = GPUStoreOp.Store; } if ( supportsStencil ) { - clearConfig.stencilLoadOp = stencil ? GPULoadOp.Clear : GPULoadOp.Load; - clearConfig.stencilClearValue = stencil ? renderer.getClearStencil() : undefined; - clearConfig.stencilStoreOp = GPUStoreOp.Store; + _clearConfig.stencilLoadOp = stencil ? GPULoadOp.Clear : GPULoadOp.Load; + _clearConfig.stencilClearValue = stencil ? renderer.getClearStencil() : undefined; + _clearConfig.stencilStoreOp = GPUStoreOp.Store; } - const descriptor = this._getRenderPassDescriptor( renderTargetContext, clearConfig ); + const descriptor = this._getRenderPassDescriptor( renderTargetContext, _clearConfig ); colorAttachments = descriptor.colorAttachments; depthStencilAttachment = descriptor.depthStencilAttachment; @@ -1356,15 +1532,16 @@ class WebGPUBackend extends Backend { // - const encoder = device.createCommandEncoder( { label: 'clear' } ); - const currentPass = encoder.beginRenderPass( { - colorAttachments, - depthStencilAttachment - } ); + const encoder = device.createCommandEncoder( _clearEncoderOptions ); + _clearPassDescriptor.colorAttachments = colorAttachments; + _clearPassDescriptor.depthStencilAttachment = depthStencilAttachment; + const currentPass = encoder.beginRenderPass( _clearPassDescriptor ); + _clearPassDescriptor.colorAttachments = null; + _clearPassDescriptor.depthStencilAttachment = null; currentPass.end(); - device.queue.submit( [ encoder.finish() ] ); + _submit( device.queue, encoder.finish() ); } @@ -1379,18 +1556,36 @@ class WebGPUBackend extends Backend { beginCompute( computeGroup ) { const groupGPU = this.get( computeGroup ); + const traceComputeSteps = groupGPU.traceComputeSteps === true; - // + if ( groupGPU.encoderOptions === undefined ) { - const descriptor = { - label: 'computeGroup_' + computeGroup.id - }; + const isComputeGroup = Array.isArray( computeGroup ) === true; + const computeGroupId = isComputeGroup ? computeGroup.map( computeNode => computeNode.id ).join( '_' ) : computeGroup.id; + const label = 'computeGroup_' + computeGroupId; + + groupGPU.encoderOptions = { label }; + groupGPU.passDescriptor = { label }; + groupGPU.isComputeGroup = isComputeGroup; + + } - this.initTimestampQuery( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ), descriptor ); + groupGPU.cmdEncoderGPU = this.device.createCommandEncoder( groupGPU.encoderOptions ); + groupGPU.passEncoderGPU = null; + groupGPU.currentPipeline = null; - groupGPU.cmdEncoderGPU = this.device.createCommandEncoder( { label: 'computeGroup_' + computeGroup.id } ); + if ( groupGPU.isComputeGroup === false || traceComputeSteps === false ) { - groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass( descriptor ); + const descriptor = groupGPU.passDescriptor; + + // Clear properties that may have been set by initTimestampQuery on a previous call. + descriptor.timestampWrites = undefined; + + this.initTimestampQuery( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ), descriptor ); + + groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass( descriptor ); + + } } @@ -1409,13 +1604,19 @@ class WebGPUBackend extends Backend { compute( computeGroup, computeNode, bindings, pipeline, dispatchSize = null ) { const computeNodeData = this.get( computeNode ); - const { passEncoderGPU } = this.get( computeGroup ); + const groupData = this.get( computeGroup ); + const { passEncoderGPU } = groupData; // pipeline const pipelineGPU = this.get( pipeline ).pipeline; - this.pipelineUtils.setPipeline( passEncoderGPU, pipelineGPU ); + if ( groupData.currentPipeline !== pipelineGPU ) { + + passEncoderGPU.setPipeline( pipelineGPU ); + groupData.currentPipeline = pipelineGPU; + + } // bind groups @@ -1509,9 +1710,14 @@ class WebGPUBackend extends Backend { const groupData = this.get( computeGroup ); - groupData.passEncoderGPU.end(); + if ( groupData.passEncoderGPU !== null ) { - this.device.queue.submit( [ groupData.cmdEncoderGPU.finish() ] ); + groupData.passEncoderGPU.end(); + groupData.passEncoderGPU = null; + + } + + _submit( this.device.queue, groupData.cmdEncoderGPU.finish() ); } @@ -1537,8 +1743,12 @@ class WebGPUBackend extends Backend { const hasIndex = ( index !== null ); // pipeline - this.pipelineUtils.setPipeline( passEncoderGPU, pipelineGPU ); - currentSets.pipeline = pipelineGPU; + if ( currentSets.pipeline !== pipelineGPU ) { + + passEncoderGPU.setPipeline( pipelineGPU ); + currentSets.pipeline = pipelineGPU; + + } // bind groups const currentBindingGroups = currentSets.bindingGroups; @@ -1637,7 +1847,18 @@ class WebGPUBackend extends Backend { const buffer = this.get( indirect ).buffer; const indirectOffset = renderObject.getIndirectOffset(); - const indirectOffsets = Array.isArray( indirectOffset ) ? indirectOffset : [ indirectOffset ]; + let indirectOffsets; + + if ( Array.isArray( indirectOffset ) ) { + + indirectOffsets = indirectOffset; + + } else { + + _indirectOffsets[ 0 ] = indirectOffset; + indirectOffsets = _indirectOffsets; + + } for ( let i = 0; i < indirectOffsets.length; i ++ ) { @@ -1663,7 +1884,18 @@ class WebGPUBackend extends Backend { const buffer = this.get( indirect ).buffer; const indirectOffset = renderObject.getIndirectOffset(); - const indirectOffsets = Array.isArray( indirectOffset ) ? indirectOffset : [ indirectOffset ]; + let indirectOffsets; + + if ( Array.isArray( indirectOffset ) ) { + + indirectOffsets = indirectOffset; + + } else { + + _indirectOffsets[ 0 ] = indirectOffset; + indirectOffsets = _indirectOffsets; + + } for ( let i = 0; i < indirectOffsets.length; i ++ ) { @@ -2044,11 +2276,14 @@ class WebGPUBackend extends Backend { const baseOffset = timestampQueryPool.allocateQueriesForContext( uid ); - descriptor.timestampWrites = { - querySet: timestampQueryPool.querySet, - beginningOfPassWriteIndex: baseOffset, - endOfPassWriteIndex: baseOffset + 1, - }; + if ( baseOffset !== null ) { + + _timestampWrites.querySet = timestampQueryPool.querySet; + _timestampWrites.beginningOfPassWriteIndex = baseOffset; + _timestampWrites.endOfPassWriteIndex = baseOffset + 1; + descriptor.timestampWrites = _timestampWrites; + + } } @@ -2135,7 +2370,17 @@ class WebGPUBackend extends Backend { renderContextData._currentPass = renderContextData.currentPass; renderContextData._currentSets = renderContextData.currentSets; - renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null }; + if ( renderContextData._bundleCurrentSets !== undefined ) { + + _resetCurrentSets( renderContextData._bundleCurrentSets ); + + } else { + + renderContextData._bundleCurrentSets = { attributes: [], bindingGroups: [], pipeline: null, index: null }; + + } + + renderContextData.currentSets = renderContextData._bundleCurrentSets; renderContextData.currentPass = this.pipelineUtils.createBundleEncoder( renderContext ); } @@ -2389,30 +2634,24 @@ class WebGPUBackend extends Backend { } - const encoder = this.device.createCommandEncoder( { label: 'copyTextureToTexture_' + srcTexture.id + '_' + dstTexture.id } ); + _texCopyEncoderOptions.label = 'copyTextureToTexture_' + srcTexture.id + '_' + dstTexture.id; + const encoder = this.device.createCommandEncoder( _texCopyEncoderOptions ); const sourceGPU = this.get( srcTexture ).texture; const destinationGPU = this.get( dstTexture ).texture; - encoder.copyTextureToTexture( - { - texture: sourceGPU, - mipLevel: srcLevel, - origin: { x: srcX, y: srcY, z: srcZ } - }, - { - texture: destinationGPU, - mipLevel: dstLevel, - origin: { x: dstX, y: dstY, z: dstZ } - }, - [ - srcWidth, - srcHeight, - srcDepth - ] - ); + _texCopySrc.setTexture( sourceGPU ).setMipLevel( srcLevel ).setOrigin( srcX, srcY, srcZ ); + _texCopyDst.setTexture( destinationGPU ).setMipLevel( dstLevel ).setOrigin( dstX, dstY, dstZ ); + _texCopySize[ 0 ] = srcWidth; + _texCopySize[ 1 ] = srcHeight; + _texCopySize[ 2 ] = srcDepth; - this.device.queue.submit( [ encoder.finish() ] ); + encoder.copyTextureToTexture( _texCopySrc, _texCopyDst, _texCopySize ); + + _texCopySrc.reset(); + _texCopyDst.reset(); + + _submit( this.device.queue, encoder.finish() ); if ( dstLevel === 0 && dstTexture.generateMipmaps ) { @@ -2481,23 +2720,27 @@ class WebGPUBackend extends Backend { } else { - encoder = this.device.createCommandEncoder( { label: 'copyFramebufferToTexture_' + texture.id } ); + const textureData = this.get( texture ); + + if ( textureData.copyEncoderOptions === undefined ) { + + textureData.copyEncoderOptions = { label: 'copyFramebufferToTexture_' + texture.id }; + + } + + encoder = this.device.createCommandEncoder( textureData.copyEncoderOptions ); } - encoder.copyTextureToTexture( - { - texture: sourceGPU, - origin: [ rectangle.x, rectangle.y, 0 ], - }, - { - texture: destinationGPU - }, - [ - rectangle.z, - rectangle.w - ] - ); + _copySrc.setTexture( sourceGPU ).setOrigin( rectangle.x, rectangle.y, 0 ); + _copyDst.setTexture( destinationGPU ); + _copySize[ 0 ] = rectangle.z; + _copySize[ 1 ] = rectangle.w; + + encoder.copyTextureToTexture( _copySrc, _copyDst, _copySize ); + + _copySrc.reset(); + _copyDst.reset(); // mipmaps must be genereated with the same encoder otherwise the copied texture data // might be out-of-sync, see #31768 @@ -2522,7 +2765,7 @@ class WebGPUBackend extends Backend { if ( renderContext.stencil ) descriptor.depthStencilAttachment.stencilLoadOp = GPULoadOp.Load; renderContextData.currentPass = encoder.beginRenderPass( descriptor ); - renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null }; + _resetCurrentSets( renderContextData.currentSets ); if ( renderContext.viewport ) { @@ -2538,7 +2781,7 @@ class WebGPUBackend extends Backend { } else { - this.device.queue.submit( [ encoder.finish() ] ); + _submit( this.device.queue, encoder.finish() ); } diff --git a/src/renderers/webgpu/utils/TexelCopyTextureInfo.js b/src/renderers/webgpu/utils/TexelCopyTextureInfo.js new file mode 100644 index 00000000000000..0780832b7c6aae --- /dev/null +++ b/src/renderers/webgpu/utils/TexelCopyTextureInfo.js @@ -0,0 +1,93 @@ +/** + * Reusable wrapper around `GPUTexelCopyTextureInfo`. Mutates in place to avoid + * per-frame GC pressure while keeping call sites readable via chainable setters. + * + * @private + */ +class TexelCopyTextureInfo { + + /** + * Constructs a new texel-copy texture info. + */ + constructor() { + + /** + * The source/destination GPU texture. + * + * @type {?GPUTexture} + * @default null + */ + this.texture = null; + + /** + * The mip level of the texture to copy from/to. + * + * @type {number} + * @default 0 + */ + this.mipLevel = 0; + + /** + * The origin of the copy within the texture. + * + * @type {{x: number, y: number, z: number}} + */ + this.origin = { x: 0, y: 0, z: 0 }; + + } + + /** + * Sets the texture. + * + * @param {GPUTexture} texture - The GPU texture. + * @return {TexelCopyTextureInfo} A reference to this instance. + */ + setTexture( texture ) { + + this.texture = texture; + return this; + + } + + /** + * Sets the mip level. + * + * @param {number} mipLevel - The mip level. + * @return {TexelCopyTextureInfo} A reference to this instance. + */ + setMipLevel( mipLevel ) { + + this.mipLevel = mipLevel; + return this; + + } + + /** + * Sets the origin of the copy. + * + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @param {number} z - The z coordinate. + * @return {TexelCopyTextureInfo} A reference to this instance. + */ + setOrigin( x, y, z ) { + + this.origin.x = x; + this.origin.y = y; + this.origin.z = z; + return this; + + } + + /** + * Clears the texture reference so the scratch doesn't retain a GPU object across frames. + */ + reset() { + + this.texture = null; + + } + +} + +export default TexelCopyTextureInfo; diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js index 75a947b416e45d..1bc1bbdf7fd1be 100644 --- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -38,35 +38,6 @@ class WebGPUPipelineUtils { */ this.backend = backend; - /** - * A Weak Map that tracks the active pipeline for render or compute passes. - * - * @private - * @type {WeakMap<(GPURenderPassEncoder|GPUComputePassEncoder),(GPURenderPipeline|GPUComputePipeline)>} - */ - this._activePipelines = new WeakMap(); - - } - - /** - * Sets the given pipeline for the given pass. The method makes sure to only set the - * pipeline when necessary. - * - * @param {(GPURenderPassEncoder|GPUComputePassEncoder)} pass - The pass encoder. - * @param {(GPURenderPipeline|GPUComputePipeline)} pipeline - The pipeline. - */ - setPipeline( pass, pipeline ) { - - const currentPipeline = this._activePipelines.get( pass ); - - if ( currentPipeline !== pipeline ) { - - pass.setPipeline( pipeline ); - - this._activePipelines.set( pass, pipeline ); - - } - } /** diff --git a/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js b/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js index 89ceaeadf66776..d33e66fc924055 100644 --- a/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +++ b/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js @@ -1,6 +1,8 @@ import { error, warnOnce } from '../../../utils.js'; import TimestampQueryPool from '../../common/TimestampQueryPool.js'; +const _submitArray = [ null ]; + /** * Manages a pool of WebGPU timestamp queries for performance measurement. * Extends the base TimestampQueryPool to provide WebGPU-specific implementation. @@ -154,8 +156,9 @@ class WebGPUTimestampQueryPool extends TimestampQueryPool { bytesUsed ); - const commandBuffer = commandEncoder.finish(); - this.device.queue.submit( [ commandBuffer ] ); + _submitArray[ 0 ] = commandEncoder.finish(); + this.device.queue.submit( _submitArray ); + _submitArray[ 0 ] = null; if ( this.resultBuffer.mapState !== 'unmapped' ) {