Skip to content

feat(viewer): make the render-loop frame cap configurable via maxFps - #671

Open
ztffn wants to merge 1 commit into
pascalorg:mainfrom
ztffn:feat/viewer-max-fps
Open

feat(viewer): make the render-loop frame cap configurable via maxFps#671
ztffn wants to merge 1 commit into
pascalorg:mainfrom
ztffn:feat/viewer-max-fps

Conversation

@ztffn

@ztffn ztffn commented Aug 17, 2026

Copy link
Copy Markdown

What

Adds a maxFps prop to <Viewer>, threaded to the existing <FrameLimiter>. Defaults to 50, 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

  • fps is already in FrameLimiter's effect dependency array, so changing maxFps at runtime re-runs the effect and takes effect immediately. No other change needed.
  • One file touched, one behavioural line.
  • I named it maxFps rather than fps to read as a ceiling at the <Viewer> level, since FrameLimiter treats it as one. Happy to rename if you'd prefer it match the inner prop.

Verification

  • bun check — clean
  • bun run test — 3071 tests across 8 packages, 0 failures
  • packages/viewer tsc --build — clean, and maxFps?: number is 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 maxFps prop 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 to maxFps already propagate through FrameLimiter’s existing fps effect dependency.

Reviewed by Cursor Bugbot for commit f170274. Bugbot is set up for automated code reviews on this repo. Configure here.

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).

@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 1 potential issue.

Fix All in Cursor

❌ 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} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f170274. Configure here.

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