From e53fd049ede1d9dcb43d06f00d8aff419d0f4df6 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 2 Jul 2026 04:23:07 -0400 Subject: [PATCH] Gate WebGL context retention to Windows Context re-creation is ~5ms on macOS/Linux GL, so retention buys no perceptible switch latency there while still costing hidden-pane GPU memory and a new lifecycle to reason about. Only Windows (ANGLE -> D3D11, 100-500ms per pane) retains; other platforms keep the long-proven dispose-on-hide behavior. --- .../pane-manager/pane-rendering-control.ts | 19 +++++-- .../pane-webgl-context-retention.test.ts | 52 ++++++++++++++++--- .../pane-webgl-context-retention.ts | 8 +++ 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts index 3747495d2c9..c79598ff0eb 100644 --- a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts +++ b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts @@ -7,7 +7,11 @@ import { markComplexScriptOutput, resetWebglTextureAtlas } from './pane-webgl-renderer' -import { retainSuspendedWebglPane, unretainWebglPane } from './pane-webgl-context-retention' +import { + retainSuspendedWebglPane, + shouldRetainSuspendedWebglContexts, + unretainWebglPane +} from './pane-webgl-context-retention' import { reattachWebglIfNeeded } from './pane-webgl-reattach' export function setPaneGpuRenderingState( @@ -44,12 +48,17 @@ export function markPaneComplexScriptOutput( } export function suspendPaneRendering(panes: Iterable): void { + const retainContexts = shouldRetainSuspendedWebglContexts() for (const pane of panes) { - // Why: deferred blocks NEW context creation while hidden; live contexts - // are retained (not disposed) so the return switch repaints instantly - // instead of paying ANGLE context re-creation. The LRU cap bounds how - // many hidden panes may keep one. + // Why: deferred blocks NEW context creation while hidden; on Windows, + // live contexts are retained (not disposed) so the return switch repaints + // instantly instead of paying ANGLE context re-creation. The LRU cap + // bounds how many hidden panes may keep one. pane.webglAttachmentDeferred = true + if (!retainContexts) { + disposeWebgl(pane) + continue + } for (const evicted of retainSuspendedWebglPane(pane)) { disposeWebgl(evicted) } diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.test.ts b/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.test.ts index a838d6de074..eb4c14bc6d5 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.test.ts @@ -66,16 +66,21 @@ function fireContextLoss(pane: ManagedPaneInternal): void { addon._onContextLoss.fire() } -describe('terminal WebGL context retention across hide/show', () => { +function stubCommonGlobals(userAgent: string): void { + vi.stubGlobal('navigator', { userAgent }) + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + callback(16) + return 1 + }) + vi.stubGlobal('cancelAnimationFrame', vi.fn()) +} + +describe('terminal WebGL context retention across hide/show (Windows)', () => { beforeEach(() => { resetTerminalWebglSuggestion() clearRetainedWebglPanes() vi.spyOn(console, 'warn').mockImplementation(() => {}) - vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { - callback(16) - return 1 - }) - vi.stubGlobal('cancelAnimationFrame', vi.fn()) + stubCommonGlobals('Mozilla/5.0 (Windows NT 10.0; Win64; x64)') }) afterEach(() => { @@ -193,3 +198,38 @@ describe('terminal WebGL context retention across hide/show', () => { expect(pane.webglAddon).not.toBeNull() }) }) + +describe('terminal WebGL context retention is Windows-only', () => { + beforeEach(() => { + resetTerminalWebglSuggestion() + clearRetainedWebglPanes() + vi.spyOn(console, 'warn').mockImplementation(() => {}) + stubCommonGlobals('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)') + }) + + afterEach(() => { + clearRetainedWebglPanes() + vi.unstubAllGlobals() + vi.restoreAllMocks() + }) + + it('keeps the dispose-on-hide behavior off Windows', () => { + const pane = createAttachedPane() + + suspendPaneRendering([pane]) + + expect(pane.webglAttachmentDeferred).toBe(true) + expect(pane.webglAddon).toBeNull() + expect(retainedWebglPaneCount()).toBe(0) + }) + + it('re-attaches on resume off Windows as before', () => { + const pane = createAttachedPane() + suspendPaneRendering([pane]) + + resumePaneRendering([pane]) + + expect(pane.webglAddon).not.toBeNull() + expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.ts b/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.ts index 742f65f1842..fe997b21745 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.ts @@ -1,3 +1,4 @@ +import { getRendererAppPlatform } from '@/lib/renderer-app-platform' import type { ManagedPaneInternal } from './pane-manager-types' // Why: workspace switches used to dispose every hidden pane's WebGL context @@ -7,6 +8,13 @@ import type { ManagedPaneInternal } from './pane-manager-types' // this LRU cap only bounds hidden-pane GPU memory. export const RETAINED_WEBGL_PANE_CAP = 16 +/** Windows-only: context re-creation is cheap on macOS/Linux GL, so hidden + * panes there keep the long-proven dispose-on-hide behavior instead of + * paying retention's GPU-memory cost for no perceptible switch latency win. */ +export function shouldRetainSuspendedWebglContexts(): boolean { + return getRendererAppPlatform() === 'win32' +} + // Insertion order doubles as LRU order: re-retaining deletes + re-adds. const retainedPanes = new Set()