From 409750fe4866c5ca5af5a92277efad0cb11cfe8b Mon Sep 17 00:00:00 2001 From: Renaud Rohlinger Date: Wed, 29 Apr 2026 11:56:32 +0900 Subject: [PATCH 1/3] WebGPURenderer: Surface uncaptured GPU errors and WGSL diagnostics (#33418) Co-authored-by: Claude Opus 4.7 (1M context) --- src/renderers/common/Renderer.js | 31 +++++ src/renderers/webgpu/WebGPUBackend.js | 15 +++ .../webgpu/utils/WebGPUPipelineUtils.js | 108 ++++++++++++++++-- 3 files changed, 144 insertions(+), 10 deletions(-) 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 145906e7c1c96e..df85e1862f79e7 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -250,6 +250,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 edb20ab034a354..f5cb377fd40b91 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 ); + + } + + } + } /** From cb787f28e2a951ad16f0890b0c77ec7c1b9b72ba Mon Sep 17 00:00:00 2001 From: sunag Date: Wed, 29 Apr 2026 00:31:06 -0300 Subject: [PATCH 2/3] TSL: Fix stack node sequence (#33402) --- src/nodes/core/StackNode.js | 72 ++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/nodes/core/StackNode.js b/src/nodes/core/StackNode.js index 5615596e835860..9ecd173c302f21 100644 --- a/src/nodes/core/StackNode.js +++ b/src/nodes/core/StackNode.js @@ -78,6 +78,14 @@ class StackNode extends Node { */ this._currentNode = null; + /** + * Stores additional data for nodes that are added to the stack. + * + * @private + * @type {Map} + */ + this._nodeDataLibrary = new Map(); + /** * This flag can be used for type testing. * @@ -111,10 +119,10 @@ class StackNode extends Node { * Adds a node to this stack. * * @param {Node} node - The node to add. - * @param {number} [index=this.nodes.length] - The index where the node should be added. + * @param {number} [index=-1] - The index of the node. If not specified, the node will be added to the end of the stack. * @return {StackNode} A reference to this stack node. */ - addToStack( node, index = this.nodes.length ) { + addToStack( node, index = - 1 ) { if ( node.isNode !== true ) { @@ -123,6 +131,35 @@ class StackNode extends Node { } + + if ( index === - 1 ) { + + if ( this._currentNode ) { + + let nodeData = this._nodeDataLibrary.get( this._currentNode ); + + if ( nodeData === undefined ) { + + nodeData = { + delta: 0 + }; + + this._nodeDataLibrary.set( this._currentNode, nodeData ); + + } + + nodeData.delta ++; + + index = this.nodes.indexOf( this._currentNode ) + nodeData.delta; + + } else { + + index = this.nodes.length; + + } + + } + this.nodes.splice( index, 0, node ); return this; @@ -137,7 +174,7 @@ class StackNode extends Node { */ addToStackBefore( node ) { - const index = this._currentNode ? this.nodes.indexOf( this._currentNode ) : 0; + const index = this._currentNode !== null ? this.nodes.indexOf( this._currentNode ) : - 1; return this.addToStack( node, index ); @@ -324,7 +361,10 @@ class StackNode extends Node { // - const buildNode = ( node ) => { + for ( let i = 0; i < this.nodes.length; i ++ ) { + + const node = this.nodes[ i ]; + const previousNode = this._currentNode; this._currentNode = node; @@ -332,7 +372,7 @@ class StackNode extends Node { if ( node.isAssign( builder ) !== true ) { - return; + continue; } @@ -353,7 +393,7 @@ class StackNode extends Node { if ( node.isVarNode && parents && parents.length === 1 && parents[ 0 ] && parents[ 0 ].isStackNode ) { - return; // skip var nodes that are only used in .toVarying() + continue; // skip var nodes that are only used in .toVarying() } @@ -361,25 +401,7 @@ class StackNode extends Node { } - }; - - // - - const nodes = [ ...this.nodes ]; - - for ( const node of nodes ) { - - buildNode( node ); - - } - - this._currentNode = null; - - const newNodes = this.nodes.filter( ( node ) => nodes.indexOf( node ) === - 1 ); - - for ( const node of newNodes ) { - - buildNode( node ); + this._currentNode = previousNode; } From 36d7cc3a44bcb938c913b52f5608dd413f8fc6c1 Mon Sep 17 00:00:00 2001 From: sunag Date: Wed, 29 Apr 2026 00:49:16 -0300 Subject: [PATCH 3/3] Update StackNode.js --- src/nodes/core/StackNode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nodes/core/StackNode.js b/src/nodes/core/StackNode.js index 9ecd173c302f21..20fe951a359318 100644 --- a/src/nodes/core/StackNode.js +++ b/src/nodes/core/StackNode.js @@ -174,7 +174,7 @@ class StackNode extends Node { */ addToStackBefore( node ) { - const index = this._currentNode !== null ? this.nodes.indexOf( this._currentNode ) : - 1; + const index = this._currentNode ? this.nodes.indexOf( this._currentNode ) : 0; return this.addToStack( node, index );