Skip to content

feat(viewer): ?disable=draw — skip scene rendering for graph-only consumers#482

Merged
wass08 merged 1 commit into
mainfrom
feat/bake-disable-draw
Jul 9, 2026
Merged

feat(viewer): ?disable=draw — skip scene rendering for graph-only consumers#482
wass08 merged 1 commit into
mainfrom
feat/bake-disable-draw

Conversation

@wass08

@wass08 wass08 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds a draw token to the existing ?disable= diagnostic params: the frame loop keeps running (all useFrame systems, 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 requestAnimationFrame to 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

  1. bun dev, open /bake/demo_1?disable=postFx,draw — the canvas stays blank (expected; nothing draws) but window.__BAKE__ resolves with a valid GLB, much faster than without draw.
  2. Same page without draw — unchanged behavior.
  3. Editor/viewer surfaces without the param — unchanged (the flag is read once from the URL, default off).

Screenshots / screen recording

N/A — the change's entire purpose is that nothing is drawn; validation is the exported GLB equivalence above.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 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 draw to the existing ?disable= perf diagnostics in post-processing.tsx. When enabled, useFrame still runs (theme uniforms, outliner sanitization, and other systems keep ticking) but the real scene and post pipeline are bypassed: each frame calls renderer.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 requestAnimationFrame to ~1Hz when no frames are submitted, which would stall bake logic that depends on useFrame.

Default behavior is unchanged unless draw appears in the URL; it composes with flags like postFx (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.

…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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()

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.

;(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.

@wass08 wass08 merged commit bbe0e2b into main Jul 9, 2026
2 checks passed
wass08 added a commit that referenced this pull request Jul 10, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant