fix(viewer): drive the frame loop with a timer when ?disable=draw (#483)

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>
This commit is contained in:
Wassim SAMAD
2026-07-09 20:25:09 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent bbe0e2b0b5
commit aa30899090
@@ -6,6 +6,20 @@ type FrameLimiterProps = {
fps?: number fps?: number
} }
// `?disable=draw` (see post-processing.tsx): the page renders no real frames,
// and Chromium's no-damage scheduler then throttles requestAnimationFrame to
// 1Hz on Linux (measured in the headless bake worker — every useFrame system
// ticked once per second and a heavy scene took 300+ seconds to settle). A
// plain timer is never throttled on a visible page, so drive the loop with
// setInterval instead of rAF when nothing is drawn.
const DRAW_DISABLED =
typeof window !== 'undefined' &&
new Set(
(new URLSearchParams(window.location.search).get('disable') ?? '')
.split(',')
.map((s) => s.trim()),
).has('draw')
const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => { const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
const { advance, set, frameloop: initFrameloop, scene, clock } = useThree() const { advance, set, frameloop: initFrameloop, scene, clock } = useThree()
const renderer = useThree((state) => state.gl) const renderer = useThree((state) => state.gl)
@@ -18,6 +32,7 @@ const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
let then = 0 let then = 0
let i = 0 let i = 0
let raf: number | null = null let raf: number | null = null
let timer: ReturnType<typeof setInterval> | null = null
const interval = 1000 / fps const interval = 1000 / fps
function tick(t: DOMHighResTimeStamp) { function tick(t: DOMHighResTimeStamp) {
raf = requestAnimationFrame(tick) raf = requestAnimationFrame(tick)
@@ -30,13 +45,23 @@ const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
} }
// Set frameloop to never, it will shut down the default render loop // Set frameloop to never, it will shut down the default render loop
set({ frameloop: 'never' }) set({ frameloop: 'never' })
// Kick off custom render loop if (DRAW_DISABLED) {
raf = requestAnimationFrame(tick) timer = setInterval(() => {
i += interval / 1000
advance(i)
}, interval)
} else {
// Kick off custom render loop
raf = requestAnimationFrame(tick)
}
// Restore initial setting // Restore initial setting
return () => { return () => {
if (raf) { if (raf) {
cancelAnimationFrame(raf) cancelAnimationFrame(raf)
} }
if (timer) {
clearInterval(timer)
}
set({ frameloop: initFrameloop }) set({ frameloop: initFrameloop })
} }
}, [fps, advance, set, initFrameloop, renderPaused]) }, [fps, advance, set, initFrameloop, renderPaused])