1. Síntoma
La vista de vuelo se iba a blanco o vacío tras usar astillero / preview de lock / mapa / hangar. Consola: “Too many active WebGL contexts” y luego THREE.WebGLRenderer: Context Lost en el canvas principal. Los chips de HUD podían seguir pintando.
2. Impacto en el jugador
La sesión parecía un crash del espacio: UI de cabina viva, estrellas idas. Recargar recuperaba; repetir Equip/tier en el astillero lo reproducía sin un error de “gameplay”.
3. Reproducción
Abrir y cerrar previews 3D (hangar inline del astillero, visor de nave, mapa galáctico, holograma de lock) varias veces, o refrescar el astillero en cada Equip. Mirar el aviso de context-lost en el canvas de vuelo. El módulo pool documenta esta secuencia en su comentario de cabecera.
4. Ownership del sistema
Renderer principal del mundo: play/three/scene.js (listener webglcontextlost). Previews: shipyard.js, preview de lock del HUD, galaxy-map-3d, hangar-ship, ui-ship-preview. Pool compartido: webgl-preview-pool.js (ids PREVIEW_GL). gpu-warning.js muestra info de GPU/renderer.
5. Supuesto erróneo
Llamar renderer.dispose() devuelve el contexto al presupuesto antes de la siguiente apertura. Crear un WebGLRenderer fresco por cada open de panel es “arquitectura limpia”. El canvas principal está aislado de los canvas de preview.
6. Causa raíz
El recuento de contextos es de todo el proceso. dispose() es asíncrono respecto al presupuesto de Chromium. Los ciclos de reopen apilaban contextos vivos hasta que mataban el más antiguo — a menudo el renderer de vuelo. renderShipyard() históricamente destruía y reconstruía el preview inline en Equip/tier/compra, un camino de mucho churn.
7. Arreglo mínimo
Adquirir una vez por id de preview, pausar RAF al cerrar, destruir solo si el contexto ya se perdió. No reemplazar con innerHTML una columna de hangar viva. El contrato es acquirePreviewRenderer / pausePreviewRenderer / destroyPreviewRenderer del pool.
8. Test de regresión
tests/webgl-preview-lifecycle.test.mjs cubre reuso vs recuento de creates y destroy por contexto perdido. El churn anterior del astillero también quedó cubierto para que Equip no abra contextos extra.
9. Validación de release
Log de desarrollo 19 ago 2026, v0.4.158 (pool de previews). Entradas anteriores registran el flash blanco al atracar (F abría un segundo WebGL de estación) y el teardown del hangar. El canvas principal sigue escuchando webglcontextlost.
10. Lección generalizada
Los contextos GPU son un presupuesto compartido, no un campo privado de un componente. El lifecycle es pausar vs destruir. Una IA que “limpia” con dispose() en cada unmount estilo React puede tumbar la vista del juego. Cuenta renderers vivos; testa bucles de reopen, no un solo open.
Extracto simplificado
Simplificado / pseudocódigo para enseñar. No es un volcado de archivos de producción.
La regla del pool, simplificada del comentario del módulo.
// 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()
}Evidencia pública
- Comentario de cabecera de webgl-preview-pool.js y API acquire/pause/destroy.
- Log de desarrollo de CONTRABAND, 19 ago 2026, v0.4.158; notas anteriores de canvas blanco / churn de astillero.
- webgl-preview-lifecycle.test.mjs