Skip to content

Commit bbe0e2b

Browse files
wass08claude
andauthored
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>
1 parent 352954a commit bbe0e2b

1 file changed

Lines changed: 34 additions & 1 deletion

File tree

packages/viewer/src/components/viewer/post-processing.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useFrame, useThree } from '@react-three/fiber'
22
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3-
import { Color, Layers, type Object3D, UnsignedByteType } from 'three'
3+
import { Color, Layers, type Object3D, Scene, UnsignedByteType } from 'three'
44
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
55
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
66
import {
@@ -57,6 +57,11 @@ export const SSGI_PARAMS = {
5757
// - outline: skip the merged-outline node and its 14 internal RTs
5858
// - postFx: bypass the whole RenderPipeline and use renderer.render(scene, camera)
5959
// directly — isolates raw scene-render cost from any post-FX overhead
60+
// - draw: skip the render call entirely — frames still tick (useFrame
61+
// systems, scene-ready) but no draw is ever submitted. For
62+
// consumers that only need the built scene graph, never pixels:
63+
// the headless bake worker renders on SwiftShader (CPU), where
64+
// per-frame vertex/draw cost dominates the whole capture.
6065
function readPerfDisableFlags() {
6166
if (typeof window === 'undefined') {
6267
return { ao: false, denoise: false, outline: false, postFx: false }
@@ -84,6 +89,17 @@ const PERF_POST_FX_DISABLED =
8489
.map((s) => s.trim()),
8590
).has('postFx')
8691

92+
const PERF_DRAW_DISABLED =
93+
typeof window !== 'undefined' &&
94+
new Set(
95+
(new URLSearchParams(window.location.search).get('disable') ?? '')
96+
.split(',')
97+
.map((s) => s.trim()),
98+
).has('draw')
99+
100+
// Stand-in scene for `?disable=draw` frames — cleared, never populated.
101+
const emptyScene = new Scene()
102+
87103
const MAX_PIPELINE_RETRIES = 3
88104
const RETRY_DELAY_MS = 500
89105

@@ -562,6 +578,23 @@ const PostProcessingPasses = ({
562578
return
563579
}
564580

581+
// `?disable=draw`: nothing downstream wants pixels — render an EMPTY scene
582+
// instead of the real one. This is the only render call (positive-priority
583+
// useFrame subscribers already disable R3F's automatic render), so the real
584+
// scene is never drawn: per-frame vertex/draw cost drops to a single 64×64
585+
// clear, which is what makes headless bakes viable on SwiftShader (CPU).
586+
// Rendering nothing at all is NOT an option — with zero submitted frames
587+
// Chromium's no-damage scheduler throttles rAF to 1Hz (measured), stalling
588+
// the useFrame systems the bake still needs.
589+
if (PERF_DRAW_DISABLED) {
590+
try {
591+
;(renderer as any).render(emptyScene, camera)
592+
} catch {
593+
// A failed empty draw changes nothing — systems keep ticking.
594+
}
595+
return
596+
}
597+
565598
// Animate background colour toward the current scene theme target (same lerp as AnimatedBackground)
566599
bgTarget.current.set(getSceneTheme(useViewer.getState().sceneTheme).background)
567600
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)

0 commit comments

Comments
 (0)