From f9f3d3db24cda914d343043f3426d85483889500 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sun, 31 May 2026 19:02:27 +0800 Subject: [PATCH] fix(viewer): mount/unmount the zoom viewer instead of Activity hide/show MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full-screen WebGL zoom viewer was wrapped in `` to show/ hide it. React `` preserves the child's DOM (the same ``) and state, but runs the child's effect cleanup on hide and re-runs effects on show. So closing the zoom fires WebGLImageViewer's effect cleanup → the FU-13 `destroy()` → `loseContext()`, which permanently loses the (reused) canvas's WebGL context and nulls the engine ref. On the second open the effect re-runs, but `isInitialized` state is preserved across Activity hide/show, so `setUpWebGLEngine`'s `if (isInitialized) return` guard skips rebuilding the engine — leaving a null engine on a canvas whose context is dead. The engine (when it does run on the lost context) gets a null program from createProgram() and throws in attachShader → blank image + full-page crash on the 2nd zoom. Reproduced via devtools: open#1 context alive → close `lost=1` → open#2 creates no new context and `ctxAlive=false`. Fix: conditionally MOUNT/UNMOUNT the viewer (`{showFullScreenViewer && ...}`) instead of toggling ``. Every open is a fresh WebGLImageViewer → fresh `` → fresh context → fresh `isInitialized` → the engine builds normally. Every close is a real unmount, so the FU-13 `destroy()`/`loseContext()` runs on a canvas that is being discarded — correct, and the leak fix is fully preserved (open/close still create+release exactly one context each). The only trade-off is that zoom/pan position resets on each open, which is expected for a lightbox. Regression introduced by FU-13 (#503), whose 10-cycle test only covered the album→detail→zoom→navigate-away (true unmount) path, not detail-page open→close→reopen (Activity hide/show, reused canvas). Co-Authored-By: Claude Opus 4.8 --- components/album/progressive-image.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/components/album/progressive-image.tsx b/components/album/progressive-image.tsx index a2e08327..1ea6ec73 100644 --- a/components/album/progressive-image.tsx +++ b/components/album/progressive-image.tsx @@ -191,8 +191,17 @@ export default function ProgressiveImage( }} /> - - {webGLAvailable ?
hidden/visible. preserves the DOM and + component state but runs the child's effect cleanup on hide — which + fires the WebGL engine's destroy()/loseContext(). Because the same + canvas is reused and `isInitialized` state is preserved, re-opening + never rebuilds the engine and lands on a permanently-lost context → + blank image + crash on the 2nd open. Mount/unmount gives every open a + fresh canvas + context (and destroy() on a discarded canvas stays + correct), fixing the crash while keeping the FU-13 leak fix intact. */} + {showFullScreenViewer && ( + webGLAvailable ?
{ // 点击背景关闭 @@ -283,8 +292,8 @@ export default function ProgressiveImage( src={highResImageUrl} alt={props.alt || 'image'} /> -
} -
+
+ )} ) : null}