Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/renderers/common/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down
15 changes: 15 additions & 0 deletions src/renderers/webgpu/WebGPUBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
152 changes: 142 additions & 10 deletions src/renderers/webgpu/utils/WebGPUPipelineUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 );
Expand All @@ -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 );

}

Expand All @@ -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();

resolve();
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 {

// Guarantee resolution so `compileAsync`'s Promise.all cannot hang on an
// unexpected throw from any await above.
resolve();

}

} );

Expand Down Expand Up @@ -373,13 +398,120 @@ 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 any
* errors/warnings/info messages. Called from pipeline creation error paths
* to turn opaque validation failures into actionable WGSL feedback.
*
* Contract: this method is best-effort and must never propagate an error
* to its caller. All failures (spec gaps, custom logger throwing, future
* edits) are swallowed by the top-level try/catch. Callers can fire and
* forget without a `.catch()` guard.
*
* @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<void>}
*/
async _reportShaderDiagnostics( stages, pipelineLabel ) {

try {

for ( const { program, module } of stages ) {

if ( ! module || typeof module.getCompilationInfo !== 'function' ) continue;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes, for me, LLMs add many extra checks; could that be the case? Maybe the info checks bellow is also overloaded with verification?


let info;

try {

info = await module.getCompilationInfo();

} catch ( _ ) {

continue;

}

if ( ! info || ! info.messages || info.messages.length === 0 ) continue;

const stageName = program ? program.stage : 'shader';
const sourceLines = program && program.code ? program.code.split( '\n' ) : null;

for ( const msg of info.messages ) {

const location = ( msg.lineNum > 0 )
? ` at line ${ msg.lineNum }${ msg.linePos > 0 ? `:${ msg.linePos }` : '' }`
: '';

const header = `WebGPURenderer [${ pipelineLabel } / ${ stageName } ${ msg.type }]${ location }: ${ msg.message }`;

let excerpt = '';
if ( sourceLines && msg.lineNum > 0 ) {

const line = sourceLines[ msg.lineNum - 1 ];
if ( line !== undefined ) {

excerpt = `\n ${ line }`;
if ( msg.linePos > 0 ) {

excerpt += `\n ${ ' '.repeat( Math.max( 0, msg.linePos - 1 ) ) }^`;

}

}

}

if ( msg.type === 'error' ) {

error( header + excerpt );

} else {

warn( header + excerpt );

}

}

}

} catch ( _ ) {

// Diagnostics are best-effort; never propagate.

}

}

/**
Expand Down
Loading