Skip to content

perf(renderer): throttle instead of skip occluded-surface frame generation - #265

Merged
arzafran merged 2 commits into
mainfrom
perf/occluded-render-throttle
Aug 5, 2026
Merged

perf(renderer): throttle instead of skip occluded-surface frame generation#265
arzafran merged 2 commits into
mainfrom
perf/occluded-render-throttle

Conversation

@arzafran

@arzafran arzafran commented Aug 5, 2026

Copy link
Copy Markdown
Member

What this does

Replaces the reverted occluded-surface updateFrame skip with a throttle. Hidden panes (background agent tabs, unfocused splits) still get almost all of the CPU savings, but they no longer freeze state that only progresses inside updateFrame.

This supersedes the earlier skip approach (c25020f99), which shipped a real CPU win but caused two production-visible regressions in CI, where every surface is permanently occluded on the virtual display:

  • a use-after-free via stale surface userdata in the .scrollbar mailbox path (already fixed separately, app-side, and unrelated to this change)
  • the app hanging during socket event-subscription tests, because ghostty_surface_read_text locks renderer_state.mutex and depends on updateFrame making progress — the hard skip left the scrollbar dirty/clear handshake with no live half (scrollbar_dirty is set in updateFrame but only cleared in drawFrame, which is also gated off while invisible)

Both issues are fixed or avoided by this throttle, and the CPU win is preserved at ~95%+.

Summary

  • ghostty submodule bumped to 08bac45e9 on the fork branch perf/occluded-update-frame-throttle (based on c25020f99).
  • renderCallback in src/renderer/Thread.zig now calls updateFrame on every wakeup while visible (unchanged from upstream), and at most once per 250ms (4Hz) while occluded, tracked via a monotonic std.time.Instant timestamp on Thread (mirrors the existing bell-ring throttle pattern in Surface.zig).
  • The timestamp resets to null on the visible→occluded transition so the first occluded update fires immediately rather than waiting a full interval.
  • markDirty() on the occluded→visible transition in drainMailbox is unchanged from the original skip implementation.
  • docs/ghostty-fork.md section 8 rewritten for the throttle, with the merge-conflict note about upstream's second updateFrame caller (renderNow) retained and updated to point at the throttle instead of a hard skip.

Merge gate

This is a probabilistic failure mode (CI hangs on the permanently-occluded virtual display), so the fork branch should not merge to fork main until 3 consecutive green CI runs on this PR.

Test Plan

  • zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast in ghostty/ exits 0
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme programa -configuration Debug -destination 'platform=macOS' succeeds against the new framework
  • 3 consecutive green CI runs on this PR (see merge gate above)

Supersedes the reverted occluded-render skip (c25020f99, PR that caused
CI hangs via ghostty_surface_read_text on the permanently-occluded
virtual display). Throttles updateFrame to 4Hz while occluded instead of
skipping it outright, preserving ~95%+ of the CPU win while keeping the
scrollbar dirty/clear handshake and other updateFrame-gated state
machines making forward progress.

See docs/ghostty-fork.md section 8 for the full rationale.
Downloaded and verified the release asset published by Build GhosttyKit
for PR #265: sha256 8794693b5b06fa6702a61be57442ced102b7ea8955fba97397c72ecb668d970d.
@arzafran
arzafran merged commit 8582e1d into main Aug 5, 2026
25 checks passed
@arzafran
arzafran deleted the perf/occluded-render-throttle branch August 5, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant