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.
This commit is contained in:
Jinwoo Hong
2026-07-02 15:28:23 -04:00
parent 90e08b01ea
commit e53fd049ed
3 changed files with 68 additions and 11 deletions
@@ -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<ManagedPaneInternal>): 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)
}
@@ -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)
})
})
@@ -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<ManagedPaneInternal>()