mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(terminal): retry deferred DPR repair
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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).
|
||||
|
||||
+72
-11
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user