Skip to content

fix(viewport): eliminate intermittent white screen (demand-frame + dead-context)#236

Merged
ecto merged 3 commits into
mainfrom
fix-white-screen-context-restore
Jun 5, 2026
Merged

fix(viewport): eliminate intermittent white screen (demand-frame + dead-context)#236
ecto merged 3 commits into
mainfrom
fix-white-screen-context-restore

Conversation

@ecto

@ecto ecto commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Problem

The 3D viewport intermittently showed a blank/white rectangle — geometry present in the scene, but nothing drawn.

Root causes (two independent failures)

1. Missing invalidate() under frameloop="demand".
SceneMesh/ImportedMesh populate their geometry buffers imperatively in an effect (geo.setAttribute(...)). R3F's reconciler never observes that mutation, so no frame is scheduled — the scene sits populated-but-unpainted until some unrelated event (orbit, resize, hover) happens to kick a frame. This is the common case (plain reload, returning from the schematic overlay, tab refocus).

2. Dead WebGL context with no recovery.
On a context loss that never restores, or a context that fails to create at all (GPU exhaustion / too many live contexts — onCreated never fires and no webglcontextlost event is emitted), the canvas stayed blank permanently.

Fixes

  • Per-mesh kick: invalidate() immediately after the imperative buffer/transform write in SceneMesh + ImportedMesh.
  • Initial-paint burst + reactive kicks in ViewportContent: a self-terminating rAF pump over the first ~600ms (covers the async engine scene, EffectComposer mount, and canvas sizing all settling), plus invalidation on engine-scene swap and on visibility/focus transitions.
  • Context recovery in Viewport: remount the <Canvas> via a changing key — triggered by a stuck-loss event from useWebGLContextLost and a failed-init watchdog (onCreated not firing in time). Capped + budget-forgiveness so a context that genuinely can't be created can't spin.
  • Repaint on webglcontextrestored (original commit).

Verification

Diagnosis confirmed live: on a healthy-but-blank viewport, a single invalidate() (via a synthetic resize) repaints the scene permanently — proving the geometry was present and only the frame was missing. Full live re-verification of the load path was blocked by WebGL-context exhaustion in the shared preview browser (multiple concurrent dev servers), so the fix is locked in with source-structural regression guards in demand-rendering.test.ts (12 pass; full app suite 28 pass).

🤖 Generated with Claude Code

The viewport runs frameloop="demand" with a transparent canvas, so it
only paints when something calls invalidate(). Browsers drop the WebGL
context intermittently — WebKit especially, under GPU memory pressure,
with too many live contexts, or when a tab is backgrounded and restored.
On webglcontextrestored three.js re-initialises and the fragile GPU
subtrees remount, but nothing scheduled a frame — so the transparent
canvas stayed unpainted and the page background showed through as a
"pure white screen" until the next user interaction invalidated a frame.

useWebGLContextLost now invalidates when the context returns (now and on
the next rAF, after subtrees remount), so the scene reappears on its
own. Also covers a context that was already lost at mount and later
restored.

tsc + app build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
mecheval Ignored Ignored Jun 5, 2026 12:42am
vcad Ignored Ignored Jun 5, 2026 12:42am
vcad-docs Ignored Ignored Jun 5, 2026 12:42am
vcad-mcp Ignored Ignored Jun 5, 2026 12:42am

Request Review

Root-causes the intermittent white viewport. Two independent failures:

1. Missing invalidate under frameloop="demand". SceneMesh/ImportedMesh
   populate their geometry buffers imperatively in an effect
   (geo.setAttribute(...)), which R3F's reconciler never observes — so no
   frame is scheduled and the scene sits populated-but-unpainted until an
   unrelated event (orbit, resize, hover) happens to kick one. Fix: kick
   invalidate() right after the buffer write, plus an initial-paint burst +
   reactive scene/visibility kicks in ViewportContent so a freshly hydrated
   or swapped scene always paints on its own.

2. Dead WebGL context with no recovery. On a genuine context loss that
   doesn't restore, or a context that fails to create at all (GPU
   exhaustion / too many live contexts — onCreated never fires, no
   webglcontextlost event), the canvas stayed a blank rectangle forever.
   Fix: remount the <Canvas> via a changing key — driven by a stuck-loss
   event from useWebGLContextLost and a failed-init watchdog in Viewport —
   capped so a context that truly can't be created can't spin.

Verified the diagnosis live (a single invalidate via a synthetic resize
repaints a healthy-but-blank viewport permanently). Adds source-structural
regression guards to demand-rendering.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ecto ecto changed the title fix(viewport): repaint after WebGL context restore (white screen) fix(viewport): eliminate intermittent white screen (demand-frame + dead-context) Jun 5, 2026
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ecto ecto merged commit 811f45c into main Jun 5, 2026
10 checks passed
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