fix(viewer): skip post-processing pipeline when WebGPU is unavailable (#234)

`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.
This commit is contained in:
billy
2026-04-15 17:23:25 -04:00
committed by GitHub
parent b1709de44a
commit 93d9d68e19
@@ -117,6 +117,24 @@ const PostProcessingPasses = () => {
hasPipelineErrorRef.current = false 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 // Clear outliner arrays synchronously to prevent stale Object3D refs
// from the previous project leaking into the new pipeline's outline passes. // from the previous project leaking into the new pipeline's outline passes.
const outliner = useViewer.getState().outliner const outliner = useViewer.getState().outliner
@@ -263,15 +281,7 @@ const PostProcessingPasses = () => {
} }
renderPipelineRef.current = null renderPipelineRef.current = null
} }
}, [ }, [renderer, scene, camera, hoverHighlightMode, zoneLayers, projectId, pipelineVersion])
renderer,
scene,
camera,
hoverHighlightMode,
zoneLayers,
projectId,
pipelineVersion,
])
useFrame((_, delta) => { useFrame((_, delta) => {
// Animate background colour toward the current theme target (same lerp as AnimatedBackground) // Animate background colour toward the current theme target (same lerp as AnimatedBackground)