diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index aee25e75e19df6..e9aa10ed40fe4f 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -594,6 +594,17 @@ class Renderer { */ this.onDeviceLost = this._onDeviceLost; + /** + * A callback function that defines what should happen when an uncaptured + * backend error is reported (e.g. a WebGPU validation/out-of-memory/internal + * error raised outside an error scope). Applications can override this to + * surface errors in their own UI without letting them escalate to a device + * loss. The default implementation logs to the console. + * + * @type {Function} + */ + this.onError = this._onError; + /** * Defines the type of output buffers. The default `HalfFloatType` is recommend for * best quality. To save memory and bandwidth, `UnsignedByteType` might be used. @@ -1196,6 +1207,26 @@ class Renderer { } + /** + * Default implementation of the uncaptured backend error callback. + * + * @private + * @param {Object} info - Information about the uncaptured error. + */ + _onError( info ) { + + let errorMessage = `WebGPURenderer: Uncaptured ${ info.api } ${ info.type }`; + + if ( info.message ) { + + errorMessage += `: ${ info.message }`; + + } + + error( errorMessage ); + + } + /** * Renders the given render bundle. * diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 6d9bdfa2e1a5bb..04a163865e1d1d 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -251,6 +251,21 @@ class WebGPUBackend extends Backend { } ); + device.onuncapturederror = ( event ) => { + + const gpuError = event.error; + const type = gpuError && gpuError.constructor ? gpuError.constructor.name : 'GPUError'; + const message = ( gpuError && gpuError.message ) || 'Unknown uncaptured GPU error'; + + renderer.onError( { + api: 'WebGPU', + type, + message, + originalEvent: event + } ); + + }; + this.device = device; this.trackTimestamp = this.trackTimestamp && this.hasFeature( GPUFeatureName.TimestampQuery ); diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js index 75a947b416e45d..51c2878c0170fd 100644 --- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -15,7 +15,7 @@ import { NeverStencilFunc, AlwaysStencilFunc, LessStencilFunc, LessEqualStencilFunc, EqualStencilFunc, GreaterEqualStencilFunc, GreaterStencilFunc, NotEqualStencilFunc } from '../../../constants.js'; -import { error, ReversedDepthFuncs, warnOnce } from '../../../utils.js'; +import { error, ReversedDepthFuncs, warn, warnOnce } from '../../../utils.js'; /** * A WebGPU backend utility module for managing pipelines. @@ -272,6 +272,12 @@ class WebGPUPipelineUtils { device.pushErrorScope( 'validation' ); + const stages = [ + { program: vertexProgram, module: vertexModule.module }, + { program: fragmentProgram, module: fragmentModule.module } + ]; + const pipelineLabel = pipelineDescriptor.label; + if ( promises === null ) { pipelineData.pipeline = device.createRenderPipeline( pipelineDescriptor ); @@ -282,7 +288,9 @@ class WebGPUPipelineUtils { pipelineData.error = true; - error( err.message ); + error( `WebGPURenderer: Render pipeline creation failed (${ pipelineLabel }): ${ err.message }` ); + + this._reportShaderDiagnostics( stages, pipelineLabel ); } @@ -294,21 +302,38 @@ class WebGPUPipelineUtils { try { - pipelineData.pipeline = await device.createRenderPipelineAsync( pipelineDescriptor ); + let asyncError = null; - } catch ( err ) { } + try { - const errorScope = await device.popErrorScope(); + pipelineData.pipeline = await device.createRenderPipelineAsync( pipelineDescriptor ); - if ( errorScope !== null ) { + } catch ( err ) { - pipelineData.error = true; + asyncError = err; - error( errorScope.message ); + } - } + const errorScope = await device.popErrorScope(); + + if ( errorScope !== null || asyncError !== null ) { + + pipelineData.error = true; + + const reason = ( errorScope && errorScope.message ) || ( asyncError && asyncError.message ) || 'unknown'; + error( `WebGPURenderer: Async render pipeline creation failed (${ pipelineLabel }): ${ reason }` ); + + await this._reportShaderDiagnostics( stages, pipelineLabel ); + + } + + } finally { - resolve(); + // Guarantee resolution so `compileAsync`'s Promise.all cannot hang on an + // unexpected throw from any await above. + resolve(); + + } } ); @@ -373,13 +398,76 @@ class WebGPUPipelineUtils { } + const computeStage = pipeline.computeProgram; + const pipelineLabel = `computePipeline_${ computeStage.stage }${ computeStage.name ? `_${ computeStage.name }` : '' }`; + + device.pushErrorScope( 'validation' ); + pipelineGPU.pipeline = device.createComputePipeline( { + label: pipelineLabel, compute: computeProgram, layout: device.createPipelineLayout( { bindGroupLayouts } ) } ); + device.popErrorScope().then( ( err ) => { + + if ( err !== null ) { + + pipelineGPU.error = true; + + error( `WebGPURenderer: Compute pipeline creation failed (${ pipelineLabel }): ${ err.message }` ); + + this._reportShaderDiagnostics( [ { program: computeStage, module: computeProgram.module } ], pipelineLabel ); + + } + + } ); + + } + + /** + * Reads line-accurate diagnostics from shader modules and logs them. + * Called from pipeline creation error paths to turn opaque validation + * failures into actionable WGSL feedback. + * + * @private + * @param {Array<{program: ProgrammableStage, module: GPUShaderModule}>} stages - Pairs of program + compiled shader module. + * @param {string} pipelineLabel - Label of the owning pipeline, used as log prefix. + * @return {Promise} + */ + async _reportShaderDiagnostics( stages, pipelineLabel ) { + + for ( const { program, module } of stages ) { + + const info = await module.getCompilationInfo(); + if ( info.messages.length === 0 ) continue; + + const sourceLines = program.code.split( '\n' ); + + for ( const msg of info.messages ) { + + const location = msg.lineNum > 0 + ? ` at line ${ msg.lineNum }${ msg.linePos > 0 ? `:${ msg.linePos }` : '' }` + : ''; + + const header = `WebGPURenderer [${ pipelineLabel } / ${ program.stage } ${ msg.type }]${ location }: ${ msg.message }`; + + let excerpt = ''; + if ( msg.lineNum > 0 && msg.lineNum <= sourceLines.length ) { + + excerpt = `\n ${ sourceLines[ msg.lineNum - 1 ] }`; + if ( msg.linePos > 0 ) excerpt += `\n ${ ' '.repeat( msg.linePos - 1 ) }^`; + + } + + ( msg.type === 'error' ? error : warn )( header + excerpt ); + + } + + } + } /**