fix: use WebGL fallback for Safari scenes
This commit is contained in:
parent
47067bf278
commit
62a6a4a7f0
@ -22,6 +22,7 @@ import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf'
|
||||
import { applyIsolation, clearIsolation } from '../../lib/isolation'
|
||||
import { ensureKtx2Support } from '../../lib/ktx2-loader'
|
||||
import type { ColorPreset, RenderShading } from '../../lib/materials'
|
||||
import { shouldForceWebGLRenderer } from '../../lib/renderer-backend'
|
||||
import { getSceneTheme } from '../../lib/scene-themes'
|
||||
import useViewer, { type RenderContext } from '../../store/use-viewer'
|
||||
import { FloorElevationSystem } from '../../systems/floor-elevation/floor-elevation-system'
|
||||
@ -496,6 +497,12 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
||||
// Desktops (fine pointer) keep the original 1.5 cap.
|
||||
const maxDpr =
|
||||
typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5
|
||||
const forceWebGL =
|
||||
typeof window !== 'undefined' &&
|
||||
shouldForceWebGLRenderer({
|
||||
search: window.location.search,
|
||||
userAgent: window.navigator.userAgent,
|
||||
})
|
||||
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
|
||||
@ -522,12 +529,20 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
||||
if (cached) return cached
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const renderer = new THREE.WebGPURenderer({ ...(props as any), alpha: true })
|
||||
const renderer = new THREE.WebGPURenderer({
|
||||
...(props as any),
|
||||
alpha: true,
|
||||
forceWebGL,
|
||||
})
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
renderer.toneMappingExposure = getSceneTheme(
|
||||
useViewer.getState().sceneTheme,
|
||||
).toneMappingExposure
|
||||
await renderer.init()
|
||||
if (canvas) {
|
||||
canvas.dataset.pascalRenderer =
|
||||
(renderer as any).backend?.isWebGPUBackend === true ? 'webgpu' : 'webgl'
|
||||
}
|
||||
installEmptyDrawGuard(renderer)
|
||||
return renderer
|
||||
} catch (err) {
|
||||
|
||||
@ -327,14 +327,14 @@ 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
|
||||
// WebGPU-only APIs. When the renderer uses its WebGL2 fallback (because
|
||||
// WebGPU is unavailable, unstable, or explicitly disabled), 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' && 'gpu' in navigator
|
||||
const hasWebGPU = (renderer as any).backend?.isWebGPUBackend === true
|
||||
if (!hasWebGPU) {
|
||||
hasPipelineErrorRef.current = true
|
||||
renderPipelineRef.current = null
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
// @ts-expect-error - bun:test is provided by the Bun runtime; viewer does not
|
||||
// depend on @types/bun so the import type is unresolved at compile time.
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { isSafariUserAgent, shouldForceWebGLRenderer } from './renderer-backend'
|
||||
|
||||
const SAFARI_MAC =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15'
|
||||
const CHROME_MAC =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36'
|
||||
|
||||
describe('renderer backend selection', () => {
|
||||
test('identifies desktop Safari without classifying Chrome as Safari', () => {
|
||||
expect(isSafariUserAgent(SAFARI_MAC)).toBe(true)
|
||||
expect(isSafariUserAgent(CHROME_MAC)).toBe(false)
|
||||
})
|
||||
|
||||
test('uses WebGL for Safari and leaves Chrome on the default backend', () => {
|
||||
expect(shouldForceWebGLRenderer({ search: '', userAgent: SAFARI_MAC })).toBe(true)
|
||||
expect(shouldForceWebGLRenderer({ search: '', userAgent: CHROME_MAC })).toBe(false)
|
||||
})
|
||||
|
||||
test('honors an explicit renderer override', () => {
|
||||
expect(shouldForceWebGLRenderer({ search: '?renderer=webgpu', userAgent: SAFARI_MAC })).toBe(
|
||||
false,
|
||||
)
|
||||
expect(shouldForceWebGLRenderer({ search: '?renderer=webgl', userAgent: CHROME_MAC })).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,18 @@
|
||||
export interface RendererBackendContext {
|
||||
search: string
|
||||
userAgent: string
|
||||
}
|
||||
|
||||
const SAFARI_USER_AGENT = /Safari\//i
|
||||
const NON_SAFARI_BROWSER = /(?:Chrome|Chromium|CriOS|Edg|EdgiOS|OPR|OPiOS|Android)\//i
|
||||
|
||||
export function isSafariUserAgent(userAgent: string): boolean {
|
||||
return SAFARI_USER_AGENT.test(userAgent) && !NON_SAFARI_BROWSER.test(userAgent)
|
||||
}
|
||||
|
||||
export function shouldForceWebGLRenderer({ search, userAgent }: RendererBackendContext): boolean {
|
||||
const override = new URLSearchParams(search).get('renderer')?.toLowerCase()
|
||||
if (override === 'webgl') return true
|
||||
if (override === 'webgpu') return false
|
||||
return isSafariUserAgent(userAgent)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user