diff --git a/src/main/index.ts b/src/main/index.ts index 94ce5b119..8d8c429f2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -73,7 +73,6 @@ import { notifyServeSupervisorReady } from './serve-update-handoff' import { - configureMainProcessWebglContextBudget, configureElectronNetworkCompatibility, configureDevUserDataPath, configureOrcaUserDataPathEnv, @@ -666,10 +665,7 @@ if (hasSingleInstanceLock) { configureElectronNetworkCompatibility() enableRendererHeapHeadroom() maybeApplyGpuFallbackForThisLaunch() - if (gpuFallbackActiveThisLaunch) { - // Software fallback still shares the renderer's bounded Windows retention policy. - configureMainProcessWebglContextBudget() - } else { + if (!gpuFallbackActiveThisLaunch) { enableMainProcessGpuFeatures() } // Why: headless serve's offscreen BrowserWindows need an X display (Xvfb) on Linux; the result gates whether the offscreen backend is installed. diff --git a/src/main/startup/configure-process.test.ts b/src/main/startup/configure-process.test.ts index 55f4d59b3..512467eb7 100644 --- a/src/main/startup/configure-process.test.ts +++ b/src/main/startup/configure-process.test.ts @@ -398,21 +398,6 @@ describe('enableMainProcessGpuFeatures', () => { expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('max-active-webgl-contexts', '128') }) - it('configures the shared WebGL budget independently for GPU fallback', async () => { - const { app } = await import('electron') - const { TERMINAL_WEBGL_MAX_ACTIVE_CONTEXTS } = - await import('../../shared/terminal-webgl-context-budget') - const { configureMainProcessWebglContextBudget } = await import('./configure-process') - - vi.mocked(app.commandLine.appendSwitch).mockClear() - configureMainProcessWebglContextBudget() - - expect(app.commandLine.appendSwitch).toHaveBeenCalledWith( - 'max-active-webgl-contexts', - String(TERMINAL_WEBGL_MAX_ACTIVE_CONTEXTS) - ) - }) - it('disables Skia Graphite only on macOS without disabling hardware acceleration', async () => { const { app } = await import('electron') const { enableMainProcessGpuFeatures } = await import('./configure-process') diff --git a/src/main/startup/configure-process.ts b/src/main/startup/configure-process.ts index 8d4b51b5b..292f61952 100644 --- a/src/main/startup/configure-process.ts +++ b/src/main/startup/configure-process.ts @@ -2,7 +2,6 @@ import { app } from 'electron' import { existsSync, mkdirSync, readFileSync } from 'node:fs' import { homedir } from 'node:os' import { join, resolve } from 'node:path' -import { TERMINAL_WEBGL_MAX_ACTIVE_CONTEXTS } from '../../shared/terminal-webgl-context-budget' import { getVersionManagerBinPaths } from '../codex-cli/command' import { getMainE2EConfig } from '../e2e-config' @@ -255,13 +254,6 @@ export function installDevParentSignalQuit(isDev: boolean): void { process.once('SIGTERM', onSignal) } -export function configureMainProcessWebglContextBudget(): void { - app.commandLine.appendSwitch( - 'max-active-webgl-contexts', - String(TERMINAL_WEBGL_MAX_ACTIVE_CONTEXTS) - ) -} - export function enableMainProcessGpuFeatures(): void { if (process.platform === 'linux' && getMainE2EConfig().userDataDir) { // Why: Ubuntu/Xvfb runners fail Electron startup with "GPU process isn't usable"; E2E needs no GPU, so use the software path. @@ -279,7 +271,7 @@ export function enableMainProcessGpuFeatures(): void { // Why: Blink evicts the oldest WebGL context past 16/renderer and each terminal pane holds one, silently downgrading panes to DOM. // 128 raises the ceiling for real layouts while staying bounded so context leaks still surface. - configureMainProcessWebglContextBudget() + app.commandLine.appendSwitch('max-active-webgl-contexts', '128') const ozonePlatform = (app.commandLine.getSwitchValue('ozone-platform') ?? '').toLowerCase() const ozonePlatformHint = (process.env.ELECTRON_OZONE_PLATFORM_HINT ?? '').toLowerCase() diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-effect-phase.test.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-effect-phase.test.ts new file mode 100644 index 000000000..3520975c7 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-effect-phase.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest' +import { getTerminalVisibilityEffectPhase } from './terminal-visibility-effect-phase' + +describe('terminal visibility effect phase', () => { + it('runs pre-paint only on macOS', () => { + expect(getTerminalVisibilityEffectPhase('darwin')).toBe('layout') + expect(getTerminalVisibilityEffectPhase('win32')).toBe('passive') + expect(getTerminalVisibilityEffectPhase('linux')).toBe('passive') + }) +}) diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-effect-phase.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-effect-phase.ts new file mode 100644 index 000000000..dcf959331 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-effect-phase.ts @@ -0,0 +1,7 @@ +export type TerminalVisibilityEffectPhase = 'layout' | 'passive' + +export function getTerminalVisibilityEffectPhase( + platform: NodeJS.Platform +): TerminalVisibilityEffectPhase { + return platform === 'darwin' ? 'layout' : 'passive' +} diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts index b98d6f934..28ed449ee 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts @@ -126,7 +126,7 @@ describe('resumeTerminalVisibility reveal repaint', () => { }) it('defers backlog and shared atlas recovery until after resume and fit', async () => { - // Why: resume before flush avoids DOM bold flash; fit before flush avoids + // On macOS resume before flush avoids DOM bold flash; fit before flush avoids // writing TUI backlog onto the transient DOM↔WebGL one-column-off grid. const order: string[] = [] const manager = createManager(order) diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts index beee0986a..8dcbb6e93 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts @@ -128,7 +128,6 @@ export function hideTerminalVisibility({ captureViewportPositions(false) } if (!isWorktreeActive && (wasVisible || surfaceBecameHidden)) { - // Windows defers new contexts but retains live ones; other clients dispose. manager.suspendRendering() return { hiddenReason: 'surface', renderingSuspended: true } } @@ -203,7 +202,6 @@ function requestLightTabBacklogRecovery(manager: PaneManager): void { } function resumeTerminalVisibilityBeforePaint(manager: PaneManager, isActive: boolean): void { - // Retained Windows surfaces reuse WebGL; disposed or pressure-lost surfaces reattach. manager.resumeRendering() manager.fitAllRevealedPanes() if (isActive) { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts index b61869116..4bf7faae2 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts @@ -30,7 +30,8 @@ const mocks = vi.hoisted(() => ({ const reactRefState = vi.hoisted(() => ({ slots: [] as { current: unknown }[], - index: 0 + index: 0, + effectPhase: null as 'layout' | 'passive' | null })) function beginHookRender(): void { @@ -48,12 +49,21 @@ vi.mock('react', async (importOriginal) => { ...actual, useCallback: unknown>(callback: T) => callback, useEffect: (effect: () => void | (() => void)) => { - effect() + reactRefState.effectPhase = 'passive' + try { + effect() + } finally { + reactRefState.effectPhase = null + } }, - // Why: visibility suspend/resume runs in useLayoutEffect so WebGL is live - // before the first paint of a revealed worktree (avoids DOM bold flash). + // macOS visibility suspend/resume runs here before reveal paint. useLayoutEffect: (effect: () => void | (() => void)) => { - effect() + reactRefState.effectPhase = 'layout' + try { + effect() + } finally { + reactRefState.effectPhase = null + } }, useRef: (value: T) => { const index = reactRefState.index @@ -146,7 +156,8 @@ function useMountForFileDrop( isWorktreeActive?: boolean isSyncFitEnabled?: boolean paneCount?: number - } = {} + } = {}, + useGlobalEffects: typeof useTerminalPaneGlobalEffects = useTerminalPaneGlobalEffects ): { onFileDrop: DropCallback manager: { @@ -160,6 +171,7 @@ function useMountForFileDrop( fitAllRevealedPanes: ReturnType } paneTransports: Map + renderingEffectPhases: ('layout' | 'passive' | null)[] } { let onFileDrop: DropCallback = () => { throw new Error('onFileDrop callback was not registered') @@ -168,9 +180,10 @@ function useMountForFileDrop( onFileDrop = callback return vi.fn() }) + const renderingEffectPhases: ('layout' | 'passive' | null)[] = [] const manager = { getPanes: vi.fn(() => []), - resumeRendering: vi.fn(), + resumeRendering: vi.fn(() => renderingEffectPhases.push(reactRefState.effectPhase)), resetWebglTextureAtlases: vi.fn(), scheduleRevealRepaint: vi.fn(), scheduleRevealPresent: vi.fn(), @@ -181,7 +194,7 @@ function useMountForFileDrop( const paneTransports = new Map() beginHookRender() - useTerminalPaneGlobalEffects({ + useGlobalEffects({ tabId: options.tabId ?? 'tab-1', worktreeId: options.worktreeId ?? 'wt-1', cwd: options.cwd, @@ -198,7 +211,7 @@ function useMountForFileDrop( toggleExpandPane: vi.fn() }) - return { onFileDrop, manager, paneTransports } + return { onFileDrop, manager, paneTransports, renderingEffectPhases } } describe('useTerminalPaneGlobalEffects', () => { @@ -231,6 +244,26 @@ describe('useTerminalPaneGlobalEffects', () => { ;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = MockResizeObserver }) + it.each([ + ['darwin', 'layout'], + ['win32', 'passive'], + ['linux', 'passive'] + ] as const)('runs visibility transitions in the %s effect phase', async (platform, phase) => { + window.api.platform = { + get: () => ({ platform, osRelease: 'test', displayServer: null }) + } + vi.resetModules() + const { useTerminalPaneGlobalEffects: usePlatformTerminalPaneGlobalEffects } = + await import('./use-terminal-pane-global-effects') + + const { renderingEffectPhases } = useMountForFileDrop( + { isActive: true, isVisible: true }, + usePlatformTerminalPaneGlobalEffects + ) + + expect(renderingEffectPhases).toEqual([phase]) + }) + afterEach(() => { for (const manager of registeredManagers.splice(0)) { unregisterLivePaneManager(manager) @@ -242,7 +275,7 @@ describe('useTerminalPaneGlobalEffects', () => { }) it('resumes WebGL and fits before flushing backlog so paint is GPU and grid is stable', () => { - // Why: resume before flush avoids DOM bold flash; fit before flush avoids + // On macOS resume before flush avoids DOM bold flash; fit before flush avoids // writing backlog onto the transient DOM↔WebGL one-column-off grid. const order: string[] = [] const terminalA = { name: 'terminal-a' } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts index 4d0d4e3c0..4e81b820c 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts @@ -26,6 +26,13 @@ import { releaseRendererPtyVisibilityClaim, setRendererPtyVisibilityClaim } from './pty-renderer-delivery-claims' +import { getRendererAppPlatform } from '@/lib/renderer-app-platform' +import { getTerminalVisibilityEffectPhase } from './terminal-visibility-effect-phase' + +const useTerminalVisibilityEffect = + getTerminalVisibilityEffectPhase(getRendererAppPlatform()) === 'layout' + ? useLayoutEffect + : useEffect type UseTerminalPaneGlobalEffectsArgs = { tabId: string @@ -130,8 +137,8 @@ export function useTerminalPaneGlobalEffects({ } }, [rendererVisible, paneTransportsRef]) - // Why layout: pre-paint WebGL resume avoids one-frame DOM bold flash on reveal. - useLayoutEffect(() => { + // macOS can rebuild WebGL pre-paint without blocking reveal on slow ANGLE paths. + useTerminalVisibilityEffect(() => { isActiveRef.current = isActive isVisibleRef.current = rendererVisible postPaintVisibilityRecoveryRef.current = applyTerminalVisibilityTransition({ diff --git a/src/renderer/src/lib/pane-manager/pane-dom-creation.ts b/src/renderer/src/lib/pane-manager/pane-dom-creation.ts index 127b2a7ea..3b1498b0a 100644 --- a/src/renderer/src/lib/pane-manager/pane-dom-creation.ts +++ b/src/renderer/src/lib/pane-manager/pane-dom-creation.ts @@ -127,7 +127,6 @@ export function createPaneDOM( gpuRenderingEnabled: ENABLE_WEBGL_RENDERER, webglAttachmentDeferred: false, webglDisabledAfterContextLoss: false, - webglRebuildDeferred: false, hasComplexScriptOutput: false, fitAddon, fitResizeObserver: null, diff --git a/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts b/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts index add24eb31..607ad1cdb 100644 --- a/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts @@ -6,7 +6,7 @@ import { markComplexScriptOutput, resetTerminalWebglSuggestion } from './pane-webgl-renderer' -import { attachLigatures, disposePane, openTerminal, setLigaturesEnabled } from './pane-lifecycle' +import { attachLigatures, disposePane, openTerminal } from './pane-lifecycle' import { ensureArabicShapingJoinerForText } from './terminal-arabic-shaping-joiner' import { buildDefaultTerminalOptions, @@ -430,32 +430,6 @@ describe('attachLigatures', () => { expect(pane.terminal.refresh).toHaveBeenCalledWith(0, 23) expect(pane.ligaturesAddon).not.toBeNull() }) - - it('defers a retained WebGL rebuild while enabling ligatures offscreen', () => { - const pane = createPane() - const retainedAddon = { dispose: vi.fn() } as never - pane.webglAddon = retainedAddon - pane.webglAttachmentDeferred = true - - attachLigatures(pane) - - expect(pane.webglAddon).toBe(retainedAddon) - expect(pane.webglRebuildDeferred).toBe(true) - expect(pane.terminal.refresh).not.toHaveBeenCalled() - }) - - it('defers a retained WebGL rebuild while disabling ligatures offscreen', () => { - const pane = createPane() - const retainedAddon = { dispose: vi.fn() } as never - pane.webglAddon = retainedAddon - pane.ligaturesAddon = { dispose: vi.fn() } as never - pane.webglAttachmentDeferred = true - - setLigaturesEnabled(pane, false) - - expect(pane.webglAddon).toBe(retainedAddon) - expect(pane.webglRebuildDeferred).toBe(true) - }) }) describe('openTerminal — addon and provider wiring', () => { diff --git a/src/renderer/src/lib/pane-manager/pane-lifecycle.ts b/src/renderer/src/lib/pane-manager/pane-lifecycle.ts index db7331335..59e244710 100644 --- a/src/renderer/src/lib/pane-manager/pane-lifecycle.ts +++ b/src/renderer/src/lib/pane-manager/pane-lifecycle.ts @@ -167,18 +167,6 @@ export function disposeLigatures(pane: ManagedPaneInternal): void { } } -function rebuildWebglAfterLigatureChange(pane: ManagedPaneInternal): void { - if (!pane.webglAddon) { - return - } - if (pane.webglAttachmentDeferred) { - pane.webglRebuildDeferred = true - return - } - disposeWebgl(pane) - attachWebgl(pane) -} - export function attachLigatures(pane: ManagedPaneInternal): void { if (pane.ligaturesAddon) { return @@ -189,15 +177,16 @@ export function attachLigatures(pane: ManagedPaneInternal): void { pane.ligaturesAddon = ligaturesAddon // Why: ligatures can be enabled after rows already rendered, especially // from Settings. Force existing glyph runs to be recomputed immediately. - if (!pane.webglAttachmentDeferred) { - pane.terminal.refresh(0, pane.terminal.rows - 1) - } + pane.terminal.refresh(0, pane.terminal.rows - 1) // Why: the WebGL renderer builds its glyph texture atlas at activation // time, so `font-feature-settings` applied after WebGL loaded won't // reach the GPU-rendered cells until the atlas is rebuilt. The upstream // docs call this out explicitly — reactivating WebGL after ligatures // forces a fresh atlas that includes the ligated glyphs. - rebuildWebglAfterLigatureChange(pane) + if (pane.webglAddon) { + disposeWebgl(pane) + attachWebgl(pane) + } } catch (err) { console.warn('[terminal] ligatures addon failed to attach for pane', pane.id, err) pane.ligaturesAddon = null @@ -214,7 +203,10 @@ export function setLigaturesEnabled(pane: ManagedPaneInternal, enabled: boolean) // Why: ligatures lived inside the WebGL atlas, so after disposing the // addon the atlas still holds the ligated glyphs. Rebuild it so text // renders as the non-ligated fallback immediately. - rebuildWebglAfterLigatureChange(pane) + if (pane.webglAddon) { + disposeWebgl(pane) + attachWebgl(pane) + } } } diff --git a/src/renderer/src/lib/pane-manager/pane-manager-types.ts b/src/renderer/src/lib/pane-manager/pane-manager-types.ts index d2de1093c..ffe314f63 100644 --- a/src/renderer/src/lib/pane-manager/pane-manager-types.ts +++ b/src/renderer/src/lib/pane-manager/pane-manager-types.ts @@ -136,7 +136,6 @@ export type ManagedPaneInternal = { gpuRenderingEnabled: boolean webglAttachmentDeferred: boolean webglDisabledAfterContextLoss: boolean - webglRebuildDeferred?: boolean // Why: expose complex-output diagnostics without changing renderer choice; // auto renderer fallback is reserved for platform or WebGL failures. hasComplexScriptOutput: boolean diff --git a/src/renderer/src/lib/pane-manager/pane-manager.ts b/src/renderer/src/lib/pane-manager/pane-manager.ts index 1a61a45cd..ff5676433 100644 --- a/src/renderer/src/lib/pane-manager/pane-manager.ts +++ b/src/renderer/src/lib/pane-manager/pane-manager.ts @@ -214,10 +214,6 @@ export class PaneManager { refreshAllPanes(): void { for (const pane of this.panes.values()) { - // Retained contexts repaint on resume without scaling recovery across hidden workspaces. - if (pane.webglAttachmentDeferred && pane.webglAddon) { - continue - } try { if (pane.terminal.rows > 0) { pane.terminal.refresh(0, pane.terminal.rows - 1) 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 0819aa21f..8ad22f07e 100644 --- a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts +++ b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts @@ -4,16 +4,10 @@ import { attachWebgl, clearTerminalWebglAttachBackoff, disposeWebgl, - isPaneWebglContextLost, markComplexScriptOutput, resetWebglTextureAtlas } from './pane-webgl-renderer' -import { - releaseRetainedWebglPane, - retainSuspendedWebglPane, - shouldRetainSuspendedWebglContexts -} from './pane-webgl-context-retention' -import { rebuildAttachedWebgl, reattachWebglIfNeeded } from './pane-webgl-reattach' +import { reattachWebglIfNeeded } from './pane-webgl-reattach' export function setPaneGpuRenderingState( panes: Map, @@ -49,17 +43,9 @@ export function markPaneComplexScriptOutput( } export function suspendPaneRendering(panes: Iterable): void { - const retainLiveContexts = shouldRetainSuspendedWebglContexts() for (const pane of panes) { pane.webglAttachmentDeferred = true - if (!retainLiveContexts) { - disposeWebgl(pane) - continue - } - const evicted = retainSuspendedWebglPane(pane) - if (evicted) { - disposeWebgl(evicted) - } + disposeWebgl(pane) } } @@ -69,31 +55,8 @@ export function resumePaneRendering(panes: Iterable): void // loss, and bounding retries to resume events cannot loop on live loss. clearTerminalWebglAttachBackoff() for (const pane of panes) { - releaseRetainedWebglPane(pane) - const wasDeferred = pane.webglAttachmentDeferred - const rebuildDeferred = pane.webglRebuildDeferred === true pane.webglAttachmentDeferred = false pane.webglDisabledAfterContextLoss = false - pane.webglRebuildDeferred = false - const contextLost = Boolean(pane.webglAddon && isPaneWebglContextLost(pane)) - if (wasDeferred && pane.webglAddon && !contextLost) { - if (rebuildDeferred) { - rebuildAttachedWebgl(pane) - continue - } - // Shared-atlas recovery skips deferred panes, so repaint the retained model now. - try { - if (pane.terminal.rows > 0) { - pane.terminal.refresh(0, pane.terminal.rows - 1) - } - } catch { - /* ignore — pane may be tearing down during resume */ - } - continue - } - if (contextLost) { - disposeWebgl(pane) - } reattachWebglIfNeeded(pane) } } diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-context-inspection.test.ts b/src/renderer/src/lib/pane-manager/pane-webgl-context-inspection.test.ts deleted file mode 100644 index 0e0d7ecbb..000000000 --- a/src/renderer/src/lib/pane-manager/pane-webgl-context-inspection.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { ManagedPaneInternal } from './pane-manager-types' - -describe('terminal WebGL context inspection', () => { - afterEach(async () => { - const { setTerminalWebglDiagnosticRecorder } = - await import('../../../../shared/terminal-webgl-diagnostics') - setTerminalWebglDiagnosticRecorder(null) - vi.resetModules() - }) - - it('records missing xterm internals once without treating the context as lost', async () => { - vi.resetModules() - const diagnostics = vi.fn() - const { setTerminalWebglDiagnosticRecorder } = - await import('../../../../shared/terminal-webgl-diagnostics') - setTerminalWebglDiagnosticRecorder(diagnostics) - const { isPaneWebglContextLost } = await import('./pane-webgl-renderer') - const pane = { id: 7, webglAddon: {} } as ManagedPaneInternal - - expect(isPaneWebglContextLost(pane)).toBe(false) - expect(isPaneWebglContextLost(pane)).toBe(false) - expect(diagnostics).toHaveBeenCalledTimes(1) - expect(diagnostics).toHaveBeenCalledWith('webgl-context-inspection-unavailable', { paneId: 7 }) - }) -}) diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts b/src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts index 520f0c6d2..785009ac6 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts @@ -1,19 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { setTerminalWebglDiagnosticRecorder } from '../../../../shared/terminal-webgl-diagnostics' import type { ManagedPaneInternal } from './pane-manager-types' -import { resumePaneRendering, suspendPaneRendering } from './pane-rendering-control' -import { - clearRetainedWebglPanesForTests, - RETAINED_WEBGL_PANE_LIMIT, - retainedWebglPaneCount -} from './pane-webgl-context-retention' -import { rebuildAttachedWebgl } from './pane-webgl-reattach' +import { resumePaneRendering } from './pane-rendering-control' import { attachWebgl, resetTerminalWebglSuggestion } from './pane-webgl-renderer' -function createPane(options: { id?: number; loadAddon?: () => void } = {}): ManagedPaneInternal { +function createPane(options: { loadAddon?: () => void } = {}): ManagedPaneInternal { const leafId = '11111111-1111-4111-8111-111111111111' as never return { - id: options.id ?? 1, + id: 1, leafId, stablePaneId: leafId, terminal: { @@ -61,13 +55,6 @@ function fireContextLoss(pane: ManagedPaneInternal): void { addon._onContextLoss.fire() } -function stubWindowsDesktop(): void { - vi.stubGlobal('window', { - api: { platform: { get: () => ({ platform: 'win32' }) } }, - location: { pathname: '/index.html' } - }) -} - describe('terminal WebGL context recovery', () => { beforeEach(() => { resetTerminalWebglSuggestion() @@ -80,7 +67,6 @@ describe('terminal WebGL context recovery', () => { }) afterEach(() => { - clearRetainedWebglPanesForTests() vi.unstubAllGlobals() vi.restoreAllMocks() }) @@ -126,160 +112,6 @@ describe('terminal WebGL context recovery', () => { expect(pane.webglAddon).not.toBeNull() }) - it('reuses a retained Windows context without loading another addon', () => { - stubWindowsDesktop() - const pane = createPane() - attachWebgl(pane) - const addon = pane.webglAddon - vi.mocked(pane.terminal.refresh).mockClear() - - suspendPaneRendering([pane]) - resumePaneRendering([pane]) - - expect(pane.webglAddon).toBe(addon) - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1) - expect(pane.terminal.refresh).toHaveBeenCalledWith(0, 23) - }) - - it('keeps a healthy visible context on window wake', () => { - const pane = createPane() - attachWebgl(pane) - const addon = pane.webglAddon - - resumePaneRendering([pane]) - - expect(pane.webglAddon).toBe(addon) - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1) - }) - - it('replaces a synchronously lost visible context on window wake', () => { - const pane = createPane() - const dispose = vi.fn() - pane.webglAddon = { - dispose, - _renderer: { - _gl: { - getExtension: vi.fn(() => null), - isContextLost: vi.fn(() => true) - } - } - } as never - - resumePaneRendering([pane]) - - expect(dispose).toHaveBeenCalledTimes(1) - expect(pane.webglAddon).not.toBeNull() - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1) - }) - - it('replaces a retained context lost before xterm fires its delayed event', () => { - stubWindowsDesktop() - const pane = createPane() - const dispose = vi.fn() - pane.webglAddon = { - dispose, - _renderer: { - _gl: { - getExtension: vi.fn(() => null), - isContextLost: vi.fn(() => true) - } - } - } as never - - suspendPaneRendering([pane]) - resumePaneRendering([pane]) - - expect(dispose).toHaveBeenCalledTimes(1) - expect(pane.webglAddon).not.toBeNull() - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1) - }) - - it('defers requested WebGL rebuilds until a hidden pane resumes', () => { - stubWindowsDesktop() - const pane = createPane() - attachWebgl(pane) - const retainedAddon = pane.webglAddon - suspendPaneRendering([pane]) - - rebuildAttachedWebgl(pane) - - expect(pane.webglAddon).toBe(retainedAddon) - expect(pane.webglRebuildDeferred).toBe(true) - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1) - - resumePaneRendering([pane]) - - expect(pane.webglAddon).not.toBe(retainedAddon) - expect(pane.webglRebuildDeferred).toBe(false) - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(2) - }) - - it('reattaches an LRU-evicted context when its pane resumes', () => { - stubWindowsDesktop() - const panes = Array.from({ length: RETAINED_WEBGL_PANE_LIMIT + 1 }, (_, id) => - createPane({ id }) - ) - for (const pane of panes) { - attachWebgl(pane) - } - - suspendPaneRendering(panes) - expect(panes[0].webglAddon).toBeNull() - expect(panes[0].terminal.loadAddon).toHaveBeenCalledTimes(1) - - resumePaneRendering(panes) - - expect(retainedWebglPaneCount()).toBe(0) - expect(panes[0].webglAddon).not.toBeNull() - expect(panes[0].terminal.loadAddon).toHaveBeenCalledTimes(2) - expect(panes[1].terminal.loadAddon).toHaveBeenCalledTimes(1) - }) - - it('blocks new WebGL contexts while a Windows pane is hidden', () => { - stubWindowsDesktop() - const pane = createPane() - - suspendPaneRendering([pane]) - attachWebgl(pane) - - expect(pane.webglAddon).toBeNull() - expect(pane.terminal.loadAddon).not.toHaveBeenCalled() - }) - - it('recovers a retained Windows context lost while hidden', () => { - stubWindowsDesktop() - const pane = createPane() - attachWebgl(pane) - suspendPaneRendering([pane]) - expect(retainedWebglPaneCount()).toBe(1) - - fireContextLoss(pane) - - expect(pane.webglAddon).toBeNull() - expect(pane.webglDisabledAfterContextLoss).toBe(true) - expect(retainedWebglPaneCount()).toBe(0) - - resumePaneRendering([pane]) - - expect(pane.webglDisabledAfterContextLoss).toBe(false) - expect(pane.webglAddon).not.toBeNull() - expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(2) - }) - - it('does not schedule a DOM refit when a hidden retained context is lost', () => { - stubWindowsDesktop() - const requestAnimationFrame = vi.fn(() => 1) - vi.stubGlobal('requestAnimationFrame', requestAnimationFrame) - const pane = createPane() - attachWebgl(pane) - suspendPaneRendering([pane]) - - fireContextLoss(pane) - - expect(requestAnimationFrame).not.toHaveBeenCalled() - expect(pane.pendingWebglRefreshRafId).toBeNull() - }) - it('re-latches when the retried context is lost again', () => { const pane = createPane() 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 deleted file mode 100644 index cb51c379d..000000000 --- a/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { ManagedPaneInternal } from './pane-manager-types' -import { - clearRetainedWebglPanesForTests, - releaseRetainedWebglPane, - RETAINED_WEBGL_PANE_LIMIT, - retainedWebglPaneCount, - retainSuspendedWebglPane, - shouldRetainSuspendedWebglContexts -} from './pane-webgl-context-retention' -import { resumePaneRendering, suspendPaneRendering } from './pane-rendering-control' -import { disposeWebgl } from './pane-webgl-renderer' - -function createAttachedPane(id: number): ManagedPaneInternal { - return { - id, - webglAddon: { dispose: vi.fn() }, - webglAttachmentDeferred: false, - pendingWebglRefreshRafId: null - } as never -} - -function stubRendererWindow(platform: NodeJS.Platform, webClient = false): void { - vi.stubGlobal('window', { - __ORCA_WEB_CLIENT__: webClient, - api: { platform: { get: () => ({ platform }) } }, - location: { pathname: webClient ? '/web-index.html' : '/index.html' } - }) -} - -describe('pane WebGL context retention', () => { - afterEach(() => { - clearRetainedWebglPanesForTests() - vi.unstubAllGlobals() - }) - - it('retains contexts only on Windows desktop', () => { - stubRendererWindow('win32') - expect(shouldRetainSuspendedWebglContexts()).toBe(true) - - stubRendererWindow('darwin') - expect(shouldRetainSuspendedWebglContexts()).toBe(false) - - stubRendererWindow('linux') - expect(shouldRetainSuspendedWebglContexts()).toBe(false) - - stubRendererWindow('win32', true) - expect(shouldRetainSuspendedWebglContexts()).toBe(false) - }) - - it('recognizes a web client from its entrypoint', () => { - vi.stubGlobal('window', { - api: { platform: { get: () => ({ platform: 'win32' }) } }, - location: { pathname: '/web-index.html' } - }) - - expect(shouldRetainSuspendedWebglContexts()).toBe(false) - }) - - it('evicts and disposes the oldest hidden contexts past the limit', () => { - stubRendererWindow('win32') - const panes = Array.from({ length: RETAINED_WEBGL_PANE_LIMIT + 2 }, (_, index) => - createAttachedPane(index) - ) - const oldestAddon = panes[0].webglAddon - - suspendPaneRendering(panes) - - expect(retainedWebglPaneCount()).toBe(RETAINED_WEBGL_PANE_LIMIT) - expect(oldestAddon?.dispose).toHaveBeenCalledTimes(1) - expect(panes[0].webglAddon).toBeNull() - expect(panes[1].webglAddon).toBeNull() - expect(panes[2].webglAddon).not.toBeNull() - expect(panes.at(-1)?.webglAddon).not.toBeNull() - - disposeWebgl(panes[0]) - expect(oldestAddon?.dispose).toHaveBeenCalledTimes(1) - }) - - it('moves a re-retained pane to the newest position', () => { - const panes = Array.from({ length: RETAINED_WEBGL_PANE_LIMIT + 1 }, (_, index) => - createAttachedPane(index) - ) - for (const pane of panes.slice(0, RETAINED_WEBGL_PANE_LIMIT)) { - retainSuspendedWebglPane(pane) - } - - retainSuspendedWebglPane(panes[0]) - const evicted = retainSuspendedWebglPane(panes.at(-1)!) - - expect(evicted).toBe(panes[1]) - expect(retainedWebglPaneCount()).toBe(RETAINED_WEBGL_PANE_LIMIT) - }) - - it('evicts across pane managers using global worktree hide recency', () => { - stubRendererWindow('win32') - const firstManagerPanes = Array.from({ length: RETAINED_WEBGL_PANE_LIMIT }, (_, index) => - createAttachedPane(index) - ) - const secondManagerPane = createAttachedPane(RETAINED_WEBGL_PANE_LIMIT) - - suspendPaneRendering(firstManagerPanes) - suspendPaneRendering([secondManagerPane]) - - expect(firstManagerPanes[0].webglAddon).toBeNull() - expect(secondManagerPane.webglAddon).not.toBeNull() - expect(retainedWebglPaneCount()).toBe(RETAINED_WEBGL_PANE_LIMIT) - }) - - it('releases panes from the retained set', () => { - const pane = createAttachedPane(1) - retainSuspendedWebglPane(pane) - - releaseRetainedWebglPane(pane) - - expect(retainedWebglPaneCount()).toBe(0) - }) - - it('never leaves resumed panes eligible for eviction', () => { - stubRendererWindow('win32') - const pane = createAttachedPane(1) - pane.webglAttachmentDeferred = true - retainSuspendedWebglPane(pane) - - resumePaneRendering([pane]) - - expect(retainedWebglPaneCount()).toBe(0) - expect(pane.webglAddon).not.toBeNull() - }) -}) 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 deleted file mode 100644 index 295e1ba4e..000000000 --- a/src/renderer/src/lib/pane-manager/pane-webgl-context-retention.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { getRendererAppPlatform } from '@/lib/renderer-app-platform' -import { isWebClientLocation } from '@/lib/web-client-location' -import { TERMINAL_WEBGL_RETAINED_WORKTREE_CONTEXTS } from '../../../../shared/terminal-webgl-context-budget' -import type { ManagedPaneInternal } from './pane-manager-types' - -export const RETAINED_WEBGL_PANE_LIMIT = TERMINAL_WEBGL_RETAINED_WORKTREE_CONTEXTS - -export function shouldRetainSuspendedWebglContexts(): boolean { - return ( - typeof window !== 'undefined' && getRendererAppPlatform() === 'win32' && !isWebClientLocation() - ) -} - -// Set insertion order tracks worktree-surface hide recency. -const retainedPanes = new Set() - -export function retainSuspendedWebglPane(pane: ManagedPaneInternal): ManagedPaneInternal | null { - if (!pane.webglAddon) { - retainedPanes.delete(pane) - return null - } - retainedPanes.delete(pane) - retainedPanes.add(pane) - if (retainedPanes.size <= RETAINED_WEBGL_PANE_LIMIT) { - return null - } - const oldest = retainedPanes.values().next().value - if (!oldest) { - return null - } - retainedPanes.delete(oldest) - return oldest -} - -export function releaseRetainedWebglPane(pane: ManagedPaneInternal): void { - retainedPanes.delete(pane) -} - -export function retainedWebglPaneCount(): number { - return retainedPanes.size -} - -export function clearRetainedWebglPanesForTests(): void { - retainedPanes.clear() -} diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-reattach.ts b/src/renderer/src/lib/pane-manager/pane-webgl-reattach.ts index b1bde4b7d..82f8c5d22 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-reattach.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-reattach.ts @@ -11,11 +11,6 @@ export function rebuildAttachedWebgl(pane: ManagedPaneInternal): void { if (!pane.webglAddon || pane.webglDisabledAfterContextLoss) { return } - if (pane.webglAttachmentDeferred) { - pane.webglRebuildDeferred = true - return - } - pane.webglRebuildDeferred = false disposeWebgl(pane) // Why: the live addon just proved context creation works, so a stale attach // backoff from an earlier failure must not downgrade this pane to DOM. diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts b/src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts index b5a2618ed..fe661327c 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts @@ -1,12 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { PaneManager } from './pane-manager' import type { ManagedPaneInternal } from './pane-manager-types' import { disposePane } from './pane-lifecycle' -import { resumePaneRendering, suspendPaneRendering } from './pane-rendering-control' -import { - clearRetainedWebglPanesForTests, - retainedWebglPaneCount -} from './pane-webgl-context-retention' +import { suspendPaneRendering } from './pane-rendering-control' import { disposeWebgl } from './pane-webgl-renderer' import { beginTerminalScrollIntentBufferRebuild, @@ -14,9 +9,7 @@ import { } from './terminal-scroll-intent-rebuild' function createPane( - overrides: Partial< - Pick - > = {} + overrides: Partial> = {} ): ManagedPaneInternal { const leafId = '11111111-1111-4111-8111-111111111111' as never return { @@ -66,17 +59,8 @@ function createPane( } } -function stubRendererWindow(platform: NodeJS.Platform, webClient = false): void { - vi.stubGlobal('window', { - __ORCA_WEB_CLIENT__: webClient, - api: { platform: { get: () => ({ platform }) } }, - location: { pathname: webClient ? '/web-index.html' : '/index.html' } - }) -} - describe('pane WebGL refresh lifecycle', () => { afterEach(() => { - clearRetainedWebglPanesForTests() vi.unstubAllGlobals() }) @@ -137,8 +121,7 @@ describe('pane WebGL refresh lifecycle', () => { expect(pane.webglAddon).toBeNull() }) - it('disposes WebGL when rendering is suspended', () => { - stubRendererWindow('darwin') + it('disposes WebGL whenever rendering is suspended', () => { const dispose = vi.fn() const pane = createPane({ webglAddon: { dispose } as never }) @@ -149,112 +132,6 @@ describe('pane WebGL refresh lifecycle', () => { expect(pane.webglAddon).toBeNull() }) - it('retains and reuses a live WebGL addon on Windows desktop', () => { - stubRendererWindow('win32') - const dispose = vi.fn() - const addon = { dispose } as never - const pane = createPane({ webglAddon: addon }) - vi.mocked(pane.terminal.refresh).mockClear() - - suspendPaneRendering([pane]) - - expect(pane.webglAttachmentDeferred).toBe(true) - expect(pane.webglAddon).toBe(addon) - expect(dispose).not.toHaveBeenCalled() - - resumePaneRendering([pane]) - - expect(pane.webglAttachmentDeferred).toBe(false) - expect(pane.webglAddon).toBe(addon) - expect(pane.terminal.refresh).toHaveBeenCalledWith(0, 23) - }) - - it('disposes suspended WebGL in a Windows web client', () => { - stubRendererWindow('win32', true) - const dispose = vi.fn() - const pane = createPane({ webglAddon: { dispose } as never }) - - suspendPaneRendering([pane]) - - expect(pane.webglAttachmentDeferred).toBe(true) - expect(dispose).toHaveBeenCalledTimes(1) - expect(pane.webglAddon).toBeNull() - }) - - it('skips retained panes but refreshes hidden DOM panes during global recovery', () => { - const retained = createPane({ id: 1 }) - const hiddenDom = createPane({ id: 2, webglAddon: null }) - const visible = createPane() - retained.webglAttachmentDeferred = true - hiddenDom.webglAttachmentDeferred = true - vi.mocked(retained.terminal.refresh).mockClear() - vi.mocked(hiddenDom.terminal.refresh).mockClear() - vi.mocked(visible.terminal.refresh).mockClear() - - PaneManager.prototype.refreshAllPanes.call({ - panes: new Map([ - [1, retained], - [2, hiddenDom], - [3, visible] - ]) - } as never) - - expect(retained.terminal.refresh).not.toHaveBeenCalled() - expect(hiddenDom.terminal.refresh).toHaveBeenCalledWith(0, 23) - expect(visible.terminal.refresh).toHaveBeenCalledWith(0, 23) - }) - - it('disposes a retained Windows context when its pane unmounts', () => { - stubRendererWindow('win32') - const dispose = vi.fn() - const pane = createPane({ webglAddon: { dispose } as never }) - const panes = new Map([[pane.id, pane]]) - suspendPaneRendering([pane]) - expect(retainedWebglPaneCount()).toBe(1) - - disposePane(pane, panes) - - expect(dispose).toHaveBeenCalledTimes(1) - expect(pane.webglAddon).toBeNull() - expect(panes.has(pane.id)).toBe(false) - expect(retainedWebglPaneCount()).toBe(0) - }) - - it('drains all retained contexts when a pane manager is destroyed', () => { - stubRendererWindow('win32') - const first = createPane({ id: 1 }) - const second = createPane({ id: 2 }) - const panes = new Map([ - [first.id, first], - [second.id, second] - ]) - suspendPaneRendering(panes.values()) - const root = { - innerHTML: 'mounted', - querySelectorAll: vi.fn(() => []) - } - - PaneManager.prototype.destroy.call({ - destroyed: false, - panes, - identities: { clear: vi.fn() }, - root, - activePaneId: first.id, - dragState: { - dragSourcePaneId: null, - dropOverlay: null, - currentDropTarget: null, - currentExternalDropTarget: null, - cleanupActiveDrag: null - }, - cancelPendingPaneReparentFrames: vi.fn() - } as never) - - expect(retainedWebglPaneCount()).toBe(0) - expect(panes.size).toBe(0) - expect(root.innerHTML).toBe('') - }) - it('cancels a pending WebGL refresh when the pane is disposed', () => { const cancelAnimationFrame = vi.fn() vi.stubGlobal('cancelAnimationFrame', cancelAnimationFrame) diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts b/src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts index be4d97d97..26919ed4d 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts @@ -123,23 +123,4 @@ describe('terminal WebGL addon lifecycle', () => { expect(pane.terminal.refresh).not.toHaveBeenCalled() }) - - it('skips atlas recovery while a retained pane is hidden', () => { - const pane = createPane() - pane.webglAttachmentDeferred = true - pane.webglAddon = { clearTextureAtlas: vi.fn() } as never - - resetWebglTextureAtlas(pane) - - expect(pane.terminal.refresh).not.toHaveBeenCalled() - }) - - it('keeps atlas recovery for a hidden DOM-rendered pane', () => { - const pane = createPane() - pane.webglAttachmentDeferred = true - - resetWebglTextureAtlas(pane) - - expect(pane.terminal.refresh).toHaveBeenCalledWith(0, 23) - }) }) diff --git a/src/renderer/src/lib/pane-manager/pane-webgl-renderer.ts b/src/renderer/src/lib/pane-manager/pane-webgl-renderer.ts index fff802a41..65e78c008 100644 --- a/src/renderer/src/lib/pane-manager/pane-webgl-renderer.ts +++ b/src/renderer/src/lib/pane-manager/pane-webgl-renderer.ts @@ -3,7 +3,6 @@ import type { ManagedPaneInternal } from './pane-manager-types' import { recordTerminalWebglDiagnostic } from '../../../../shared/terminal-webgl-diagnostics' import { getLivePaneCensus } from './pane-manager-registry' import { forceRepaintThroughRenderPause } from './terminal-render-pause-release' -import { releaseRetainedWebglPane } from './pane-webgl-context-retention' import { getTerminalWebglAutoDecision, resetTerminalWebglAutoDecision @@ -18,11 +17,9 @@ let suggestedRendererType: 'dom' | undefined // attach constantly in "on" mode. Latch the first failure and skip attempts // until the next recovery boundary (rendering resume or GPU-setting change). let webglAttachFailedSinceRecovery = false -let contextInspectionUnavailableRecorded = false type ReleasableWebglContext = { getExtension(name: 'WEBGL_lose_context'): WEBGL_lose_context | null - isContextLost?: () => boolean } type XtermWebglAddonInternals = { @@ -74,28 +71,10 @@ export function cancelPendingWebglRefresh(pane: ManagedPaneInternal): void { pane.pendingWebglRefreshRafId = null } -export function isPaneWebglContextLost(pane: ManagedPaneInternal): boolean { - try { - const renderer = (pane.webglAddon as unknown as XtermWebglAddonInternals | null)?._renderer - const isContextLost = renderer?._gl?.isContextLost - if (!isContextLost) { - if (pane.webglAddon && !contextInspectionUnavailableRecorded) { - contextInspectionUnavailableRecorded = true - recordTerminalWebglDiagnostic('webgl-context-inspection-unavailable', { paneId: pane.id }) - } - return false - } - return isContextLost.call(renderer._gl) === true - } catch { - return true - } -} - export function disposeWebgl( pane: ManagedPaneInternal, options?: { refreshDimensions?: boolean } ): void { - releaseRetainedWebglPane(pane) cancelPendingWebglRefresh(pane) if (!pane.webglAddon) { return @@ -146,8 +125,7 @@ export function markComplexScriptOutput(pane: ManagedPaneInternal): void { } export function resetWebglTextureAtlas(pane: ManagedPaneInternal): void { - // Retained contexts repaint on resume; hidden DOM panes keep the existing recovery path. - if (pane.webglDisabledAfterContextLoss || (pane.webglAttachmentDeferred && pane.webglAddon)) { + if (pane.webglDisabledAfterContextLoss) { return } try { @@ -215,11 +193,10 @@ export function attachWebgl(pane: ManagedPaneInternal): void { // visually blank, so keep the pane on the DOM renderer until the next // rendering resume (worktree foreground / window wake) retries it. pane.webglDisabledAfterContextLoss = true - disposeWebgl(pane, { refreshDimensions: !pane.webglAttachmentDeferred }) + disposeWebgl(pane, { refreshDimensions: true }) }) pane.terminal.loadAddon(addon) pane.webglAddon = addon - pane.webglRebuildDeferred = false refreshTerminalAfterWebglAttach(pane) } catch (err) { if (pane.terminalGpuAcceleration === 'auto') { diff --git a/src/shared/terminal-webgl-context-budget.ts b/src/shared/terminal-webgl-context-budget.ts deleted file mode 100644 index 05456ccd8..000000000 --- a/src/shared/terminal-webgl-context-budget.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const TERMINAL_WEBGL_MAX_ACTIVE_CONTEXTS = 128 - -// Bounds worktree-surface retention; active-worktree tabs use the remaining budget. -export const TERMINAL_WEBGL_RETAINED_WORKTREE_CONTEXTS = 32