fix(viewport): eliminate intermittent white screen (demand-frame + dead-context)#236
Merged
Conversation
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>
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>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()underframeloop="demand".SceneMesh/ImportedMeshpopulate 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 —
onCreatednever fires and nowebglcontextlostevent is emitted), the canvas stayed blank permanently.Fixes
invalidate()immediately after the imperative buffer/transform write inSceneMesh+ImportedMesh.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.Viewport: remount the<Canvas>via a changingkey— triggered by a stuck-loss event fromuseWebGLContextLostand a failed-init watchdog (onCreatednot firing in time). Capped + budget-forgiveness so a context that genuinely can't be created can't spin.webglcontextrestored(original commit).Verification
Diagnosis confirmed live: on a healthy-but-blank viewport, a single
invalidate()(via a syntheticresize) 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 indemand-rendering.test.ts(12 pass; full app suite 28 pass).🤖 Generated with Claude Code