diff --git a/examples/jsm/postprocessing/UnrealBloomPass.js b/examples/jsm/postprocessing/UnrealBloomPass.js index 6879db6386d20e..decd2eb38d41ea 100644 --- a/examples/jsm/postprocessing/UnrealBloomPass.js +++ b/examples/jsm/postprocessing/UnrealBloomPass.js @@ -56,7 +56,7 @@ class UnrealBloomPass extends Pass { this.strength = strength; /** - * The Bloom radius. + * The Bloom radius. Must be in the range `[0,1]`. * * @type {number} */ diff --git a/examples/jsm/tsl/display/BloomNode.js b/examples/jsm/tsl/display/BloomNode.js index f890222867a637..8cd9a9af1fb86c 100644 --- a/examples/jsm/tsl/display/BloomNode.js +++ b/examples/jsm/tsl/display/BloomNode.js @@ -77,7 +77,7 @@ class BloomNode extends TempNode { this.strength = uniform( strength ); /** - * The radius of the bloom. + * The radius of the bloom. Must be in the range `[0,1]`. * * @type {UniformNode} */ @@ -443,6 +443,15 @@ class BloomNode extends TempNode { this._renderTargetBright.dispose(); + if ( this._highPassFilterMaterial !== null ) this._highPassFilterMaterial.dispose(); + if ( this._compositeMaterial !== null ) this._compositeMaterial.dispose(); + + for ( let i = 0; i < this._separableBlurMaterials.length; i ++ ) { + + this._separableBlurMaterials[ i ].dispose(); + + } + } /** diff --git a/examples/screenshots/webgl_loader_texture_ktx.jpg b/examples/screenshots/webgl_loader_texture_ktx.jpg index d80f53efd67555..e9fffb50c6324b 100644 Binary files a/examples/screenshots/webgl_loader_texture_ktx.jpg and b/examples/screenshots/webgl_loader_texture_ktx.jpg differ diff --git a/examples/textures/compressed/normal.bc5.ktx b/examples/textures/compressed/normal.bc5.ktx new file mode 100644 index 00000000000000..86a16842a3be4c Binary files /dev/null and b/examples/textures/compressed/normal.bc5.ktx differ diff --git a/examples/textures/compressed/normal.eac_rg.ktx b/examples/textures/compressed/normal.eac_rg.ktx new file mode 100644 index 00000000000000..cbd302e9469961 Binary files /dev/null and b/examples/textures/compressed/normal.eac_rg.ktx differ diff --git a/examples/webgl_loader_texture_ktx.html b/examples/webgl_loader_texture_ktx.html index ff453a207bb529..62ccc2ec5b5e53 100644 --- a/examples/webgl_loader_texture_ktx.html +++ b/examples/webgl_loader_texture_ktx.html @@ -58,6 +58,7 @@ const formats = { astc: renderer.extensions.has( 'WEBGL_compressed_texture_astc' ), etc1: renderer.extensions.has( 'WEBGL_compressed_texture_etc1' ), + etc2: renderer.extensions.has( 'WEBGL_compressed_texture_etc' ), s3tc: renderer.extensions.has( 'WEBGL_compressed_texture_s3tc' ), pvrtc: renderer.extensions.has( 'WEBGL_compressed_texture_pvrtc' ) }; @@ -67,8 +68,15 @@ scene = new THREE.Scene(); + const ambientLight = new THREE.AmbientLight( 0xffffff, 0.02 ); + scene.add( ambientLight ); + + const pointLight = new THREE.PointLight( 0xffffff, 2, 0, 0 ); + pointLight.position.z = - 300; + scene.add( pointLight ); + const geometry = new THREE.BoxGeometry( 200, 200, 200 ); - let material1, material2; + let material1, material2, material3; // TODO: add cubemap support const loader = new KTXLoader(); @@ -105,9 +113,13 @@ side: THREE.DoubleSide } ); material2.map.colorSpace = THREE.SRGBColorSpace; + material3 = new THREE.MeshStandardMaterial( { + normalMap: loader.load( 'textures/compressed/normal.bc5.ktx' ) + } ); meshes.push( new THREE.Mesh( geometry, material1 ) ); meshes.push( new THREE.Mesh( geometry, material2 ) ); + meshes.push( new THREE.Mesh( geometry, material3 ) ); } @@ -121,6 +133,16 @@ } + if ( formats.etc2 ) { + + material1 = new THREE.MeshStandardMaterial( { + normalMap: loader.load( 'textures/compressed/normal.eac_rg.ktx' ) + } ); + + meshes.push( new THREE.Mesh( geometry, material1 ) ); + + } + if ( formats.astc ) { material1 = new THREE.MeshBasicMaterial( { @@ -140,12 +162,22 @@ } - let x = - meshes.length / 2 * 150; - for ( let i = 0; i < meshes.length; ++ i, x += 300 ) { + let x0 = - Math.min( 4, meshes.length ) * 300 / 2 + 150; + for ( let i = 0; i < Math.min( 4, meshes.length ); ++ i, x0 += 300 ) { + + const mesh = meshes[ i ]; + mesh.position.x = x0; + mesh.position.y = 150; + scene.add( mesh ); + + } + + let x1 = - ( meshes.length - 4 ) * 300 / 2 + 150; + for ( let i = 4; i < meshes.length; ++ i, x1 += 300 ) { const mesh = meshes[ i ]; - mesh.position.x = x; - mesh.position.y = 0; + mesh.position.x = x1; + mesh.position.y = - 150; scene.add( mesh ); } diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index ff616efe8e7fc3..c7405e82dc5ea9 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -736,6 +736,14 @@ class Backend { } + /** + * Delete GPU data associated with a bind group. + * + * @abstract + * @param {BindGroup} bindGroup - The bind group. + */ + deleteBindGroupData( /*bindGroup*/ ) { } + /** * Frees internal resources. * diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js index 89339b20f078f5..f7675a18046e38 100644 --- a/src/renderers/common/Bindings.js +++ b/src/renderers/common/Bindings.js @@ -164,6 +164,7 @@ class Bindings extends DataMap { for ( const bindGroup of bindings ) { + this.backend.deleteBindGroupData( bindGroup ); this.delete( bindGroup ); } @@ -181,6 +182,7 @@ class Bindings extends DataMap { for ( const bindGroup of bindings ) { + this.backend.deleteBindGroupData( bindGroup ); this.delete( bindGroup ); } diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 07ff008e9a127e..01afbc472e6a25 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -204,6 +204,8 @@ class WebGPUBackend extends Backend { device.lost.then( ( info ) => { + if ( info.reason === 'destroyed' ) return; + const deviceLossInfo = { api: 'WebGPU', message: info.message || 'Unknown reason', @@ -1665,7 +1667,9 @@ class WebGPUBackend extends Backend { data[ 0 ] = i; - const bindGroupIndex = this.bindingUtils.createBindGroupIndex( data, bindingsData.layout ); + const { layoutGPU } = bindingsData.layout; + + const bindGroupIndex = this.bindingUtils.createBindGroupIndex( data, layoutGPU ); indexesGPU.push( bindGroupIndex ); @@ -2157,6 +2161,17 @@ class WebGPUBackend extends Backend { } + /** + * Delete data associated with the current bind group. + * + * @param {BindGroup} bindGroup - The bind group. + */ + deleteBindGroupData( bindGroup ) { + + this.bindingUtils.deleteBindGroupData( bindGroup ); + + } + // attributes /** @@ -2486,8 +2501,37 @@ class WebGPUBackend extends Backend { dispose() { + this.bindingUtils.dispose(); this.textureUtils.dispose(); + if ( this.occludedResolveCache ) { + + for ( const buffer of this.occludedResolveCache.values() ) { + + buffer.destroy(); + + } + + this.occludedResolveCache.clear(); + + } + + if ( this.timestampQueryPool ) { + + for ( const queryPool of Object.values( this.timestampQueryPool ) ) { + + if ( queryPool !== null ) queryPool.dispose(); + + } + + } + + if ( this.parameters.device === undefined && this.device !== null ) { + + this.device.destroy(); + + } + } } diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js index cfe8db92f7f2bf..b425c1a1536bc6 100644 --- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js +++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js @@ -7,6 +7,38 @@ import { FloatType, IntType, UnsignedIntType } from '../../../constants.js'; import { NodeAccess } from '../../../nodes/core/constants.js'; import { isTypedArray, error } from '../../../utils.js'; +/** + * Class representing a WebGPU bind group layout. + * + * @private + */ +class BindGroupLayout { + + /** + * Constructs a new layout. + * + * @param {GPUBindGroupLayout} layoutGPU - A GPU Bind Group Layout. + */ + constructor( layoutGPU ) { + + /** + * The current GPUBindGroupLayout. + * + * @type {GPUBindGroupLayout} + */ + this.layoutGPU = layoutGPU; + + /** + * The number of bind groups that use this layout. + * + * @type {number} + */ + this.usedTimes = 0; + + } + +} + /** * A WebGPU backend utility module for managing bindings. * @@ -34,11 +66,12 @@ class WebGPUBindingUtils { this.backend = backend; /** - * A cache for managing bind group layouts. + * A cache that maps combinations of layout entries to existing bind group layouts. * - * @type {WeakMap,GPUBindGroupLayout>} + * @private + * @type {Map} */ - this.bindGroupLayoutCache = new WeakMap(); + this._bindGroupLayoutCache = new Map(); } @@ -53,185 +86,40 @@ class WebGPUBindingUtils { const backend = this.backend; const device = backend.device; - const entries = []; - - let index = 0; - - for ( const binding of bindGroup.bindings ) { - - const bindingGPU = { - binding: index ++, - visibility: binding.visibility - }; - - if ( binding.isUniformBuffer || binding.isStorageBuffer ) { - - const buffer = {}; // GPUBufferBindingLayout - - if ( binding.isStorageBuffer ) { - - if ( binding.visibility & GPUShaderStage.COMPUTE ) { - - // compute - - if ( binding.access === NodeAccess.READ_WRITE || binding.access === NodeAccess.WRITE_ONLY ) { - - buffer.type = GPUBufferBindingType.Storage; - - } else { - - buffer.type = GPUBufferBindingType.ReadOnlyStorage; - - } - - } else { - - buffer.type = GPUBufferBindingType.ReadOnlyStorage; - - } - - } - - bindingGPU.buffer = buffer; - - } else if ( binding.isSampledTexture && binding.store ) { - - const storageTexture = {}; // GPUStorageTextureBindingLayout - storageTexture.format = this.backend.get( binding.texture ).texture.format; - - const access = binding.access; - - if ( access === NodeAccess.READ_WRITE ) { - - storageTexture.access = GPUStorageTextureAccess.ReadWrite; - - } else if ( access === NodeAccess.WRITE_ONLY ) { - - storageTexture.access = GPUStorageTextureAccess.WriteOnly; - - } else { - - storageTexture.access = GPUStorageTextureAccess.ReadOnly; - - } - - if ( binding.texture.isArrayTexture ) { - - storageTexture.viewDimension = GPUTextureViewDimension.TwoDArray; - - } else if ( binding.texture.is3DTexture ) { - - storageTexture.viewDimension = GPUTextureViewDimension.ThreeD; - - } - - bindingGPU.storageTexture = storageTexture; - - } else if ( binding.isSampledTexture ) { - - const texture = {}; // GPUTextureBindingLayout - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - - if ( primarySamples > 1 ) { - - texture.multisampled = true; - - if ( ! binding.texture.isDepthTexture ) { - - texture.sampleType = GPUTextureSampleType.UnfilterableFloat; - - } - - } - - if ( binding.texture.isDepthTexture ) { - - if ( backend.compatibilityMode && binding.texture.compareFunction === null ) { - - texture.sampleType = GPUTextureSampleType.UnfilterableFloat; - - } else { - - texture.sampleType = GPUTextureSampleType.Depth; - - } - - } else if ( binding.texture.isDataTexture || binding.texture.isDataArrayTexture || binding.texture.isData3DTexture ) { - - const type = binding.texture.type; - - if ( type === IntType ) { - - texture.sampleType = GPUTextureSampleType.SInt; - - } else if ( type === UnsignedIntType ) { - - texture.sampleType = GPUTextureSampleType.UInt; - - } else if ( type === FloatType ) { - - if ( this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - - } else { - - texture.sampleType = GPUTextureSampleType.UnfilterableFloat; - - } - - } - - } - - if ( binding.isSampledCubeTexture ) { - - texture.viewDimension = GPUTextureViewDimension.Cube; - - } else if ( binding.texture.isArrayTexture || binding.texture.isDataArrayTexture || binding.texture.isCompressedArrayTexture ) { - - texture.viewDimension = GPUTextureViewDimension.TwoDArray; - - } else if ( binding.isSampledTexture3D ) { - - texture.viewDimension = GPUTextureViewDimension.ThreeD; - - } - - bindingGPU.texture = texture; - - } else if ( binding.isSampler ) { + const bindingsData = backend.get( bindGroup ); - const sampler = {}; // GPUSamplerBindingLayout + // check if the the bind group already has a layout - if ( binding.texture.isDepthTexture ) { + if ( bindingsData.layout ) { - if ( binding.texture.compareFunction !== null ) { + return bindingsData.layout.layoutGPU; - sampler.type = GPUSamplerBindingType.Comparison; + } - } else if ( backend.compatibilityMode ) { + // if not, assing one - sampler.type = GPUSamplerBindingType.NonFiltering; + const entries = this._createLayoutEntries( bindGroup ); + const bindGroupLayoutKey = JSON.stringify( entries ); - } + // try to find an existing layout in the cache - } + let bindGroupLayout = this._bindGroupLayoutCache.get( bindGroupLayoutKey ); - bindingGPU.sampler = sampler; + // if not create a new one - } else { + if ( bindGroupLayout === undefined ) { - error( `WebGPUBindingUtils: Unsupported binding "${ binding }".` ); + bindGroupLayout = new BindGroupLayout( device.createBindGroupLayout( { entries } ) ); + this._bindGroupLayoutCache.set( bindGroupLayoutKey, bindGroupLayout ); - } + } - entries.push( bindingGPU ); + bindGroupLayout.usedTimes ++; - } + bindingsData.layout = bindGroupLayout; + bindingsData.layoutKey = bindGroupLayoutKey; - return device.createBindGroupLayout( { entries } ); + return bindGroupLayout.layoutGPU; } @@ -245,19 +133,12 @@ class WebGPUBindingUtils { */ createBindings( bindGroup, bindings, cacheIndex, version = 0 ) { - const { backend, bindGroupLayoutCache } = this; + const { backend } = this; const bindingsData = backend.get( bindGroup ); // setup (static) binding layout and (dynamic) binding group - let bindLayoutGPU = bindGroupLayoutCache.get( bindGroup.bindingsReference ); - - if ( bindLayoutGPU === undefined ) { - - bindLayoutGPU = this.createBindingsLayout( bindGroup ); - bindGroupLayoutCache.set( bindGroup.bindingsReference, bindLayoutGPU ); - - } + const bindLayoutGPU = this.createBindingsLayout( bindGroup ); let bindGroupGPU; @@ -292,7 +173,6 @@ class WebGPUBindingUtils { } bindingsData.group = bindGroupGPU; - bindingsData.layout = bindLayoutGPU; } @@ -354,10 +234,10 @@ class WebGPUBindingUtils { * Creates a GPU bind group for the camera index. * * @param {Uint32Array} data - The index data. - * @param {GPUBindGroupLayout} layout - The GPU bind group layout. + * @param {GPUBindGroupLayout} layoutGPU - The GPU bind group layout. * @return {GPUBindGroup} The GPU bind group. */ - createBindGroupIndex( data, layout ) { + createBindGroupIndex( data, layoutGPU ) { const backend = this.backend; const device = backend.device; @@ -377,7 +257,7 @@ class WebGPUBindingUtils { return device.createBindGroup( { label: 'bindGroupCameraIndex_' + index, - layout, + layout: layoutGPU, entries } ); @@ -538,6 +418,236 @@ class WebGPUBindingUtils { } + /** + * Creates a GPU bind group layout entries for the given bind group. + * + * @private + * @param {BindGroup} bindGroup - The bind group. + * @return {Array} The GPU bind group layout entries. + */ + _createLayoutEntries( bindGroup ) { + + const entries = []; + let index = 0; + + for ( const binding of bindGroup.bindings ) { + + const backend = this.backend; + + const bindingGPU = { + binding: index, + visibility: binding.visibility + }; + + if ( binding.isUniformBuffer || binding.isStorageBuffer ) { + + const buffer = {}; // GPUBufferBindingLayout + + if ( binding.isStorageBuffer ) { + + if ( binding.visibility & GPUShaderStage.COMPUTE ) { + + // compute + + if ( binding.access === NodeAccess.READ_WRITE || binding.access === NodeAccess.WRITE_ONLY ) { + + buffer.type = GPUBufferBindingType.Storage; + + } else { + + buffer.type = GPUBufferBindingType.ReadOnlyStorage; + + } + + } else { + + buffer.type = GPUBufferBindingType.ReadOnlyStorage; + + } + + } + + bindingGPU.buffer = buffer; + + } else if ( binding.isSampledTexture && binding.store ) { + + const storageTexture = {}; // GPUStorageTextureBindingLayout + storageTexture.format = this.backend.get( binding.texture ).texture.format; + + const access = binding.access; + + if ( access === NodeAccess.READ_WRITE ) { + + storageTexture.access = GPUStorageTextureAccess.ReadWrite; + + } else if ( access === NodeAccess.WRITE_ONLY ) { + + storageTexture.access = GPUStorageTextureAccess.WriteOnly; + + } else { + + storageTexture.access = GPUStorageTextureAccess.ReadOnly; + + } + + if ( binding.texture.isArrayTexture ) { + + storageTexture.viewDimension = GPUTextureViewDimension.TwoDArray; + + } else if ( binding.texture.is3DTexture ) { + + storageTexture.viewDimension = GPUTextureViewDimension.ThreeD; + + } + + bindingGPU.storageTexture = storageTexture; + + } else if ( binding.isSampledTexture ) { + + const texture = {}; // GPUTextureBindingLayout + + const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); + + if ( primarySamples > 1 ) { + + texture.multisampled = true; + + if ( ! binding.texture.isDepthTexture ) { + + texture.sampleType = GPUTextureSampleType.UnfilterableFloat; + + } + + } + + if ( binding.texture.isDepthTexture ) { + + if ( backend.compatibilityMode && binding.texture.compareFunction === null ) { + + texture.sampleType = GPUTextureSampleType.UnfilterableFloat; + + } else { + + texture.sampleType = GPUTextureSampleType.Depth; + + } + + } else if ( binding.texture.isDataTexture || binding.texture.isDataArrayTexture || binding.texture.isData3DTexture ) { + + const type = binding.texture.type; + + if ( type === IntType ) { + + texture.sampleType = GPUTextureSampleType.SInt; + + } else if ( type === UnsignedIntType ) { + + texture.sampleType = GPUTextureSampleType.UInt; + + } else if ( type === FloatType ) { + + if ( this.backend.hasFeature( 'float32-filterable' ) ) { + + texture.sampleType = GPUTextureSampleType.Float; + + } else { + + texture.sampleType = GPUTextureSampleType.UnfilterableFloat; + + } + + } + + } + + if ( binding.isSampledCubeTexture ) { + + texture.viewDimension = GPUTextureViewDimension.Cube; + + } else if ( binding.texture.isArrayTexture || binding.texture.isDataArrayTexture || binding.texture.isCompressedArrayTexture ) { + + texture.viewDimension = GPUTextureViewDimension.TwoDArray; + + } else if ( binding.isSampledTexture3D ) { + + texture.viewDimension = GPUTextureViewDimension.ThreeD; + + } + + bindingGPU.texture = texture; + + } else if ( binding.isSampler ) { + + const sampler = {}; // GPUSamplerBindingLayout + + if ( binding.texture.isDepthTexture ) { + + if ( binding.texture.compareFunction !== null ) { + + sampler.type = GPUSamplerBindingType.Comparison; + + } else if ( backend.compatibilityMode ) { + + sampler.type = GPUSamplerBindingType.NonFiltering; + + } + + } + + bindingGPU.sampler = sampler; + + } else { + + error( `WebGPUBindingUtils: Unsupported binding "${ binding }".` ); + + } + + entries.push( bindingGPU ); + index ++; + + } + + return entries; + + } + + /** + * Delete the data associated with a bind group. + * + * @param {BindGroup} bindGroup - The bind group. + */ + deleteBindGroupData( bindGroup ) { + + const { backend } = this; + + const bindingsData = backend.get( bindGroup ); + + if ( bindingsData.layout ) { + + bindingsData.layout.usedTimes --; + + if ( bindingsData.layout.usedTimes === 0 ) { + + this._bindGroupLayoutCache.delete( bindingsData.layoutKey ); + + } + + bindingsData.layout = undefined; + bindingsData.layoutKey = undefined; + + } + + } + + /** + * Frees internal resources. + */ + dispose() { + + this._bindGroupLayoutCache.clear(); + + } + } export default WebGPUBindingUtils; diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js index 330d48cfb15545..5b1a2e4c4da2ad 100644 --- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -106,8 +106,9 @@ class WebGPUPipelineUtils { for ( const bindGroup of renderObject.getBindings() ) { const bindingsData = backend.get( bindGroup ); + const { layoutGPU } = bindingsData.layout; - bindGroupLayouts.push( bindingsData.layout ); + bindGroupLayouts.push( layoutGPU ); } @@ -341,8 +342,9 @@ class WebGPUPipelineUtils { for ( const bindingsGroup of bindings ) { const bindingsData = backend.get( bindingsGroup ); + const { layoutGPU } = bindingsData.layout; - bindGroupLayouts.push( bindingsData.layout ); + bindGroupLayouts.push( layoutGPU ); } diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 60f38fed406e18..afb9bd2832ec85 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -771,21 +771,25 @@ class WebGPUTextureUtils { const width = ( mipLevel > 0 ) ? image.width : textureDescriptorGPU.size.width; const height = ( mipLevel > 0 ) ? image.height : textureDescriptorGPU.size.height; - device.queue.copyExternalImageToTexture( - { - source: image, - flipY: flipY - }, { - texture: textureGPU, - mipLevel: mipLevel, - origin: { x: 0, y: 0, z: originDepth }, - premultipliedAlpha: premultiplyAlpha - }, { - width: width, - height: height, - depthOrArrayLayers: 1 - } - ); + try { + + device.queue.copyExternalImageToTexture( + { + source: image, + flipY: flipY + }, { + texture: textureGPU, + mipLevel: mipLevel, + origin: { x: 0, y: 0, z: originDepth }, + premultipliedAlpha: premultiplyAlpha + }, { + width: width, + height: height, + depthOrArrayLayers: 1 + } + ); + + } catch ( _ ) {} // try/catch has been added to fix bad video frame data on certain devices, see #32391 }