fix(webgl2): evict and rebuild GL handles on real context restore (B-09, #275)#285
Merged
Conversation
…09, #275) On a genuine WebGL2 context loss (mobile tab-switch, driver TDR) the backend only flipped a flag and called restoreContext(); the cached GL handles (_textureStates, _renderTargetStates, the shared transform texture, renderer buffers/VAOs/shader programs, compositors, retained bundles) were never invalidated. After a real restore every one of those handles is dead, so the next frame drew with dangling handles -> blank canvas or an INVALID_OPERATION storm. Synthetic signal-only tests passed because they never exercised the rebuild path. Root cause (two independent bugs): - The `webglcontextlost` handler never called `event.preventDefault()`, so the browser never scheduled a `webglcontextrestored` event — a real loss was permanent. It also called `restoreContext()` synchronously inside the lost handler, which Chromium ignores, and via a fresh `getExtension()` result; `restoreContext()` only works on the extension instance that is live from before the loss. - The `webglcontextrestored` handler evicted nothing beyond the retained bundles, leaving all other caches pointing at dead handles. Approach (mirrors WebGPU's `_teardownDeviceState`, rated production-grade in the audit): - `_onContextLost` now cancels the default action so the browser can restore, and schedules the extension-driven restore on a fresh task using the extension instance cached in the constructor (pre-loss). - `_onContextRestored` delegates to a new `_reinitializeDeviceState()` that re-enables EXT_color_buffer_float, evicts every managed texture/render-target handle, drops the transform texture, disconnects+reconnects all renderers (rebuilding buffers/VAOs/programs and resetting the VAO appliedVersion), disconnects the compositors/stencil clipper, invalidates the retained bundles, resets the cached GL bind state, re-applies the GL global state, and drains benign teardown errors so the app's own getError() starts clean. - destroy() now cancels any pending restore and guards against a late callback. Tests: - Browser lane (webgl2-context-restore.test.ts): drives a REAL WEBGL_lose_context lose/restore cycle and asserts the scene renders identical pixels afterwards (plus a two-cycle + fresh-texture-after-loss case). Green on Chromium and Firefox. - Node lane (context-restore-webgl2.test.ts): pins the invalidation bookkeeping against the recording fake context — preventDefault on loss, cache eviction, fresh handle identity after restore, draw-call resumption, and GPU-memory accounting release/re-accrual. Fixes #275 Claude-Session: https://claude.ai/code/session_01RHB7cLgoonJHLQg9ScdXby
Bundle ReportChanges will increase total bundle size by 15.76kB (0.06%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: exo-esm-modules-esmAssets Changed:
Files in
view changes for bundle: exo-iife-min-Exo-iifeAssets Changed:
Files in
view changes for bundle: exo-esm-esmAssets Changed:
Files in
view changes for bundle: exo-full-iife-Exo-iifeAssets Changed:
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
Summary
Fixes #275 (audit finding B-09, P2). On a genuine WebGL2 context loss (mobile tab-switch, driver TDR) the backend only flipped a flag and called
restoreContext(); the cached GL handles were never invalidated. After a real restore every handle is dead, so the next frame drew with dangling handles → blank canvas or anINVALID_OPERATIONstorm. The pre-existing synthetic signal-only tests passed because they never exercised the rebuild path.Root cause (two independent bugs)
webglcontextlosthandler never calledevent.preventDefault(), so per the WebGL spec the browser never scheduled awebglcontextrestoredevent — a real loss was permanent. It also calledrestoreContext()synchronously inside the lost handler (Chromium silently ignores that) and via a freshgetExtension()result. Empirically verified against headless Chromium + Firefox:restoreContext()only drives a restore when invoked on the live extension instance obtained before the loss, on a later task._onContextRestoredinvalidated only the retained instruction-set bundles;_textureStates,_renderTargetStates, the shared transform texture, renderer buffers/VAOs/shader programs, and the compositors all kept dead handles.Approach
Mirrors the WebGPU backend's
_teardownDeviceState(the audit rated that path production-grade and recommended the WebGL2 side copy it):_onContextLostnow cancels the default action so the browser can restore, and schedules the extension-driven restore on a fresh task using theWEBGL_lose_contextinstance cached in the constructor (pre-loss)._onContextRestoreddelegates to a new_reinitializeDeviceState()that:EXT_color_buffer_float(extension enablement does not survive a loss),appliedVersioncache),INVALID_OPERATIONs so the app's owngetError()starts clean.destroy()cancels any pending restore and a_destroyedguard stops a late timer callback.Pre-1.0 clean break — no shims.
Test evidence
test/rendering/browser/webgl2-context-restore.test.ts): drives a realWEBGL_lose_contextlose/restore cycle and asserts the scene renders identical pixels afterwards, including a two-cycle case and a texture created after the losses. Passes on Chromium and Firefox.test/perf/rendering/context-restore-webgl2.test.ts, 5 tests): pins the invalidation bookkeeping against the recording fake context —preventDefaulton loss, full cache eviction, fresh GL handle identity after restore for the sameTexture, draw-call resumption, and GPU-memory accounting release/re-accrual.Full verification run locally:
pnpm typecheck,eslint,prettier --check,pnpm docs:api:check, full WebGL2 Chromium browser suite (213 passed), Firefox context-restore,rendering-perfNode lane (66 passed), and the existing jsdomwebgl2-backendsuite (9 passed).Note: the retained instruction-set bundles (Slice 3) already hooked their own generation-bump into the restore path; that behavior is preserved and folded into the new teardown.
https://claude.ai/code/session_01RHB7cLgoonJHLQg9ScdXby