Stabilize terminal render recovery after split topology churn

This commit is contained in:
austinpower1258 2026-02-24 14:20:14 -08:00
parent 2da5070782
commit 98cf07ce2a
2 changed files with 66 additions and 8 deletions

View file

@ -1914,6 +1914,14 @@ final class TerminalSurface: Identifiable, ObservableObject {
return
}
// Reassert display id on topology churn (split close/reparent) before forcing a refresh.
// This avoids a first-run stuck-vsync state where Ghostty believes vsync is active
// but callbacks have not resumed for the current display.
if let displayID = (view.window?.screen ?? NSScreen.main)?.displayID,
displayID != 0 {
ghostty_surface_set_display_id(surface, displayID)
}
view.forceRefreshSurface()
ghostty_surface_refresh(surface)
}

View file

@ -577,6 +577,7 @@ final class Workspace: Identifiable, ObservableObject {
private var debugDidMoveTabEventCount: UInt64 = 0
#endif
private var geometryReconcileScheduled = false
private var geometryReconcileNeedsRerun = false
private var isNormalizingPinnedTabOrder = false
private var pendingNonFocusSplitFocusReassert: PendingNonFocusSplitFocusReassert?
private var nonFocusSplitFocusReassertGeneration: UInt64 = 0
@ -2241,18 +2242,67 @@ final class Workspace: Identifiable, ObservableObject {
/// Reconcile remaining terminal view geometries after split topology changes.
/// This keeps AppKit bounds and Ghostty surface sizes in sync in the next runloop turn.
private func reconcileTerminalGeometryPass() -> Bool {
var needsFollowUpPass = false
// Flush pending AppKit layout first so terminal-host bounds reflect latest split topology.
for window in NSApp.windows {
window.contentView?.layoutSubtreeIfNeeded()
}
for panel in panels.values {
guard let terminalPanel = panel as? TerminalPanel else { continue }
let hostedView = terminalPanel.hostedView
let hasUsableBounds = hostedView.bounds.width > 1 && hostedView.bounds.height > 1
let hasSurface = terminalPanel.surface.surface != nil
let isAttached = hostedView.window != nil && hostedView.superview != nil
// Split close/reparent churn can transiently detach a surviving terminal view.
// Force one SwiftUI representable update so the portal binding reattaches it.
if !isAttached || !hasUsableBounds || !hasSurface {
terminalPanel.requestViewReattach()
needsFollowUpPass = true
}
hostedView.reconcileGeometryNow()
terminalPanel.surface.forceRefresh()
}
return needsFollowUpPass
}
private func runScheduledTerminalGeometryReconcile(remainingPasses: Int) {
guard remainingPasses > 0 else {
geometryReconcileScheduled = false
geometryReconcileNeedsRerun = false
return
}
let needsFollowUpPass = reconcileTerminalGeometryPass()
let shouldRunAgain = geometryReconcileNeedsRerun || needsFollowUpPass
if shouldRunAgain, remainingPasses > 1 {
geometryReconcileNeedsRerun = false
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.runScheduledTerminalGeometryReconcile(remainingPasses: remainingPasses - 1)
}
return
}
geometryReconcileScheduled = false
geometryReconcileNeedsRerun = false
}
private func scheduleTerminalGeometryReconcile() {
guard !geometryReconcileScheduled else { return }
guard !geometryReconcileScheduled else {
geometryReconcileNeedsRerun = true
return
}
geometryReconcileScheduled = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.geometryReconcileScheduled = false
for panel in self.panels.values {
guard let terminalPanel = panel as? TerminalPanel else { continue }
terminalPanel.hostedView.reconcileGeometryNow()
terminalPanel.surface.forceRefresh()
}
self.runScheduledTerminalGeometryReconcile(remainingPasses: 4)
}
}