diff --git a/examples/webgpu_compute_particles_fluid.html b/examples/webgpu_compute_particles_fluid.html index cf21acb39d6662..e8f3396835ddb7 100644 --- a/examples/webgpu_compute_particles_fluid.html +++ b/examples/webgpu_compute_particles_fluid.html @@ -143,6 +143,10 @@ } ); + await renderer.init(); + await renderer.compileComputeAsync( [ clearGridKernel, p2g1Kernel, p2g2Kernel, updateGridKernel, g2pKernel ] ); + await renderer.compileAsync( scene, camera ); + window.addEventListener( 'resize', onWindowResize ); controls.update(); renderer.setAnimationLoop( render ); diff --git a/package-lock.json b/package-lock.json index 1f17c91f7682c9..bd23fb6e14c570 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1083,9 +1083,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2191,9 +2191,9 @@ } }, "node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, "funding": [ { diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index 12b344150f99f0..5032d90433149b 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -243,8 +243,9 @@ class Backend { * @abstract * @param {ComputePipeline} computePipeline - The compute pipeline. * @param {Array} bindings - The bindings. + * @param {?Array} [promises=null] - Optional compilation promises. */ - createComputePipeline( /*computePipeline, bindings*/ ) { } + createComputePipeline( /*computePipeline, bindings, promises*/ ) { } // cache key diff --git a/src/renderers/common/Pipelines.js b/src/renderers/common/Pipelines.js index 9275f9f7ccd3e4..9b48b04d6cf978 100644 --- a/src/renderers/common/Pipelines.js +++ b/src/renderers/common/Pipelines.js @@ -81,9 +81,10 @@ class Pipelines extends DataMap { * * @param {Node} computeNode - The compute node. * @param {Array} bindings - The bindings. + * @param {?Array} [promises=null] - An array of compilation promises which is only relevant in context of `Renderer.compileComputeAsync()`. * @return {ComputePipeline} The compute pipeline. */ - getForCompute( computeNode, bindings ) { + getForCompute( computeNode, bindings, promises = null ) { const { backend } = this; @@ -130,7 +131,7 @@ class Pipelines extends DataMap { if ( previousPipeline && previousPipeline.usedTimes === 0 ) this._releasePipeline( previousPipeline ); - pipeline = this._getComputePipeline( computeNode, stageCompute, cacheKey, bindings ); + pipeline = this._getComputePipeline( computeNode, stageCompute, cacheKey, bindings, promises ); } @@ -342,9 +343,10 @@ class Pipelines extends DataMap { * @param {ProgrammableStage} stageCompute - The programmable stage representing the compute shader. * @param {string} cacheKey - The cache key. * @param {Array} bindings - The bindings. + * @param {?Array} promises - An array of compilation promises which is only relevant in context of `Renderer.compileComputeAsync()`. * @return {ComputePipeline} The compute pipeline. */ - _getComputePipeline( computeNode, stageCompute, cacheKey, bindings ) { + _getComputePipeline( computeNode, stageCompute, cacheKey, bindings, promises ) { // check for existing pipeline @@ -358,7 +360,7 @@ class Pipelines extends DataMap { this.caches.set( cacheKey, pipeline ); - this.backend.createComputePipeline( pipeline, bindings ); + this.backend.createComputePipeline( pipeline, bindings, promises ); } diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 2ddc9fc31a1639..9d7bcb4be70467 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -896,9 +896,10 @@ class Renderer { * @param {Object3D} scene - The scene or 3D object to precompile. * @param {Camera} camera - The camera that is used to render the scene. * @param {?Scene} targetScene - If the first argument is a 3D object, this parameter must represent the scene the 3D object is going to be added. + * @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress. * @return {Promise} A Promise that resolves when the compile has been finished. */ - async compileAsync( scene, camera, targetScene = null ) { + async compileAsync( scene, camera, targetScene = null, onProgress = null ) { if ( this._isDeviceLost === true ) return; @@ -932,7 +933,9 @@ class Renderer { // Match render()'s logic: use frameBufferTarget when needsFrameBufferTarget is true const useFrameBufferTarget = this.needsFrameBufferTarget && this._renderTarget === null; - const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : ( this._renderTarget || this._outputRenderTarget ); + const outputRenderTarget = this._renderTarget || this._outputRenderTarget; + const useXRCamera = outputRenderTarget !== null && outputRenderTarget.isXRRenderTarget === true; + const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : outputRenderTarget; const renderContext = this._renderContexts.get( renderTarget, this._mrt ); const activeMipmapLevel = this._activeMipmapLevel; @@ -963,7 +966,7 @@ class Renderer { if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld(); - camera = this._updateCamera( camera ); + camera = this._updateCamera( camera, useXRCamera ); // @@ -1058,6 +1061,9 @@ class Renderer { // Process compilation work items sequentially to avoid freezing // Yields between objects to keep animation smooth + const total = compilationPromises.length; + let loaded = 0; + for ( const item of compilationPromises ) { const renderObject = this._objects.get( item.object, item.material, item.scene, item.camera, item.lightsNode, item.renderContext, item.clippingContext, item.passId ); @@ -1087,6 +1093,14 @@ class Renderer { this._nodes.updateAfter( renderObject ); this._isPreCompiling = false; + loaded ++; + + if ( onProgress !== null ) { + + onProgress( new ProgressEvent( 'progress', { lengthComputable: true, loaded, total } ) ); + + } + // Yield between objects to allow animation frames await yieldToMain(); @@ -1094,6 +1108,90 @@ class Renderer { } + /** + * Compile compute programs. This can be useful to avoid a + * phenomenon which is called "shader compilation stutter", which occurs when + * rendering an object with a new shader for the first time. + * + * @async + * @param {Node|Array} computeNodes - The compute node(s). + * @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress. + * @return {Promise} A Promise that resolves when the compile has been finished. + */ + async compileComputeAsync( computeNodes, onProgress = null ) { + + if ( this._isDeviceLost === true ) return; + + if ( this._initialized === false ) await this.init(); + + const computeList = Array.isArray( computeNodes ) ? computeNodes : [ computeNodes ]; + + if ( computeList.length === 0 || computeList.some( ( computeNode ) => computeNode === undefined || computeNode === null || computeNode.isComputeNode !== true ) ) { + + throw new Error( 'THREE.Renderer: .compileComputeAsync() expects a ComputeNode.' ); + + } + + const total = computeList.length; + let loaded = 0; + + // + + const pipelines = this._pipelines; + const bindings = this._bindings; + const nodes = this._nodes; + + for ( const computeNode of computeList ) { + + if ( pipelines.has( computeNode ) === false ) { + + const dispose = () => { + + computeNode.removeEventListener( 'dispose', dispose ); + + pipelines.delete( computeNode ); + bindings.deleteForCompute( computeNode ); + nodes.delete( computeNode ); + + }; + + computeNode.addEventListener( 'dispose', dispose ); + + const onInitFn = computeNode.onInitFunction; + + if ( onInitFn !== null ) { + + onInitFn.call( computeNode, { renderer: this } ); + + } + + } + + await nodes.getForComputeAsync( computeNode ); + + nodes.updateForCompute( computeNode ); + bindings.updateForCompute( computeNode ); + + const computeBindings = bindings.getForCompute( computeNode ); + const compilationPromises = []; + + pipelines.getForCompute( computeNode, computeBindings, compilationPromises ); + await Promise.all( compilationPromises ); + + loaded ++; + + if ( onProgress !== null ) { + + onProgress( new ProgressEvent( 'progress', { lengthComputable: true, loaded, total } ) ); + + } + + if ( loaded < total ) await yieldToMain(); + + } + + } + /** * Renders the scene in an async fashion. * @@ -1582,6 +1680,7 @@ class Renderer { const sceneRef = ( scene.isScene === true ) ? scene : _scene; const outputRenderTarget = this._renderTarget || this._outputRenderTarget; + const useXRCamera = outputRenderTarget !== null && outputRenderTarget.isXRRenderTarget === true; const activeCubeFace = this._activeCubeFace; const activeMipmapLevel = this._activeMipmapLevel; @@ -1650,7 +1749,7 @@ class Renderer { if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld(); - camera = this._updateCamera( camera ); + camera = this._updateCamera( camera, useXRCamera ); // @@ -3497,13 +3596,14 @@ class Renderer { * * @private * @param {Camera} camera - The camera to update. + * @param {boolean} useXRCamera - Whether the XR camera should be used when presenting. * @return {Camera} The returned camera might be different depending on whether XR is used or not. */ - _updateCamera( camera ) { + _updateCamera( camera, useXRCamera ) { const xr = this.xr; - if ( xr.isPresenting === false ) { + if ( xr.isPresenting === false || useXRCamera === false ) { let projectionMatrixNeedsUpdate = false; @@ -3573,7 +3673,7 @@ class Renderer { // handle XR - if ( xr.enabled === true && xr.isPresenting === true ) { + if ( useXRCamera === true && xr.enabled === true && xr.isPresenting === true ) { if ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera ); camera = xr.getCamera(); // use XR camera for rendering @@ -3864,7 +3964,8 @@ class Renderer { * @param {Object3D} scene - The scene or 3D object to precompile. * @param {Camera} camera - The camera that is used to render the scene. * @param {Scene} targetScene - If the first argument is a 3D object, this parameter must represent the scene the 3D object is going to be added. - * @return {function(Object3D, Camera, ?Scene): Promise|undefined} A Promise that resolves when the compile has been finished. + * @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress. + * @return {function(Object3D, Camera, ?Scene, ?onProgressCallback): Promise|undefined} A Promise that resolves when the compile has been finished. */ get compile() { diff --git a/src/renderers/common/XRManager.js b/src/renderers/common/XRManager.js index 6901d0ff2b06d9..977446ccab717f 100644 --- a/src/renderers/common/XRManager.js +++ b/src/renderers/common/XRManager.js @@ -648,11 +648,11 @@ class XRManager extends EventDispatcher { * Browser-side `XRWebGLBinding.foveateBoundTexture()` failures are treated as * non-fatal so they do not interrupt rendering. * - * @param {RenderTarget} renderTarget - The internal render target. + * @param {?RenderTarget} renderTarget - The internal render target. */ foveateBoundTexture( renderTarget ) { - if ( renderTarget.isPostProcessingRenderTarget !== true ) return; + if ( renderTarget === null || renderTarget.isPostProcessingRenderTarget !== true ) return; if ( this.isPresenting !== true ) return; if ( this._glProjLayer === null ) return; diff --git a/src/renderers/common/nodes/NodeManager.js b/src/renderers/common/nodes/NodeManager.js index 8687fef4463a96..0b5faf6a111e1c 100644 --- a/src/renderers/common/nodes/NodeManager.js +++ b/src/renderers/common/nodes/NodeManager.js @@ -450,9 +450,10 @@ class NodeManager extends DataMap { * Returns a node builder state for the given compute node. * * @param {Node} computeNode - The compute node. - * @return {NodeBuilderState} The node builder state. + * @param {boolean} [useAsync=false] - Whether to use async build with yielding. + * @return {NodeBuilderState|Promise} The node builder state (or Promise if async). */ - getForCompute( computeNode ) { + getForCompute( computeNode, useAsync = false ) { const computeData = this.get( computeNode ); @@ -465,6 +466,21 @@ class NodeManager extends DataMap { if ( onNodeBuilderCreated !== null ) onNodeBuilderCreated( nodeBuilder, computeNode ); + if ( useAsync ) { + + return nodeBuilder.buildAsync().then( () => { + + nodeBuilderState = this._createNodeBuilderState( nodeBuilder ); + + computeData.nodeBuilderState = nodeBuilderState; + computeData.version = computeNode.version; + + return nodeBuilderState; + + } ); + + } + nodeBuilder.build(); nodeBuilderState = this._createNodeBuilderState( nodeBuilder ); @@ -478,6 +494,27 @@ class NodeManager extends DataMap { } + /** + * Async version of getForCompute() that yields to main thread during build. + * Use this in compileComputeAsync() to prevent blocking the main thread. + * + * @param {Node} computeNode - The compute node. + * @return {Promise} A promise that resolves to the node builder state. + */ + getForComputeAsync( computeNode ) { + + const result = this.getForCompute( computeNode, true ); + + if ( result.then ) { + + return result; + + } + + return Promise.resolve( result ); + + } + /** * Creates a node builder state for the given node builder. * @@ -740,6 +777,27 @@ class NodeManager extends DataMap { if ( node === undefined || forceUpdate ) { + if ( node === undefined && object.isTexture === true ) { + + const onTextureDispose = () => { + + object.removeEventListener( 'dispose', onTextureDispose ); + + const node = nodeCache.get( object ); + + if ( node !== undefined ) { + + nodeCache.delete( object ); + node.dispose(); + + } + + }; + + object.addEventListener( 'dispose', onTextureDispose ); + + } + node = callback(); nodeCache.set( object, node ); diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index cbf16288229f13..70136de35b6a41 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1722,10 +1722,11 @@ class WebGLBackend extends Backend { * * @param {ComputePipeline} computePipeline - The compute pipeline. * @param {Array} bindings - The bindings. + * @param {?Array} [promises=null] - Optional compilation promises. */ - createComputePipeline( computePipeline, bindings ) { + createComputePipeline( computePipeline, bindings, promises = null ) { - const { state, gl } = this; + const { gl } = this; // Program @@ -1768,19 +1769,6 @@ class WebGLBackend extends Backend { gl.linkProgram( programGPU ); - if ( gl.getProgramParameter( programGPU, gl.LINK_STATUS ) === false ) { - - this._logProgramError( programGPU, fragmentShader, vertexShader ); - - - } - - state.useProgram( programGPU ); - - // Bindings - - this._setupBindings( bindings, programGPU ); - const attributeNodes = computeProgram.attributes; const attributes = []; const transformBuffers = []; @@ -1807,14 +1795,73 @@ class WebGLBackend extends Backend { } - // + // Store pipeline data this.set( computePipeline, { programGPU, + fragmentShader, + vertexShader, transformBuffers, attributes } ); + if ( promises !== null && this.parallel ) { + + const parallel = this.parallel; + + const p = new Promise( ( resolve ) => { + + const checkStatus = () => { + + if ( gl.getProgramParameter( programGPU, parallel.COMPLETION_STATUS_KHR ) ) { + + this._completeComputeCompile( computePipeline, bindings ); + resolve(); + + } else { + + requestAnimationFrame( checkStatus ); + + } + + }; + + checkStatus(); + + } ); + + promises.push( p ); + return; + + } + + // Sync fallback + this._completeComputeCompile( computePipeline, bindings ); + + } + + /** + * Completes the compute pipeline setup for the given compute pipeline. + * + * @param {ComputePipeline} computePipeline - The compute pipeline. + * @param {Array} bindings - Array of bind groups. + */ + _completeComputeCompile( computePipeline, bindings ) { + + const { state, gl } = this; + const { programGPU, fragmentShader, vertexShader } = this.get( computePipeline ); + + if ( gl.getProgramParameter( programGPU, gl.LINK_STATUS ) === false ) { + + this._logProgramError( programGPU, fragmentShader, vertexShader ); + + } + + state.useProgram( programGPU ); + + // Bindings (must be after link completion) + this._setupBindings( bindings, programGPU ); + } /** diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 96aa495429ce44..3a3c6f0fe1f99d 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2382,10 +2382,11 @@ class WebGPUBackend extends Backend { * * @param {ComputePipeline} computePipeline - The compute pipeline. * @param {Array} bindings - The bindings. + * @param {?Array} [promises=null] - Optional compilation promises. */ - createComputePipeline( computePipeline, bindings ) { + createComputePipeline( computePipeline, bindings, promises = null ) { - this.pipelineUtils.createComputePipeline( computePipeline, bindings ); + this.pipelineUtils.createComputePipeline( computePipeline, bindings, promises ); } diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js index 764267c51628af..0e130371451f47 100644 --- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -376,8 +376,9 @@ class WebGPUPipelineUtils { * * @param {ComputePipeline} pipeline - The compute pipeline. * @param {Array} bindings - The bindings. + * @param {?Array} [promises=null] - Optional compilation promises. */ - createComputePipeline( pipeline, bindings ) { + createComputePipeline( pipeline, bindings, promises = null ) { const backend = this.backend; const device = backend.device; @@ -414,23 +415,87 @@ class WebGPUPipelineUtils { _computePipelineDescriptor.compute = computeProgram; _computePipelineDescriptor.layout = pipelineLayout; - pipelineGPU.pipeline = device.createComputePipeline( _computePipelineDescriptor ); + if ( promises === null ) { - _computePipelineDescriptor.reset(); + pipelineGPU.pipeline = device.createComputePipeline( _computePipelineDescriptor ); - device.popErrorScope().then( ( err ) => { + _computePipelineDescriptor.reset(); - if ( err !== null ) { + device.popErrorScope().then( ( err ) => { - pipelineGPU.error = true; + if ( err !== null ) { - error( `WebGPURenderer: Compute pipeline creation failed (${ pipelineLabel }): ${ err.message }` ); + pipelineGPU.error = true; - this._reportShaderDiagnostics( [ { program: computeStage, module: computeProgram.module } ], pipelineLabel ); + error( `WebGPURenderer: Compute pipeline creation failed (${ pipelineLabel }): ${ err.message }` ); - } + this._reportShaderDiagnostics( [ { program: computeStage, module: computeProgram.module } ], pipelineLabel ); + + } + + } ); + + } else { + + const promise = new Promise( async ( resolve /*, reject*/ ) => { + + try { + + let asyncError = null; + let pipelinePromise = null; + + try { + + pipelinePromise = device.createComputePipelineAsync( _computePipelineDescriptor ); + + } catch ( err ) { + + asyncError = err; + + } + + _computePipelineDescriptor.reset(); + + if ( pipelinePromise !== null ) { + + try { - } ); + pipelineGPU.pipeline = await pipelinePromise; + + } catch ( err ) { + + asyncError = err; + + } + + } + + const errorScope = await device.popErrorScope(); + + if ( errorScope !== null || asyncError !== null ) { + + pipelineGPU.error = true; + + const reason = ( errorScope && errorScope.message ) || ( asyncError && asyncError.message ) || 'unknown'; + error( `WebGPURenderer: Async compute pipeline creation failed (${ pipelineLabel }): ${ reason }` ); + + await this._reportShaderDiagnostics( [ { program: computeStage, module: computeProgram.module } ], pipelineLabel ); + + } + + } finally { + + // Guarantee resolution so `compileComputeAsync`'s Promise.all cannot hang on an + // unexpected throw from any await above. + resolve(); + + } + + } ); + + promises.push( promise ); + + } }