diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 321339f1786..457d51fc47e 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -12307,10 +12307,40 @@ "oracle": "Screenshot the revealed pane, force a model-invalidating full repaint, screenshot again, and assert the bounded pixel diff is immaterial; pair with a JS-level render-model-vs-buffer comparison. Compositor screenshots only, because WebGL canvas readback is blank with preserveDrawingBuffer disabled.", "commands": [ "pnpm run ensure:electron-runtime && pnpm exec playwright test tests/e2e/terminal-reveal-paused-render-repro.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts", "ORCA_E2E_RECORD_VIDEO=1 pnpm exec playwright test tests/e2e/terminal-reveal-paused-render-repro.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1 --repeat-each=3" ], - "testFiles": ["tests/e2e/terminal-reveal-paused-render-repro.spec.ts"], + "testFiles": [ + "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts", + "src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts", + "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts", + "src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts", + "src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts", + "src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts", + "src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts", + "tests/e2e/terminal-reveal-paused-render-repro.spec.ts" + ], "assertionRefs": [ + { + "file": "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts", + "assertions": [ + "deduplicates a deferred dpr retry and consumes it after repair succeeds", + "caps a deferred dpr transition at sixteen animation-frame retries" + ] + }, + { + "file": "src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts", + "assertions": [ + "checks dpr after a heavy reveal even when the stable fit skips geometry work" + ] + }, + { + "file": "src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts", + "assertions": [ + "repairs when the canvas and renderer dimensions share a stale dpr cache", + "defers an unmeasurable WebGL canvas but accepts a renderer-less pane" + ] + }, { "file": "tests/e2e/terminal-reveal-paused-render-repro.spec.ts", "assertions": ["@headful tab reveal presents a synchronized-output WebGL frame"] @@ -12318,13 +12348,22 @@ ], "evidenceRuns": [ { - "date": "2026-08-22", + "date": "2026-08-23", + "runner": "local", + "platform": "macos", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts", + "result": "passed", + "durationSeconds": 2, + "summary": "All 98 renderer contracts passed, including same-DPR zero-scan behavior, visible-only fanout, tri-state DPR repair, deduplicated bounded retry, heavy-reveal repair, and listener cleanup." + }, + { + "date": "2026-08-23", "runner": "local", "platform": "macos", "command": "pnpm run ensure:electron-runtime && pnpm exec playwright test tests/e2e/terminal-reveal-paused-render-repro.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", "result": "passed", - "durationSeconds": 16, - "summary": "A real WebGL tab reveal matched the current serialized synchronized-output frame with zero changed pixels after a direct renderer present. The test persists and attaches the exact before/after screenshots and pixel-diff metadata." + "durationSeconds": 15, + "summary": "The final branch state passed: a real WebGL tab reveal matched the current serialized synchronized-output frame with zero changed pixels after a direct renderer present. The test persists and attaches the exact before/after screenshots and pixel-diff metadata." }, { "date": "2026-08-22", @@ -12342,7 +12381,7 @@ }, "flakeHistory": { "status": "unknown", - "evidence": "Four local macOS headed runs passed, including three consecutive video-recorded repeats; CI soak history and cross-platform GPU runs are not yet available." + "evidence": "Five local macOS headed runs passed, including one on the final branch state and three consecutive video-recorded repeats; CI soak history and cross-platform GPU runs are not yet available." }, "redGreenEvidence": { "status": "complete", @@ -12350,7 +12389,7 @@ }, "performanceBudget": { "required": true, - "evidence": "The tab-reveal present path runs only at event-driven recovery boundaries and adds no polling, provider calls, PTY scans, or background output work. The shared terminal-render.atlas-recovery-fanout contracts separately prove zero hidden-manager atlas clears or presents and a per-pane display:none retry deduped and capped at 16 animation frames." + "evidence": "The tab-reveal present path runs only at event-driven recovery boundaries and adds no polling, provider calls, PTY inventory scans, or background output work. Gate-local contracts prove ordinary same-DPR resize performs zero pane scans; a true transition scans only the visible manager; unavailable canvas retry is deduplicated and capped at 16 frames; and heavy reveal adds one bounded visible-pane scan. The shared terminal-render.atlas-recovery-fanout contracts separately prove zero hidden-manager atlas clears or presents and a per-pane display:none retry capped at 16 frames." }, "promotionCriteria": [ "Establish a stable material-diff threshold across platforms via nightly runs.", 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 557ef20ecb1..5837bdafda7 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 @@ -148,6 +148,16 @@ describe('resumeTerminalVisibility reveal repaint', () => { expect(flushDeferredPaneMetricOptionsIfMeasurable).not.toHaveBeenCalled() }) + it('checks dpr after a heavy reveal even when the stable fit skips geometry work', () => { + const pane = { terminal: {} } + const manager = createManager() + manager.getPanes.mockReturnValue([pane]) + + resumeTerminalVisibility(resumeArgs(manager, false)) + + expect(repairPaneWebglCanvasDprMismatch).toHaveBeenCalledWith(pane) + }) + it('does not fit on a light tab reveal', () => { const manager = createManager() resumeTerminalVisibility(resumeArgs(manager, true)) 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 97590a2e328..0d69b5c00f7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts @@ -226,6 +226,13 @@ function resumeTerminalVisibilityHeavy(manager: PaneManager, isActive: boolean): // Windows (ANGLE -> D3D11) it can be 100-500 ms but a deferred resume // would paint a stretched DOM-fallback flash, which is worse UX. manager.resumeRendering() + // Why: unchanged grid geometry can skip the reveal fit, but a retained WebGL + // canvas may still carry the previous display's backing-store DPR. + for (const pane of manager.getPanes()) { + if (repairPaneWebglCanvasDprMismatch(pane)) { + presentPaneViewport(pane) + } + } // Why: resumeRendering just re-attached WebGL, whose cell metrics briefly differ // from the DOM renderer's; a raw fit here reflows on a transient one-column-off // grid and garbles diff-painting inline TUIs (grok minimize→restore). diff --git a/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts b/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts index 81ec30fc916..c903ec1a2f9 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts @@ -2,14 +2,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderHook } from '@testing-library/react' import type { PaneManager } from '@/lib/pane-manager/pane-manager' +import type { PaneWebglCanvasDprRepairState } from '@/lib/pane-manager/terminal-canvas-dpr-repair' const { recoverVisibleTerminalWindowWakeMock, - repairPaneWebglCanvasDprMismatchMock, + repairPaneWebglCanvasDprMock, presentPaneViewportMock } = vi.hoisted(() => ({ recoverVisibleTerminalWindowWakeMock: vi.fn(), - repairPaneWebglCanvasDprMismatchMock: vi.fn(() => false), + repairPaneWebglCanvasDprMock: vi.fn<() => PaneWebglCanvasDprRepairState>(() => 'current'), presentPaneViewportMock: vi.fn() })) @@ -18,7 +19,7 @@ vi.mock('./terminal-visibility-resume', () => ({ })) vi.mock('@/lib/pane-manager/terminal-canvas-dpr-repair', () => ({ - repairPaneWebglCanvasDprMismatch: repairPaneWebglCanvasDprMismatchMock + repairPaneWebglCanvasDpr: repairPaneWebglCanvasDprMock })) vi.mock('@/lib/pane-manager/pane-webgl-renderer', () => ({ @@ -43,9 +44,9 @@ describe('useTerminalWindowWakeRecovery', () => { beforeEach(() => { systemResumedCallback = null recoverVisibleTerminalWindowWakeMock.mockClear() - repairPaneWebglCanvasDprMismatchMock.mockClear() + repairPaneWebglCanvasDprMock.mockClear() presentPaneViewportMock.mockClear() - repairPaneWebglCanvasDprMismatchMock.mockReturnValue(false) + repairPaneWebglCanvasDprMock.mockReturnValue('current') unsubscribeSystemResumed.mockClear() onSystemResumed.mockClear() resetTerminalFreezeBreadcrumbsForTesting() @@ -203,7 +204,7 @@ describe('useTerminalWindowWakeRecovery', () => { // move) even if the CSS box is unchanged. const pane = { id: 1, terminal: {} } const resizeManager = { getPanes: () => [pane] } as unknown as PaneManager - repairPaneWebglCanvasDprMismatchMock.mockReturnValue(true) + repairPaneWebglCanvasDprMock.mockReturnValue('repaired') vi.stubGlobal('devicePixelRatio', 1) const { unmount } = renderHook(() => useTerminalWindowWakeRecovery({ @@ -215,19 +216,19 @@ describe('useTerminalWindowWakeRecovery', () => { ) window.dispatchEvent(new Event('resize')) - expect(repairPaneWebglCanvasDprMismatchMock).not.toHaveBeenCalled() + expect(repairPaneWebglCanvasDprMock).not.toHaveBeenCalled() vi.stubGlobal('devicePixelRatio', 2) window.dispatchEvent(new Event('resize')) - expect(repairPaneWebglCanvasDprMismatchMock).toHaveBeenCalledWith(pane) + expect(repairPaneWebglCanvasDprMock).toHaveBeenCalledWith(pane) expect(presentPaneViewportMock).toHaveBeenCalledWith(pane) expect(recoverVisibleTerminalWindowWakeMock).not.toHaveBeenCalled() unmount() vi.stubGlobal('devicePixelRatio', 3) window.dispatchEvent(new Event('resize')) - expect(repairPaneWebglCanvasDprMismatchMock).toHaveBeenCalledTimes(1) + expect(repairPaneWebglCanvasDprMock).toHaveBeenCalledTimes(1) }) it('retains a dpr transition until the visible manager is available', () => { @@ -245,10 +246,70 @@ describe('useTerminalWindowWakeRecovery', () => { vi.stubGlobal('devicePixelRatio', 2) window.dispatchEvent(new Event('resize')) - expect(repairPaneWebglCanvasDprMismatchMock).not.toHaveBeenCalled() + expect(repairPaneWebglCanvasDprMock).not.toHaveBeenCalled() managerRef.current = { getPanes: () => [pane] } as unknown as PaneManager window.dispatchEvent(new Event('resize')) - expect(repairPaneWebglCanvasDprMismatchMock).toHaveBeenCalledWith(pane) + expect(repairPaneWebglCanvasDprMock).toHaveBeenCalledWith(pane) + }) + + it('deduplicates a deferred dpr retry and consumes it after repair succeeds', () => { + const pane = { id: 1, terminal: {} } + const callbacks: FrameRequestCallback[] = [] + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + callbacks.push(callback) + return callbacks.length + }) + vi.stubGlobal('devicePixelRatio', 1) + repairPaneWebglCanvasDprMock.mockReturnValueOnce('deferred').mockReturnValue('repaired') + renderHook(() => + useTerminalWindowWakeRecovery({ + isVisible: true, + managerRef: { current: { getPanes: () => [pane] } as unknown as PaneManager }, + isActiveRef: { current: true }, + isVisibleRef: { current: true } + }) + ) + + vi.stubGlobal('devicePixelRatio', 2) + window.dispatchEvent(new Event('resize')) + window.dispatchEvent(new Event('resize')) + expect(repairPaneWebglCanvasDprMock).toHaveBeenCalledTimes(1) + expect(callbacks).toHaveLength(1) + + callbacks.shift()?.(performance.now()) + expect(presentPaneViewportMock).toHaveBeenCalledWith(pane) + window.dispatchEvent(new Event('resize')) + expect(repairPaneWebglCanvasDprMock).toHaveBeenCalledTimes(2) + }) + + it('caps a deferred dpr transition at sixteen animation-frame retries', () => { + const pane = { id: 1, terminal: {} } + const callbacks: FrameRequestCallback[] = [] + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + callbacks.push(callback) + return callbacks.length + }) + vi.stubGlobal('devicePixelRatio', 1) + repairPaneWebglCanvasDprMock.mockReturnValue('deferred') + renderHook(() => + useTerminalWindowWakeRecovery({ + isVisible: true, + managerRef: { current: { getPanes: () => [pane] } as unknown as PaneManager }, + isActiveRef: { current: true }, + isVisibleRef: { current: true } + }) + ) + + vi.stubGlobal('devicePixelRatio', 2) + window.dispatchEvent(new Event('resize')) + let callbackCount = 0 + while (callbacks.length > 0) { + callbacks.shift()?.(performance.now()) + callbackCount += 1 + } + + expect(callbackCount).toBe(16) + expect(repairPaneWebglCanvasDprMock).toHaveBeenCalledTimes(17) }) }) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.ts b/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.ts index c5e578549df..444d0f1f9dd 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import type { PaneManager } from '@/lib/pane-manager/pane-manager' import { recoverVisibleTerminalWindowWake } from './terminal-visibility-resume' -import { repairPaneWebglCanvasDprMismatch } from '@/lib/pane-manager/terminal-canvas-dpr-repair' +import { repairPaneWebglCanvasDpr } from '@/lib/pane-manager/terminal-canvas-dpr-repair' import { presentPaneViewport } from '@/lib/pane-manager/pane-webgl-renderer' import { recordTerminalFreezeBreadcrumb } from './terminal-freeze-breadcrumbs' import type { IDisposable } from '@xterm/xterm' @@ -18,6 +18,8 @@ type WindowWakePtyBinding = IDisposable & { reassertPtySizeAfterWindowWake?: () => void } +const DPR_RECOVERY_RETRY_FRAMES = 16 + export function useTerminalWindowWakeRecovery({ isVisible, managerRef, @@ -30,6 +32,8 @@ export function useTerminalWindowWakeRecovery({ return } let wakeRecoveryFrameId: number | null = null + let dprRecoveryFrameId: number | null = null + let dprRecoveryFramesRemaining = 0 let settledClearGlyphAtlases = false let observedDevicePixelRatio = window.devicePixelRatio const cancelScheduledWakeRecovery = (): void => { @@ -40,6 +44,13 @@ export function useTerminalWindowWakeRecovery({ cancelAnimationFrame(wakeRecoveryFrameId) wakeRecoveryFrameId = null } + const cancelScheduledDprRecovery = (): void => { + if (dprRecoveryFrameId !== null && typeof cancelAnimationFrame === 'function') { + cancelAnimationFrame(dprRecoveryFrameId) + } + dprRecoveryFrameId = null + dprRecoveryFramesRemaining = 0 + } const reassertPanePtySizes = (): void => { for (const binding of panePtyBindingsRef?.current.values() ?? []) { // Why: one settled read avoids duplicate SSH RPCs while still detecting a dropped resize. @@ -114,6 +125,42 @@ export function useTerminalWindowWakeRecovery({ recoverVisibleWake(true, 'system-resumed') } } + const repairVisiblePanesForDpr = (devicePixelRatio: number): boolean => { + const manager = managerRef.current + if (!manager || !isVisibleRef.current) { + return false + } + let deferred = false + for (const pane of manager.getPanes?.() ?? []) { + const state = repairPaneWebglCanvasDpr(pane) + deferred ||= state === 'deferred' + if (state === 'repaired') { + presentPaneViewport(pane) + } + } + if (!deferred) { + observedDevicePixelRatio = devicePixelRatio + } + return !deferred + } + const scheduleDprRecovery = (): void => { + if (dprRecoveryFrameId !== null || typeof requestAnimationFrame !== 'function') { + return + } + dprRecoveryFramesRemaining = DPR_RECOVERY_RETRY_FRAMES + const retry = (): void => { + dprRecoveryFrameId = null + if (repairVisiblePanesForDpr(window.devicePixelRatio)) { + dprRecoveryFramesRemaining = 0 + return + } + dprRecoveryFramesRemaining -= 1 + if (dprRecoveryFramesRemaining > 0) { + dprRecoveryFrameId = requestAnimationFrame(retry) + } + } + dprRecoveryFrameId = requestAnimationFrame(retry) + } const onWindowResize = (): void => { // Why: Chromium emits window resize on devicePixelRatio changes even when // the CSS box is unchanged (monitor move / undock). xterm's own observer @@ -122,15 +169,11 @@ export function useTerminalWindowWakeRecovery({ if (devicePixelRatio === observedDevicePixelRatio) { return } - const manager = managerRef.current - if (!manager || !isVisibleRef.current) { + if (dprRecoveryFrameId !== null) { return } - observedDevicePixelRatio = devicePixelRatio - for (const pane of manager.getPanes?.() ?? []) { - if (repairPaneWebglCanvasDprMismatch(pane)) { - presentPaneViewport(pane) - } + if (!repairVisiblePanesForDpr(devicePixelRatio)) { + scheduleDprRecovery() } } window.addEventListener('focus', onFocus) @@ -148,6 +191,7 @@ export function useTerminalWindowWakeRecovery({ : null return () => { cancelScheduledWakeRecovery() + cancelScheduledDprRecovery() window.removeEventListener('focus', onFocus) window.removeEventListener('resize', onWindowResize) if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') { diff --git a/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts b/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts index 5c5dc972a12..0d7f241ffbf 100644 --- a/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts +++ b/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import type { ManagedPane } from './pane-manager-types' -import { repairPaneWebglCanvasDprMismatch } from './terminal-canvas-dpr-repair' +import { + repairPaneWebglCanvasDpr, + repairPaneWebglCanvasDprMismatch +} from './terminal-canvas-dpr-repair' function makePane(args: { backingWidth: number @@ -151,6 +154,24 @@ describe('repairPaneWebglCanvasDprMismatch', () => { expect(repairPaneWebglCanvasDprMismatch(noRenderer.pane)).toBe(false) }) + it('defers an unmeasurable WebGL canvas but accepts a renderer-less pane', () => { + const detached = makePane({ + backingWidth: 2160, + expectedWidth: 1080, + dpr: 1, + connected: false + }) + const noRenderer = makePane({ + backingWidth: 2160, + expectedWidth: 1080, + dpr: 1, + hasRenderer: false + }) + + expect(repairPaneWebglCanvasDpr(detached.pane)).toBe('deferred') + expect(repairPaneWebglCanvasDpr(noRenderer.pane)).toBe('current') + }) + it('reports failure without throwing when the repair path throws mid-teardown', () => { const { pane, handleResize } = makePane({ backingWidth: 2160, diff --git a/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.ts b/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.ts index 4b60b80f497..587e01288e4 100644 --- a/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.ts +++ b/src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.ts @@ -23,22 +23,27 @@ type XtermRendererInternals = { handleResize?: (cols: number, rows: number) => void } -export function repairPaneWebglCanvasDprMismatch(pane: ManagedPane): boolean { +export type PaneWebglCanvasDprRepairState = 'current' | 'deferred' | 'repaired' + +export function repairPaneWebglCanvasDpr(pane: ManagedPane): PaneWebglCanvasDprRepairState { const renderer = ( pane.terminal as unknown as { _core?: { _renderService?: { _renderer?: { value?: XtermRendererInternals } } } } )._core?._renderService?._renderer?.value const canvas = renderer?._canvas ?? renderer?._gl?.canvas - if (!renderer || !canvas?.isConnected) { - return false + if (!renderer || !canvas) { + return 'current' + } + if (!canvas.isConnected) { + return 'deferred' } const view = canvas.ownerDocument?.defaultView const expected = renderer.dimensions?.device?.canvas const expectedWidth = expected?.width ?? 0 const expectedHeight = expected?.height ?? 0 if (!view || expectedWidth <= 0 || expectedHeight <= 0) { - return false + return 'deferred' } const staleBackingWidth = canvas.width const staleBackingHeight = canvas.height @@ -53,7 +58,7 @@ export function repairPaneWebglCanvasDprMismatch(pane: ManagedPane): boolean { Math.abs(staleBackingWidth - expectedWidth) <= roundingTolerance && Math.abs(staleBackingHeight - expectedHeight) <= roundingTolerance ) { - return false + return 'current' } try { // Order matters: refresh the renderer's cached dpr/dimensions first, then @@ -63,7 +68,7 @@ export function repairPaneWebglCanvasDprMismatch(pane: ManagedPane): boolean { pane.terminal.refresh(0, pane.terminal.rows - 1) } catch { // Pane may be mid-teardown; the next reveal/fit retries the check. - return false + return 'deferred' } recordTerminalWebglDiagnostic('webgl-canvas-dpr-repair', { paneId: pane.id, @@ -72,5 +77,9 @@ export function repairPaneWebglCanvasDprMismatch(pane: ManagedPane): boolean { ...(cachedDevicePixelRatio === undefined ? {} : { cachedDevicePixelRatio }), devicePixelRatio: view.devicePixelRatio }) - return true + return 'repaired' +} + +export function repairPaneWebglCanvasDprMismatch(pane: ManagedPane): boolean { + return repairPaneWebglCanvasDpr(pane) === 'repaired' }