Surface: visibility-gated rendering, draw throttling, live-resize winsize coalescing#7
Open
robgough wants to merge 1 commit into
Open
Surface: visibility-gated rendering, draw throttling, live-resize winsize coalescing#7robgough wants to merge 1 commit into
robgough wants to merge 1 commit into
Conversation
Render-path efficiency work for hosts that keep several surfaces alive (warm caches) and for battery life generally: - New isRenderVisible parameter on TerminiTerminalView / TerminiSurfaceView (default true — additive). Combined with the window's occlusion state into a canRender gate: a gated surface runs no render timers, skips per-output draws, and gets ghostty_surface_set_occlusion(false) so libghostty's renderer idles too. Output is still processed and PTY winsize still tracks layout; one catch-up draw runs on reveal. Previously a hidden surface receiving output rendered invisibly at up to 30 Hz forever, and the focused surface's idle loop kept drawing with the window occluded or minimized. - Immediate per-output-chunk draws are capped at ~60 fps; under an output flood the 30 Hz burst timer coalesces frames instead of drawing synchronously per chunk. Render timers gained tolerance so the OS can coalesce their wakeups. - PTY winsize pushes are coalesced during live resize: AppKit emits ~60 layout passes/sec while dragging, and each TIOCSWINSZ triggers a SIGWINCH + full redraw in the child (very janky under tmux with aggressive-resize). The visual surface still tracks the window every frame; the winsize push is throttled to ~12 Hz with one authoritative final push in viewDidEndLiveResize. The iOS init accepts isRenderVisible for API parity (its rendering is CADisplayLink-driven and untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RcSW6KsDkBruL1dPD63rhX
arach
reviewed
Jul 12, 2026
| NotificationCenter.default.removeObserver(occlusionObserver) | ||
| self.occlusionObserver = nil | ||
| } | ||
| guard let window else { |
Owner
There was a problem hiding this comment.
When the view detaches, canRender becomes false, but this branch returns without calling renderGateChanged(). The surface remains alive, so Ghostty retains its previous visible state and may keep its renderer/display link active.
Could this call renderGateChanged() instead? It already stops the local render loop when canRender is false and will also propagate ghostty_surface_set_occlusion(surface, false).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Render-path efficiency work from Belfry, which keeps several warm surfaces mounted at once (instant session switching) and cares a lot about battery. Three related changes to the macOS surface:
Visibility gating. New
isRenderVisibleparameter onTerminiTerminalView/TerminiSurfaceView(defaulttrue— fully additive), combined with the hosting window's occlusion state into acanRendergate. A gated surface runs no render timers, skips per-output draws, and getsghostty_surface_set_occlusion(false)so libghostty's renderer idles too; output is still processed and the PTY winsize still tracks layout, with one catch-up draw on reveal. Before: a hidden surface receiving output rendered invisibly at up to 30 Hz forever, and the focused surface's 2 Hz idle loop kept drawing while the window was occluded or minimized.Draw throttling. Every output chunk previously did a synchronous
ghostty_surface_draw— unbounded frame rate under output floods. Immediate draws are now capped at ~60 fps with the existing 30 Hz burst timer coalescing the rest (echo latency for interactive typing is unaffected). Render timers gained tolerances so the OS can coalesce wakeups.Live-resize winsize coalescing. AppKit emits ~60 layout passes/sec during a window drag, and each
TIOCSWINSZis a SIGWINCH + full redraw in the child — very janky under tmux withaggressive-resize. The visual surface still tracks the window every frame; the PTY push is throttled to ~12 Hz with one authoritative final push inviewDidEndLiveResize.Combined with the tick-backstop PR, this took an idle host app from ~22% CPU to ~0.0% measured over 30 s windows, with a busy hidden surface costing nothing until revealed.
Builds clean for macOS (
swift build) and iOS (xcodebuild -scheme Termini -destination 'generic/platform=iOS Simulator'); the iOS init acceptsisRenderVisiblefor API parity and ignores it (its CADisplayLink rendering is untouched).🤖 Generated with Claude Code
https://claude.ai/code/session_01RcSW6KsDkBruL1dPD63rhX