fix: cancel pane webgl refresh frame (#3397)

This commit is contained in:
Neil
2026-05-29 17:59:27 -07:00
committed by GitHub
parent 73463564be
commit 8397973e89
4 changed files with 103 additions and 2 deletions
@@ -23,7 +23,12 @@ import {
detachPaneFitResizeObserver
} from './pane-fit-resize-observer'
import { buildDefaultTerminalOptions } from './pane-terminal-options'
import { ENABLE_WEBGL_RENDERER, attachWebgl, disposeWebgl } from './pane-webgl-renderer'
import {
ENABLE_WEBGL_RENDERER,
attachWebgl,
cancelPendingWebglRefresh,
disposeWebgl
} from './pane-webgl-renderer'
import { shouldFocusTerminalFromPanePointerDown } from './pane-pointer-focus'
// ---------------------------------------------------------------------------
@@ -120,6 +125,7 @@ export function createPaneDOM(
fitAddon,
fitResizeObserver: null,
pendingInitialFitRafId: null,
pendingWebglRefreshRafId: null,
pendingObservedFitRafId: null,
searchAddon,
serializeAddon,
@@ -299,6 +305,7 @@ export function disposePane(
cancelAnimationFrame(pane.pendingInitialFitRafId)
pane.pendingInitialFitRafId = null
}
cancelPendingWebglRefresh(pane)
detachPaneFitResizeObserver(pane)
if (pane.compositionHandler) {
pane.terminal.element?.removeEventListener('compositionstart', pane.compositionHandler, true)
@@ -107,6 +107,8 @@ export type ManagedPaneInternal = {
fitResizeObserver: ResizeObserver | null
// Stored so disposePane() can cancel the first post-open fit if a pane closes before paint.
pendingInitialFitRafId?: number | null
// Stored so disposePane() can cancel the post-WebGL-teardown refresh frame.
pendingWebglRefreshRafId?: number | null
pendingObservedFitRafId: number | null
serializeAddon: SerializeAddon
unicode11Addon: Unicode11Addon
@@ -0,0 +1,83 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ManagedPaneInternal } from './pane-manager-types'
import { disposePane } from './pane-lifecycle'
import { disposeWebgl } from './pane-webgl-renderer'
function createPane(
overrides: Partial<Pick<ManagedPaneInternal, 'pendingWebglRefreshRafId' | 'webglAddon'>> = {}
): ManagedPaneInternal {
const leafId = '11111111-1111-4111-8111-111111111111' as never
return {
id: 1,
leafId,
stablePaneId: leafId,
terminal: {
element: null,
rows: 24,
refresh: vi.fn(),
dispose: vi.fn()
} as never,
container: {} as never,
xtermContainer: {} as never,
linkTooltip: {} as never,
terminalGpuAcceleration: 'off',
gpuRenderingEnabled: false,
webglAttachmentDeferred: false,
webglDisabledAfterContextLoss: false,
hasComplexScriptOutput: false,
fitAddon: {
fit: vi.fn(),
dispose: vi.fn()
} as never,
fitResizeObserver: null,
pendingInitialFitRafId: null,
pendingWebglRefreshRafId: null,
pendingObservedFitRafId: null,
searchAddon: { dispose: vi.fn() } as never,
serializeAddon: { dispose: vi.fn() } as never,
unicode11Addon: { dispose: vi.fn() } as never,
webLinksAddon: { dispose: vi.fn() } as never,
webglAddon: { dispose: vi.fn() } as never,
ligaturesAddon: null,
compositionHandler: null,
pendingSplitScrollState: null,
pendingSplitScrollBufferDisposable: null,
debugLabel: null,
...overrides
}
}
describe('pane WebGL refresh lifecycle', () => {
afterEach(() => {
vi.unstubAllGlobals()
})
it('tracks the deferred refresh frame after WebGL teardown', () => {
vi.stubGlobal(
'requestAnimationFrame',
vi.fn(() => 29)
)
const pane = createPane()
disposeWebgl(pane, { refreshDimensions: true })
expect(pane.webglAddon).toBeNull()
expect(pane.pendingWebglRefreshRafId).toBe(29)
})
it('cancels a pending WebGL refresh when the pane is disposed', () => {
const cancelAnimationFrame = vi.fn()
vi.stubGlobal('cancelAnimationFrame', cancelAnimationFrame)
const pane = createPane({
pendingWebglRefreshRafId: 31,
webglAddon: null
})
const panes = new Map([[pane.id, pane]])
disposePane(pane, panes)
expect(cancelAnimationFrame).toHaveBeenCalledWith(31)
expect(pane.pendingWebglRefreshRafId).toBeNull()
expect(panes.has(pane.id)).toBe(false)
})
})
@@ -43,10 +43,18 @@ function refreshTerminalAfterWebglAttach(pane: ManagedPaneInternal): void {
}
}
export function cancelPendingWebglRefresh(pane: ManagedPaneInternal): void {
if (pane.pendingWebglRefreshRafId != null) {
cancelAnimationFrame(pane.pendingWebglRefreshRafId)
pane.pendingWebglRefreshRafId = null
}
}
export function disposeWebgl(
pane: ManagedPaneInternal,
options?: { refreshDimensions?: boolean }
): void {
cancelPendingWebglRefresh(pane)
if (!pane.webglAddon) {
return
}
@@ -60,7 +68,8 @@ export function disposeWebgl(
// Why: VS Code refreshes terminal dimensions after WebGL teardown because
// DOM and WebGL renderer cell metrics differ. Without this, Linux DOM
// scrollbars can desync and trigger visible reflow jitter.
requestAnimationFrame(() => {
pane.pendingWebglRefreshRafId = requestAnimationFrame(() => {
pane.pendingWebglRefreshRafId = null
try {
pane.fitAddon.fit()
pane.terminal.refresh(0, pane.terminal.rows - 1)