You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a node material fails to build, WebGPURenderer silently substitutes a bare NodeMaterial, and the errors that follow describe the substitute rather than the material that failed.
In NodeManager.getForRender(), if nodeBuilder.build() (or buildAsync()) throws, the error is logged as 'TSL: ' + e, a new builder is created for the same render object with new NodeMaterial(), and that state is cached under the render object's cache key.
This causes three problems:
On a multi-attachment target, the loudest error is not the real one. The fallback material has no mrtNode, so its fragment stage declares only @location( 0 ). WebGPU then rejects the pipeline with "Color target has no corresponding fragment stage output" for targets[1], followed by two follow-on validation errors. All three describe the fallback material, not the user's material or the exception that caused the fallback.
The fallback output can look like a real render. It writes a constant vec4( 0, 0, 0, 1 ). In a post-processing or data pass, black is often a plausible value. In our case it read as "the feature has no effect" for several hours.
The async path loses the stack. The sync path attaches e.stack as a StackTrace, but the useAsync branch (used by compileAsync) logs only 'TSL: ' + e.
Reproduction steps
Create a RenderTarget with count: 2 and a NodeMaterial with an mrtNode writing both attachments.
Give it a fragmentNode that throws during setup.
Render it into the target.
A standalone page is below (import map to three@0.186.0 on jsDelivr; serve it from any static server and open it in Chrome).
Expected: the build error is reported against this render object, and nothing is drawn (or the fallback is opt-in).
Actual console output, in order:
THREE.TSL: Error: deliberate build failure
THREE.WebGPURenderer: Render pipeline creation failed (renderPipeline_NodeMaterial_17): Color target has no corresponding fragment stage output but writeMask (ColorWriteMask::(Red|Green|Blue|Alpha)) is not zero.
- While validating targets[1] framebuffer output.
- While validating fragment state.
- While calling [Device].CreateRenderPipeline([RenderPipelineDescriptor ""renderPipeline_NodeMaterial_17""]).
THREE.WebGPURenderer: Uncaptured WebGPU GPUValidationError: [Invalid RenderPipeline "renderPipeline_NodeMaterial_17"] is invalid due to a previous error.
- While encoding [RenderPassEncoder (unlabeled)].SetPipeline([Invalid RenderPipeline "renderPipeline_NodeMaterial_17"]).
- While finishing [CommandEncoder "renderContext_0"].
THREE.WebGPURenderer: Uncaptured WebGPU GPUValidationError: [Invalid CommandBuffer from CommandEncoder "renderContext_0"] is invalid due to a previous error.
- While calling [Queue].Submit([[Invalid CommandBuffer from CommandEncoder "renderContext_0"]])
The build error is one line. The three validation errors after it all come from the fallback pipeline.
We also confirmed this without a GPU using three's own WGSLNodeBuilder: the same material without the throwing node declares @location( 0 ) and @location( 1 ); with it, build() throws; and a bare NodeMaterial built for the same target declares only @location( 0 ).
Full standalone repro page
<!doctype html><metacharset="utf-8"><title>three r186: silent NodeMaterial fallback on an MRT target</title><preid="log"></pre><scripttype="importmap">{"imports": {"three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.webgpu.js","three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.webgpu.js","three/tsl": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.tsl.js"}}</script><scripttype="module">import*asTHREEfrom'three/webgpu'import{Fn,mrt,output,vec4}from'three/tsl'constlog=(...a)=>{document.getElementById('log').textContent+=a.join(' ')+'\n';console.log(...a)}for(constkof['error','warn']){constorig=console[k];console[k]=(...a)=>{document.getElementById('log').textContent+=`[console.${k}] `+a.map(String).join(' ')+'\n';orig(...a)}}constrenderer=newTHREE.WebGPURenderer()awaitrenderer.init()renderer.backend.device.addEventListener('uncapturederror',e=>log('[GPU uncapturederror]',e.error.message))consttarget=newTHREE.RenderTarget(64,64,{count: 2})target.textures[0].name='output'target.textures[1].name='extra'constmaterial=newTHREE.NodeMaterial()material.fragmentNode=Fn(()=>{thrownewError('deliberate build failure')})()material.mrtNode=mrt({ output,extra: vec4(0,1,0,1)})constscene=newTHREE.Scene()scene.add(newTHREE.Mesh(newTHREE.PlaneGeometry(2,2),material))constcamera=newTHREE.OrthographicCamera(-1,1,1,-1,0,1)renderer.setRenderTarget(target)renderer.render(scene,camera)awaitrenderer.backend.device.queue.onSubmittedWorkDone()log('done: see the console.')</script>
Suggestions
Make the fallback opt-in (for example behind a renderer.debug flag). Otherwise, skip drawing the render object and report the failure once.
If the fallback stays, log the material name or type and the original exception with its stack on both the sync and async paths, and mark the cached state as a fallback so a later validation error can point back to the build failure.
Description
When a node material fails to build,
WebGPURenderersilently substitutes a bareNodeMaterial, and the errors that follow describe the substitute rather than the material that failed.In
NodeManager.getForRender(), ifnodeBuilder.build()(orbuildAsync()) throws, the error is logged as'TSL: ' + e, a new builder is created for the same render object withnew NodeMaterial(), and that state is cached under the render object's cache key.This causes three problems:
mrtNode, so its fragment stage declares only@location( 0 ). WebGPU then rejects the pipeline with "Color target has no corresponding fragment stage output" fortargets[1], followed by two follow-on validation errors. All three describe the fallback material, not the user's material or the exception that caused the fallback.vec4( 0, 0, 0, 1 ). In a post-processing or data pass, black is often a plausible value. In our case it read as "the feature has no effect" for several hours.e.stackas aStackTrace, but theuseAsyncbranch (used bycompileAsync) logs only'TSL: ' + e.Reproduction steps
RenderTargetwithcount: 2and aNodeMaterialwith anmrtNodewriting both attachments.fragmentNodethat throws during setup.A standalone page is below (import map to
three@0.186.0on jsDelivr; serve it from any static server and open it in Chrome).Code
Expected: the build error is reported against this render object, and nothing is drawn (or the fallback is opt-in).
Actual console output, in order:
The build error is one line. The three validation errors after it all come from the fallback pipeline.
We also confirmed this without a GPU using three's own
WGSLNodeBuilder: the same material without the throwing node declares@location( 0 )and@location( 1 ); with it,build()throws; and a bareNodeMaterialbuilt for the same target declares only@location( 0 ).Full standalone repro page
Suggestions
renderer.debugflag). Otherwise, skip drawing the render object and report the failure once.renderer.debug.onNodeBuilderCreated(Nodes: Addrenderer.debug.onNodeBuilderCreatedcallback #34068) helps catch the builder, but doesn't change the misleading output.Version
r186 (
three@0.186.0). Also observed in r185.Device / Browser / OS
Apple M4 (Mac mini), Chrome 154.0.8037.58, macOS 26.6.2