Files
feedBack/tests/js/highway_3d_context_loss.test.js
T
b914612f9d fix(highway_3d): recover from WebGL context loss instead of crashing on alt-tab (#790)
Switching the active window / alt-tabbing away (most often on Windows) can
trigger a GPU context reset. The 3D highway's WebGL renderer had no
webglcontextlost handler, so a lost context was left to escalate into a
render-process crash -- matching the intermittent "randomly crashes when I
change windows" desktop reports.

The renderer now binds webglcontextlost/webglcontextrestored on its own WebGL
canvas (ren.domElement): the loss is preventDefault()'d so the browser keeps the
context restorable, draw() bails while the context is down so no GL work runs on
a dead context, and on restore the viewport is re-applied and rendering resumes
(Three re-uploads scene resources on the next frame). Listeners are removed in
teardown.

Root cause is a strong hypothesis -- the crash is intermittent and
unreproducible -- but the fix is low-risk and additive and closes a real gap:
there was no context-loss handling anywhere in the renderer.

plugins/highway_3d 3.31.2 -> 3.31.3. Tests:
tests/js/highway_3d_context_loss.test.js (source-contract, like the other
highway_* tests). The sibling keys_highway_3d / drum_highway_3d renderers share
the same gap -- follow-up in their repos.


Claude-Session: https://claude.ai/code/session_01UR2Cr7GEu3yMY7SrfxH6c1

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:20:32 +02:00

46 lines
2.4 KiB
JavaScript

// Contract test: 3D Highway WebGL context-loss recovery.
//
// Switching the active window / alt-tabbing (especially on Windows) can trigger
// a GPU context reset. Without a handler the lost WebGL context escalates into a
// render-process crash. The renderer owns its own WebGL canvas + heavy Three.js
// lifecycle (too much to construct in a vm sandbox), so — like the other
// highway_* source-contract tests here — this pins the wiring at the source
// level: the loss must be preventDefault()'d (so the browser restores it), draw
// must bail while lost, and the listeners must be torn down.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const src = fs.readFileSync(SCREEN_JS, 'utf8');
test('binds webglcontextlost + webglcontextrestored on the renderer canvas', () => {
assert.match(src, /ren\.domElement\.addEventListener\(\s*['"]webglcontextlost['"]/,
'must listen for webglcontextlost on ren.domElement (the WebGL canvas)');
assert.match(src, /ren\.domElement\.addEventListener\(\s*['"]webglcontextrestored['"]/,
'must listen for webglcontextrestored on ren.domElement');
});
test('the context-lost handler preventDefaults and pauses drawing', () => {
// Without preventDefault() the browser will not attempt to restore the
// context and the loss can escalate to a renderer crash.
const m = src.match(/_onCtxLost\s*=\s*\(e\)\s*=>\s*\{[\s\S]*?\};/);
assert.ok(m, '_onCtxLost handler must exist');
assert.match(m[0], /preventDefault\(\)/, 'context-lost handler must call preventDefault()');
assert.match(m[0], /_ctxLost\s*=\s*true/, 'context-lost handler must set _ctxLost = true');
});
test('draw() early-returns while the context is lost', () => {
assert.match(src, /draw\(bundle\)\s*\{[\s\S]*?if\s*\(_ctxLost\)\s*return;/,
'draw() must bail while _ctxLost is set so no GL work runs on a dead context');
});
test('teardown removes the context-loss listeners', () => {
assert.match(src, /removeEventListener\(\s*['"]webglcontextlost['"]/,
'teardown must remove the webglcontextlost listener');
assert.match(src, /removeEventListener\(\s*['"]webglcontextrestored['"]/,
'teardown must remove the webglcontextrestored listener');
});