Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/viewer/src/components/viewer/post-processing.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useFrame, useThree } from '@react-three/fiber'
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 { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
import {
Expand Down Expand Up @@ -57,6 +57,11 @@ export const SSGI_PARAMS = {
// - outline: skip the merged-outline node and its 14 internal RTs
// - postFx: bypass the whole RenderPipeline and use renderer.render(scene, camera)
// 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() {
if (typeof window === 'undefined') {
return { ao: false, denoise: false, outline: false, postFx: false }
Expand Down Expand Up @@ -84,6 +89,17 @@ const PERF_POST_FX_DISABLED =
.map((s) => s.trim()),
).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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Draw flag skips pipeline guard

Medium Severity

When ?disable=draw is active, the post-processing pipeline's useEffect still builds and allocates the full WebGPU RenderPipeline. This creates unnecessary memory overhead and can slow startup for headless bakes, since the useFrame loop then bypasses this unused pipeline.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e339147. Configure here.

const MAX_PIPELINE_RETRIES = 3
const RETRY_DELAY_MS = 500

Expand Down Expand Up @@ -562,6 +578,23 @@ const PostProcessingPasses = ({
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.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swallowed empty draw failures

Medium Severity

The ?disable=draw path wraps render(emptyScene, camera) in a bare catch with no logging. If that draw fails every frame, the PR’s rAF-throttling workaround may not run, while the fallback direct-render path logs failures — making headless capture stalls hard to diagnose.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e339147. Configure here.

return
}

// Animate background colour toward the current scene theme target (same lerp as AnimatedBackground)
bgTarget.current.set(getSceneTheme(useViewer.getState().sceneTheme).background)
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
Expand Down
Loading