From a3f611e2277ae089c4e54f1f324f5848ea51fd95 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Sat, 30 May 2026 20:58:58 +0200 Subject: [PATCH] fix(webgl): auto-restore context after loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Canvas onCreated callback used to log Context Lost but never asked the GPU to restore it, which left the page on a frozen black canvas until the user reloaded. We now grab the WEBGL_lose_context extension on mount and call restoreContext() 500ms after a loss, giving the GPU time to free memory before we ask for a new context. The existing webglcontextrestored handler reinstates the shadow map settings, so recovery is transparent to the user. This does not prevent context loss itself — frequent losses still indicate VRAM pressure or HMR-driven context churn — but it removes the need to reload manually when the GPU recycles us. --- src/pages/page.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/page.tsx b/src/pages/page.tsx index 8196054..e4dcd72 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -96,9 +96,16 @@ export function HomePage(): React.JSX.Element | null { gl.shadowMap.type = THREE.PCFShadowMap; gl.shadowMap.autoUpdate = true; + // The browser hands us a WEBGL_lose_context extension we can use to + // ask the GPU to restore the context after a loss. Without this the + // page stays frozen on a black canvas until the user reloads. + const loseContextExt = gl.getContext().getExtension("WEBGL_lose_context"); + const handleContextLost = (event: Event) => { event.preventDefault(); - logger.error("WebGL", "Context lost - GPU resources exhausted"); + logger.error("WebGL", "Context lost - attempting auto-restore"); + // Give the GPU a moment to free resources before asking it back. + window.setTimeout(() => loseContextExt?.restoreContext(), 500); }; const handleContextRestored = () => { @@ -121,10 +128,14 @@ export function HomePage(): React.JSX.Element | null { // all hooks (rules of hooks) but BEFORE any expensive render. if (!hasSiteBeenVisitedToday()) return null; + const showFadeToVideoOverlay = + introStep === "fade-to-video" || + (introStep === "loading-map" && sceneLoadingState.status === "ready"); + const renderIntroOverlay = () => { + if (showFadeToVideoOverlay) return ; + switch (introStep) { - case "fade-to-video": - return ; case "video": return ; case "dialogue-intro":