fix(viewer): graceful fallback when WebGPU/WebGL is unavailable (#403)
Adds a capability check and an UnsupportedGpuViewerFallback so the viewer renders an informative panel instead of crashing on environments that expose neither WebGPU nor WebGL. Capability detection runs post-mount to stay hydration-safe, the renderer-init failure path swaps to the fallback, and the fallback signals scene-readiness so the host editor loader does not hang. Reworked from #403 (anton-pascal) onto current main; resolves the import conflict and the SSR/scene-ready gotchas surfaced in review. Co-authored-by: Open Pascal <open@pascal.app>
This commit is contained in:
@@ -8,7 +8,14 @@ import {
|
|||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber'
|
import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber'
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react'
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import * as THREE from 'three/webgpu'
|
import * as THREE from 'three/webgpu'
|
||||||
import { hasDrawableGeometry } from '../../lib/drawable-geometry'
|
import { hasDrawableGeometry } from '../../lib/drawable-geometry'
|
||||||
import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf'
|
import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf'
|
||||||
@@ -67,6 +74,38 @@ const DIRTY_BUILD_KINDS = new Set([
|
|||||||
|
|
||||||
const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakSet<object>()
|
const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakSet<object>()
|
||||||
|
|
||||||
|
function canCreateWebGLContext() {
|
||||||
|
if (typeof document === 'undefined') return false
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
try {
|
||||||
|
return Boolean(canvas.getContext('webgl2') ?? canvas.getContext('webgl'))
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canMountGpuViewer() {
|
||||||
|
if (typeof window === 'undefined') return false
|
||||||
|
if (!('gpu' in navigator) && !canCreateWebGLContext()) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnsupportedGpuViewerFallback() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full min-h-64 w-full items-center justify-center bg-[#fafafa] p-6 text-center text-neutral-900">
|
||||||
|
<div className="max-w-md rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm">
|
||||||
|
<h2 className="font-semibold text-lg">3D viewer unavailable</h2>
|
||||||
|
<p className="mt-2 text-neutral-600 text-sm">
|
||||||
|
This browser or environment does not expose WebGPU or WebGL, so Pascal cannot render the
|
||||||
|
3D scene here. Try opening the editor in a browser with hardware acceleration enabled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renderer-level safety net against the empty-vertex-buffer crash.
|
* Renderer-level safety net against the empty-vertex-buffer crash.
|
||||||
*
|
*
|
||||||
@@ -349,6 +388,16 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
|||||||
}
|
}
|
||||||
}, [isolate])
|
}, [isolate])
|
||||||
|
|
||||||
|
const [rendererInitFailed, setRendererInitFailed] = useState(false)
|
||||||
|
// Capability detection runs after mount. We start optimistic (true) so the
|
||||||
|
// server-rendered markup and the first client render agree (no hydration
|
||||||
|
// mismatch); the effect flips it to false only on environments that expose
|
||||||
|
// neither WebGPU nor WebGL.
|
||||||
|
const [canMountViewer, setCanMountViewer] = useState(true)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canMountGpuViewer()) setCanMountViewer(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
||||||
const transparentBackground = useViewer((state) => state.transparentBackground)
|
const transparentBackground = useViewer((state) => state.transparentBackground)
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -401,6 +450,17 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
|||||||
// Desktops (fine pointer) keep the original 1.5 cap.
|
// Desktops (fine pointer) keep the original 1.5 cap.
|
||||||
const maxDpr =
|
const maxDpr =
|
||||||
typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5
|
typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5
|
||||||
|
const showGpuFallback = !canMountViewer || rendererInitFailed
|
||||||
|
// When we can't mount the GPU canvas, the SceneReadyTracker never mounts and
|
||||||
|
// the host editor would otherwise wait on its scene-readiness timeout. Signal
|
||||||
|
// readiness explicitly so the host can drop its loader immediately.
|
||||||
|
useEffect(() => {
|
||||||
|
if (showGpuFallback) onSceneReadyChange?.(true)
|
||||||
|
}, [showGpuFallback, onSceneReadyChange])
|
||||||
|
|
||||||
|
if (showGpuFallback) {
|
||||||
|
return <UnsupportedGpuViewerFallback />
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [50, 50, 50], fov: 50 }}
|
camera={{ position: [50, 50, 50], fov: 50 }}
|
||||||
@@ -430,6 +490,7 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
|||||||
// rejection forever.
|
// rejection forever.
|
||||||
if (canvas) WEBGPU_RENDERER_CACHE.delete(canvas)
|
if (canvas) WEBGPU_RENDERER_CACHE.delete(canvas)
|
||||||
console.error('[viewer] WebGPURenderer init failed', err)
|
console.error('[viewer] WebGPURenderer init failed', err)
|
||||||
|
setRendererInitFailed(true)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
Reference in New Issue
Block a user