Skip to content

fix(viewer): kick render on focus and visibility resume#291

Open
mvanhorn wants to merge 1 commit intopascalorg:mainfrom
mvanhorn:fix/viewer-blank-on-idle
Open

fix(viewer): kick render on focus and visibility resume#291
mvanhorn wants to merge 1 commit intopascalorg:mainfrom
mvanhorn:fix/viewer-blank-on-idle

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented May 5, 2026

What does this PR do?

frameloop="never" makes FrameLimiter's requestAnimationFrame loop the only render driver. Browsers throttle rAF when the tab is hidden, the window is unfocused, or the system marks the tab as occluded. When that happens advance() stops firing and the canvas stays whatever was last in the swap chain (blank on first paint, or the previous frame on subsequent freezes).

This adds three event listeners alongside the existing rAF loop. visibilitychange (transitioning to visible), window focus, and pageshow each call advance() synchronously so the next visible frame matches the current scene state without waiting for rAF to resume.

The fix is contained to packages/viewer/src/components/viewer/frame-limiter.tsx (+21/-0). No new imports. No layer-boundary changes.

Fixes #275
Closes #196

Why prior fixes don't cover this

None of those address rAF throttling. Once the throttle hits, every render path stalls because useFrame only runs when advance() is called.

How to test

  1. bun install && bun dev, open the editor in Chrome or Firefox.
  2. Click the Pascal tab into focus. The viewer renders.
  3. Switch to a different tab for 5 seconds, then switch back to Pascal.

Without the fix, the viewer can stay frozen (blank or stale) until you move the mouse over the canvas. With the fix, the viewer renders the current scene immediately on tab return.

A second reproducer: open Pascal in a focused tab, drag another window over Pascal so it occludes the canvas, then move that window away. Some Linux WMs (and Zen on Windows) trigger the same rAF throttle that this fix recovers from.

Screenshots / screen recording

Before/after recorded locally on macOS Chrome 147 in the chrome MCP harness, which holds document.visibilityState === 'hidden' so the throttle path fires reliably. Frame 1 is the broken state on reload (blank canvas, faded toolbar). The click triggers a focus event, the new handler calls advance(), the scene renders.

fix-275-before-after

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (bun check passes)
  • I've updated relevant documentation (if applicable) - n/a, the inline comment in frame-limiter.tsx documents the rationale
  • This PR targets the main branch

`frameloop="never"` makes FrameLimiter's requestAnimationFrame loop
the only render driver. Browsers throttle rAF when the tab is hidden,
the window is unfocused, or the system marks the tab as occluded; when
that happens advance() stops firing and the canvas freezes on whatever
was last in the swap chain (blank on first paint, or the previous
frame on subsequent freezes).

Listen to visibilitychange (visible), focus, and pageshow alongside
the existing rAF loop. Each event calls advance() synchronously so the
next visible frame matches the current scene state without waiting for
rAF to resume.

Fixes pascalorg#275
Closes pascalorg#196
@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented May 5, 2026

Before/after recorded locally on macOS Chrome 147 in the chrome MCP harness, which holds document.visibilityState === 'hidden' so the throttle path fires reliably.

Frame 1 is the broken state on reload (blank canvas, faded toolbar). The click triggers a focus event, the new handler calls advance(), the scene renders.

fix-275-before-after

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.

The view is not displaying properly(mac os) Screen repaint issue

1 participant