1. Symptom
Flight view went white or blank after using shipyard / lock preview / map / hangar. Console: “Too many active WebGL contexts” then THREE.WebGLRenderer: Context Lost on the primary canvas. HUD chips could still paint.
2. Player impact
The session looked like a crash of space itself: cockpit UI alive, stars gone. Reloading recovered; repeating shipyard Equip/tier actions reproduced it without a “gameplay” error.
3. Reproduction
Open and close 3D previews (shipyard inline hangar, ship viewer, galaxy map, lock hologram) several times, or refresh shipyard on each Equip. Watch the context-lost warning on the flight canvas. The pool module documents this sequence in its header comment.
4. System ownership
Primary world renderer: play/three/scene.js (webglcontextlost listener). Previews: shipyard.js, hud lock preview, galaxy-map-3d, hangar-ship, ui-ship-preview. Shared pool: webgl-preview-pool.js (PREVIEW_GL ids). gpu-warning.js surfaces GPU/renderer info.
5. Incorrect assumption
Calling renderer.dispose() returns the context to the budget before the next open. Creating a fresh WebGLRenderer per panel open is “clean architecture.” The main canvas is isolated from preview canvases.
6. Root cause
Context count is process-wide. dispose() is asynchronous from Chromium’s budget. Reopen cycles stacked live contexts until the oldest — often the flight renderer — was killed. Shipyard renderShipyard() historically tore down and rebuilt the inline hangar preview on Equip/tier/buy, which was a high-churn path.
7. Minimal fix
Acquire once per preview id, pause RAF on close, destroy only if the context is already lost. Do not innerHTML-replace a live hangar column. The pool’s acquirePreviewRenderer / pausePreviewRenderer / destroyPreviewRenderer is that contract.
8. Regression test
tests/webgl-preview-lifecycle.test.mjs covers reuse vs create-count and lost-context destroy. Earlier shipyard churn was also guarded so Equip does not open extra contexts.
9. Release validation
Development log 19 Aug 2026, v0.4.158 (preview pool). Earlier log entries record white-flash on dock (F opening a second station WebGL) and hangar teardown. Primary canvas still listens for webglcontextlost.
10. Generalized lesson
GPU contexts are a shared budget, not a private field on a component. Lifecycle is pause vs destroy. An AI that “cleans up” with dispose() on every React-like unmount can take down the game view. Count live renderers; test reopen loops, not a single open.
Simplified excerpt
Simplified / pseudocode for teaching. Not a dump of production files.
The pool rule, simplified from the module comment.
// Simplified teaching excerpt — not CONTRABAND production source.
// Chromium ~8–16 WebGL contexts per process.
function acquirePreviewRenderer(id) {
const existing = slots.get(id);
if (existing && !existing.contextLost) return existing; // reuse
return createOnce(id);
}
function onPreviewClose(id) {
pauseRaf(id); // do not dispose every close
// destroy only if getContext().isContextLost()
}Public evidence
- webgl-preview-pool.js header comment and acquire/pause/destroy API.
- CONTRABAND development log, 19 Aug 2026, v0.4.158; earlier white-canvas / shipyard churn notes.
- webgl-preview-lifecycle.test.mjs