From b1df5319d53b40a18cf655f4fef103b5f0528414 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 4 Mar 2026 13:17:38 +0100 Subject: [PATCH] fix: prevent page unresponsive crashes from WebGL GPU stalls during session switch Disable WebGL renderer during large buffer loads (>32KB) and fall back to canvas, which handles bulk ANSI writes without synchronous GPU ReadPixels calls. Re-enable WebGL after the buffer load completes so live terminal streaming still benefits from GPU acceleration. Also reduce chunk size from 128KB to 32KB and use chunked writes for cached buffer restores instead of synchronous terminal.write(). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/web/public/app.js | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 872d35f3..59b52f08 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -542,12 +542,14 @@ class CodemanApp { const container = document.getElementById('terminalContainer'); this.terminal.open(container); - // Activate WebGL renderer for up to 900% faster rendering (fallback to canvas on failure) + // Activate WebGL renderer for up to 900% faster rendering (fallback to canvas on failure). + // Store reference so we can disable during large buffer loads to prevent GPU stalls. + this._webglAddon = null; if (typeof WebglAddon !== 'undefined') { try { - const webglAddon = new WebglAddon.WebglAddon(); - webglAddon.onContextLoss(() => { webglAddon.dispose(); }); - this.terminal.loadAddon(webglAddon); + this._webglAddon = new WebglAddon.WebglAddon(); + this._webglAddon.onContextLoss(() => { this._webglAddon.dispose(); this._webglAddon = null; }); + this.terminal.loadAddon(this._webglAddon); } catch (_e) { /* WebGL2 unavailable — canvas renderer used */ } } @@ -1344,13 +1346,30 @@ class CodemanApp { // (from historical SSE data that was stored with markers) const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, ''); + // Disable WebGL during large buffer loads to prevent GPU stalls. + // The canvas renderer handles bulk writes without blocking the main thread. + // WebGL's synchronous ReadPixels calls cause "page unresponsive" on dense ANSI buffers. + const isLargeBuffer = cleanBuffer.length > chunkSize; + if (isLargeBuffer && this._webglAddon) { + try { this._webglAddon.dispose(); } catch (_e) { /* already disposed */ } + this._webglAddon = null; + } + const finish = () => { + // Re-enable WebGL after buffer load completes + if (isLargeBuffer && !this._webglAddon && typeof WebglAddon !== 'undefined') { + try { + this._webglAddon = new WebglAddon.WebglAddon(); + this._webglAddon.onContextLoss(() => { this._webglAddon.dispose(); this._webglAddon = null; }); + this.terminal.loadAddon(this._webglAddon); + } catch (_e) { /* WebGL re-init failed — stay on canvas */ } + } this._finishBufferLoad(); resolve(); }; - // For small buffers, write directly - if (cleanBuffer.length <= chunkSize) { + // For small buffers, write directly (WebGL stays active — small writes are fine) + if (!isLargeBuffer) { this.terminal.write(cleanBuffer); finish(); return; @@ -1368,9 +1387,8 @@ class CodemanApp { this.terminal.write(chunk); offset += chunkSize; - // Double-RAF: yields to both the browser's layout/paint AND the WebGL renderer's - // GPU flush. Single RAF can still block if ReadPixels is synchronous. - requestAnimationFrame(() => requestAnimationFrame(writeChunk)); + // Schedule next chunk on next frame + requestAnimationFrame(writeChunk); }; // Start writing