feat(viewer): ?disable=draw — skip scene rendering for graph-only consumers#482
Conversation
…sumers 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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e339147. Configure here.
|
|
||
| // Stand-in scene for `?disable=draw` frames — cleared, never populated. | ||
| const emptyScene = new Scene() | ||
|
|
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit e339147. Configure here.
| ;(renderer as any).render(emptyScene, camera) | ||
| } catch { | ||
| // A failed empty draw changes nothing — systems keep ticking. | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit e339147. Configure here.
With no real frames submitted, Chromium's no-damage scheduler throttles requestAnimationFrame to 1Hz on Linux — measured in the headless bake worker: every useFrame system ticked once per second, so a heavy scene needed 300+ seconds of wall time just to run ~300 frames of build work (and exceeded the capture deadline on the worker's slower cores). The empty-scene clear from #482 avoids this on macOS but not on Linux. When `draw` is disabled, drive advance() with setInterval instead of rAF — plain timers are never throttled on a visible page, so the loop pacing is deterministic (fps prop) regardless of compositor heuristics. Normal rendering paths are untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>


What does this PR do?
Adds a
drawtoken to the existing?disable=diagnostic params: the frame loop keeps running (alluseFramesystems, scene-ready), but the real scene is never drawn — each frame renders a shared empty scene instead (a 64×64 clear). For consumers that only need the built scene graph — the headless bake worker, which renders on SwiftShader (CPU) — this removes the dominant cost of the whole capture: per-frame vertex transform + draw submission of millions of vertices whose pixels nobody ever looks at.Why an empty-scene render instead of skipping the render call: with zero submitted frames, Chromium's no-damage scheduler throttles
requestAnimationFrameto exactly 1Hz (measured in the prod worker image), which starves the very systems the bake needs.Measured (office scene, 200+ items, real GPU): capture 45.0s → 8.4s, settle 37.6s → 4.5s. Output equivalent to a normal render — structural GLB diff shows identical nodes/meshes/materials/textures; the small accessor-count variance observed also occurs between two normal renders.
How to test
bun dev, open/bake/demo_1?disable=postFx,draw— the canvas stays blank (expected; nothing draws) butwindow.__BAKE__resolves with a valid GLB, much faster than withoutdraw.draw— unchanged behavior.Screenshots / screen recording
N/A — the change's entire purpose is that nothing is drawn; validation is the exported GLB equivalence above.
Checklist
bun devbun checkto verify)mainbranch🤖 Generated with Claude Code
Note
Low Risk
Opt-in URL flag with a narrow early-return in the post-processing render path; normal viewer/editor traffic is unaffected when the param is absent.
Overview
Adds
drawto the existing?disable=perf diagnostics inpost-processing.tsx. When enabled,useFramestill runs (theme uniforms, outliner sanitization, and other systems keep ticking) but the real scene and post pipeline are bypassed: each frame callsrenderer.render(emptyScene, camera)and returns early.This targets graph-only consumers (e.g. the headless bake worker on SwiftShader) where per-frame vertex/draw work dominated capture time but pixels were unused. Skipping the render call entirely was rejected because Chromium can throttle
requestAnimationFrameto ~1Hz when no frames are submitted, which would stall bake logic that depends onuseFrame.Default behavior is unchanged unless
drawappears in the URL; it composes with flags likepostFx(e.g.?disable=postFx,draw).Reviewed by Cursor Bugbot for commit e339147. Bugbot is set up for automated code reviews on this repo. Configure here.