From 892eb62391dce96aa5da43f722f38a261d704695 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 9 Jul 2026 20:20:59 -0400 Subject: [PATCH] fix(viewer): drive the frame loop with a timer when ?disable=draw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/components/viewer/frame-limiter.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/viewer/src/components/viewer/frame-limiter.tsx b/packages/viewer/src/components/viewer/frame-limiter.tsx index 11155d41f..cb0753170 100644 --- a/packages/viewer/src/components/viewer/frame-limiter.tsx +++ b/packages/viewer/src/components/viewer/frame-limiter.tsx @@ -6,6 +6,20 @@ type FrameLimiterProps = { 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 = ({ fps = 50 }) => { const { advance, set, frameloop: initFrameloop, scene, clock } = useThree() const renderer = useThree((state) => state.gl) @@ -18,6 +32,7 @@ const FrameLimiter: React.FC = ({ fps = 50 }) => { let then = 0 let i = 0 let raf: number | null = null + let timer: ReturnType | null = null const interval = 1000 / fps function tick(t: DOMHighResTimeStamp) { raf = requestAnimationFrame(tick) @@ -30,13 +45,23 @@ const FrameLimiter: React.FC = ({ fps = 50 }) => { } // Set frameloop to never, it will shut down the default render loop set({ frameloop: 'never' }) - // Kick off custom render loop - raf = requestAnimationFrame(tick) + if (DRAW_DISABLED) { + timer = setInterval(() => { + i += interval / 1000 + advance(i) + }, interval) + } else { + // Kick off custom render loop + raf = requestAnimationFrame(tick) + } // Restore initial setting return () => { if (raf) { cancelAnimationFrame(raf) } + if (timer) { + clearInterval(timer) + } set({ frameloop: initFrameloop }) } }, [fps, advance, set, initFrameloop, renderPaused])