From 13e203883060f5fdbcd3a2018d4081656fd91a28 Mon Sep 17 00:00:00 2001 From: billy Date: Tue, 14 Apr 2026 19:22:28 -0400 Subject: [PATCH] fix(viewer): skip post-processing pipeline when WebGPU is unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RenderPipeline`, SSGI, and the denoise TSL node imported from `three/webgpu` and `three/tsl` are all WebGPU-only. On a browser without `navigator.gpu`, the WebGPURenderer falls back to WebGL2, and attempting to build the TSL post-processing pipeline either throws or produces broken output — the scene renders for a few frames, then goes black as the 3-attempt retry loop fights the direct-render fallback path in `useFrame`. This sets `hasPipelineErrorRef.current = true` at pipeline setup time when `navigator.gpu` is undefined, so `useFrame` takes the existing direct `renderer.render(scene, camera)` path exclusively and never tries to build the broken TSL pipeline. No behavioural change in WebGPU mode — the guard only fires when the WebGPU API is literally not exposed by the browser. The rare edge case of `navigator.gpu` being defined but device creation failing at runtime still falls through the existing try/catch unchanged. --- .../src/components/viewer/post-processing.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx index 8daf68be7..ef3fa6468 100644 --- a/packages/viewer/src/components/viewer/post-processing.tsx +++ b/packages/viewer/src/components/viewer/post-processing.tsx @@ -117,6 +117,24 @@ const PostProcessingPasses = () => { hasPipelineErrorRef.current = false + // WebGPU availability check: SSGI, denoise, and RenderPipeline are all + // WebGPU-only APIs. When the browser falls back to WebGL2 (no + // `navigator.gpu`, or the device couldn't be created), building the + // pipeline either throws silently or produces a broken output where + // the scene renders for a few frames and then goes black as the retry + // loop fights the direct-render fallback path. Short-circuit here so + // `useFrame` uses the direct `renderer.render(scene, camera)` path + // exclusively and never attempts the TSL pipeline. + const hasWebGPU = typeof navigator !== 'undefined' && typeof navigator.gpu !== 'undefined' + if (!hasWebGPU) { + console.warn( + '[viewer] WebGPU unavailable — rendering without post-processing (SSGI, outlines, denoise).', + ) + hasPipelineErrorRef.current = true + renderPipelineRef.current = null + return + } + // Clear outliner arrays synchronously to prevent stale Object3D refs // from the previous project leaking into the new pipeline's outline passes. const outliner = useViewer.getState().outliner @@ -263,15 +281,7 @@ const PostProcessingPasses = () => { } renderPipelineRef.current = null } - }, [ - renderer, - scene, - camera, - hoverHighlightMode, - zoneLayers, - projectId, - pipelineVersion, - ]) + }, [renderer, scene, camera, hoverHighlightMode, zoneLayers, projectId, pipelineVersion]) useFrame((_, delta) => { // Animate background colour toward the current theme target (same lerp as AnimatedBackground)