feat(viewer): make the render-loop frame cap configurable via maxFps - #671
feat(viewer): make the render-loop frame cap configurable via maxFps#671ztffn wants to merge 1 commit into
maxFps#671Conversation
The viewer runs `frameloop="never"` and advances frames itself through
`<FrameLimiter fps={50} />`, where the 50 was hardcoded. That makes the cap
unreachable from outside: a host cannot raise it, because it does not own the
loop.
That is fine for editing, where 50 is a sensible way to spare the GPU. It is a
ceiling for hosts that animate the scene on their own clock — a timeline
scrubbing node transforms, a walkthrough camera — which are pinned below display
refresh and read as judder on a 60Hz+ monitor. It also gives passive or
background canvases no way to ask for less.
`maxFps` defaults to 50, so existing behaviour is byte-identical. `fps` is
already in FrameLimiter's effect dependencies, so changing it at runtime takes
effect without further work.
bun check clean; bun run test green (3071 tests, 8 packages).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f170274. Configure here.
| }} | ||
| > | ||
| <FrameLimiter fps={50} /> | ||
| <FrameLimiter fps={maxFps} /> |
There was a problem hiding this comment.
Runtime maxFps resets the clock
Medium Severity
Threading maxFps into FrameLimiter lets fps change at runtime. On change, FrameLimiter's effect remounts and resets its manual clock i to 0 while R3F still holds the prior elapsedTime, so advance yields a large negative delta. useFrame consumers can hitch or step backward—especially when raising or lowering the cap for active vs background canvases.
Reviewed by Cursor Bugbot for commit f170274. Configure here.


What
Adds a
maxFpsprop to<Viewer>, threaded to the existing<FrameLimiter>. Defaults to50, so current behaviour is unchanged.Why
The viewer runs
frameloop="never"and advances frames itself through<FrameLimiter fps={50} />. Because the viewer owns the loop, that cap is unreachable from outside — a host has no way to raise or lower it.That default is a good one for editing, where 50 spares the GPU on a mostly-static canvas. It becomes a ceiling for hosts that animate the scene on their own clock. I hit this integrating a timeline library that drives node transforms per frame: motion is capped at 50fps and reads as judder against a 60Hz+ monitor, with no prop to turn. The reverse case matters too — a passive or background canvas has no way to ask for fewer frames.
Notes
fpsis already inFrameLimiter's effect dependency array, so changingmaxFpsat runtime re-runs the effect and takes effect immediately. No other change needed.maxFpsrather thanfpsto read as a ceiling at the<Viewer>level, sinceFrameLimitertreats it as one. Happy to rename if you'd prefer it match the inner prop.Verification
bun check— cleanbun run test— 3071 tests across 8 packages, 0 failurespackages/viewertsc --build— clean, andmaxFps?: numberis emitted into the published declarations🤖 Generated with Claude Code
Note
Low Risk
Single prop with default preserving prior behavior; no auth, data, or pipeline changes beyond frame pacing.
Overview
Adds an optional
maxFpsprop on<Viewer>(default 50) and wires it to<FrameLimiter fps={…} />, replacing the hard-coded 50fps cap.Because the canvas uses
frameloop="never", hosts could not previously raise or lower loop cadence for timeline-driven animation (judder vs 60Hz+) or passive/background canvases (GPU savings). Documented JSDoc explains when to tune it; runtime changes tomaxFpsalready propagate throughFrameLimiter’s existingfpseffect dependency.Reviewed by Cursor Bugbot for commit f170274. Bugbot is set up for automated code reviews on this repo. Configure here.