feat(viewer): ?disable=draw — skip scene rendering for graph-only consumers (#482)
The headless bake worker renders on SwiftShader (CPU), where per-frame vertex/draw cost dominates the whole capture — but a bake only reads the built scene graph; the pixels are thrown away. `?disable=draw` (same diagnostic-param family as postFx) renders an EMPTY scene each frame instead of the real one: useFrame systems and scene-ready keep ticking, per-frame cost collapses to a 64x64 clear. Rendering nothing at all does NOT work: with zero submitted frames Chromium's no-damage scheduler throttles rAF to exactly 1Hz (measured), starving the very systems the bake needs — hence the empty-scene draw. Measured on the office scene (200+ items) with a real GPU: capture 45.0s -> 8.4s (settle 37.6s -> 4.5s); output equivalent to a normal render (structural GLB diff clean; the ±13-accessor variance exists between normal runs too). CPU-only (worker) numbers via the prod-image container against a preview deploy. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
352954aafc
commit
bbe0e2b0b5
@@ -1,6 +1,6 @@
|
|||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Color, Layers, type Object3D, UnsignedByteType } from 'three'
|
import { Color, Layers, type Object3D, Scene, UnsignedByteType } from 'three'
|
||||||
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
||||||
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
||||||
import {
|
import {
|
||||||
@@ -57,6 +57,11 @@ export const SSGI_PARAMS = {
|
|||||||
// - outline: skip the merged-outline node and its 14 internal RTs
|
// - outline: skip the merged-outline node and its 14 internal RTs
|
||||||
// - postFx: bypass the whole RenderPipeline and use renderer.render(scene, camera)
|
// - postFx: bypass the whole RenderPipeline and use renderer.render(scene, camera)
|
||||||
// directly — isolates raw scene-render cost from any post-FX overhead
|
// directly — isolates raw scene-render cost from any post-FX overhead
|
||||||
|
// - draw: skip the render call entirely — frames still tick (useFrame
|
||||||
|
// systems, scene-ready) but no draw is ever submitted. For
|
||||||
|
// consumers that only need the built scene graph, never pixels:
|
||||||
|
// the headless bake worker renders on SwiftShader (CPU), where
|
||||||
|
// per-frame vertex/draw cost dominates the whole capture.
|
||||||
function readPerfDisableFlags() {
|
function readPerfDisableFlags() {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return { ao: false, denoise: false, outline: false, postFx: false }
|
return { ao: false, denoise: false, outline: false, postFx: false }
|
||||||
@@ -84,6 +89,17 @@ const PERF_POST_FX_DISABLED =
|
|||||||
.map((s) => s.trim()),
|
.map((s) => s.trim()),
|
||||||
).has('postFx')
|
).has('postFx')
|
||||||
|
|
||||||
|
const PERF_DRAW_DISABLED =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
new Set(
|
||||||
|
(new URLSearchParams(window.location.search).get('disable') ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim()),
|
||||||
|
).has('draw')
|
||||||
|
|
||||||
|
// Stand-in scene for `?disable=draw` frames — cleared, never populated.
|
||||||
|
const emptyScene = new Scene()
|
||||||
|
|
||||||
const MAX_PIPELINE_RETRIES = 3
|
const MAX_PIPELINE_RETRIES = 3
|
||||||
const RETRY_DELAY_MS = 500
|
const RETRY_DELAY_MS = 500
|
||||||
|
|
||||||
@@ -562,6 +578,23 @@ const PostProcessingPasses = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `?disable=draw`: nothing downstream wants pixels — render an EMPTY scene
|
||||||
|
// instead of the real one. This is the only render call (positive-priority
|
||||||
|
// useFrame subscribers already disable R3F's automatic render), so the real
|
||||||
|
// scene is never drawn: per-frame vertex/draw cost drops to a single 64×64
|
||||||
|
// clear, which is what makes headless bakes viable on SwiftShader (CPU).
|
||||||
|
// Rendering nothing at all is NOT an option — with zero submitted frames
|
||||||
|
// Chromium's no-damage scheduler throttles rAF to 1Hz (measured), stalling
|
||||||
|
// the useFrame systems the bake still needs.
|
||||||
|
if (PERF_DRAW_DISABLED) {
|
||||||
|
try {
|
||||||
|
;(renderer as any).render(emptyScene, camera)
|
||||||
|
} catch {
|
||||||
|
// A failed empty draw changes nothing — systems keep ticking.
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Animate background colour toward the current scene theme target (same lerp as AnimatedBackground)
|
// Animate background colour toward the current scene theme target (same lerp as AnimatedBackground)
|
||||||
bgTarget.current.set(getSceneTheme(useViewer.getState().sceneTheme).background)
|
bgTarget.current.set(getSceneTheme(useViewer.getState().sceneTheme).background)
|
||||||
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
||||||
|
|||||||
Reference in New Issue
Block a user