Skip to content

Defense-in-depth: reconcile occlusion state in forceRefresh - #2486

Open
sefr-sefr wants to merge 1 commit into
manaflow-ai:mainfrom
sefr-sefr:fix/occlusion-reconcile-refresh
Open

sefr-sefr wants to merge 1 commit into
manaflow-ai:mainfrom
sefr-sefr:fix/occlusion-reconcile-refresh

Conversation

@sefr-sefr

@sefr-sefr sefr-sefr commented Apr 1, 2026 •

Copy link
Copy Markdown

Summary

Adds setOcclusion(view.isVisibleInUI) to TerminalSurface.forceRefresh(), following the existing pattern of reconciling display ID and Metal layer size before forcing a surface redraw.

Why

forceRefresh is the established recovery method for rendering issues — it is called on focus changes, geometry reconciliation, surface creation, user input, and via the cmux refresh-surfaces CLI command. It already re-asserts the display ID (to restart stuck CVDisplayLinks) and re-syncs the Metal layer size. Adding occlusion reconciliation makes it a recovery point for any occlusion state desync.

This is defense-in-depth: if any code path causes Ghostty's renderer to think a surface is occluded when it should be visible, the next forceRefresh call will correct it. ghostty_surface_set_occlusion is cheap (lock-free mailbox push) and idempotent (renderer thread deduplicates), so the redundant calls on already-synced surfaces have negligible cost.

Change

One line added after the guard clauses in forceRefresh(). No other changes.

Test plan

Relates to #1156, #2224, #914, #2279


Summary by cubic

Add occlusion reconciliation to TerminalSurface.forceRefresh() by calling setOcclusion(view.isVisibleInUI) before forcing a redraw. This keeps renderer visibility in sync and makes each refresh a recovery point for occlusion desyncs (on focus changes, geometry updates, input, surface creation, and cmux refresh-surfaces) with negligible overhead.

Written for commit f4b4786. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where the terminal view's visibility state was not properly synchronized with the renderer during focus changes and window updates, ensuring consistent display behavior.

forceRefresh already reconciles display ID (to restart stuck
CVDisplayLinks) and Metal layer size before forcing a redraw.
Adding occlusion reconciliation makes every forceRefresh call a
recovery point for any occlusion desync, regardless of cause.

forceRefresh is called on: focus changes, geometry reconciliation,
surface creation, user input, and the cmux refresh-surfaces CLI.
ghostty_surface_set_occlusion is cheap (mailbox push) and idempotent
(renderer-side dedup), so redundant calls have negligible cost.
@vercel

vercel Bot commented Apr 1, 2026

Copy link
Copy Markdown

@peteraxelblom is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@sefr-sefr

Copy link
Copy Markdown
Author

Related PRs: #2484 (root cause fix — cache removal) and #2485 (sync occlusion on creation). Each PR is independent. This PR adds defense-in-depth recovery for any occlusion desync.

@coderabbitai

coderabbitai Bot commented Apr 1, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71c1c449-f089-487c-b64c-638848ab8122

📥 Commits

Reviewing files that changed from the base of the PR and between 3d889fe and f4b4786.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

📝 Walkthrough

Walkthrough

The change modifies TerminalSurface.forceRefresh to re-apply Ghostty occlusion state by calling setOcclusion(view.isVisibleInUI) after verifying the current surface. This synchronizes the renderer's occlusion setting with the view's visibility during refresh operations, functioning as a recovery mechanism for skipped prior updates.

Changes

Cohort / File(s) Summary
Occlusion State Synchronization
Sources/GhosttyTerminalView.swift
Re-applies Ghostty occlusion state in TerminalSurface.forceRefresh to ensure renderer's occlusion setting stays synchronized with SwiftUI/AppKit view visibility during refresh cycles.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐰 With whiskers twitching, I declare with glee,
The occlusion state now flows so free!
When surfaces refresh and views appear,
Synchronization blooms, crystal clear. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding occlusion state reconciliation to forceRefresh as a defensive measure.
Description check ✅ Passed The description covers the required Summary and Why sections with clear rationale, but the Testing section only lists planned tests without verification of completion.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@greptile-apps

greptile-apps Bot commented Apr 1, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a single line — setOcclusion(view.isVisibleInUI) — to TerminalSurface.forceRefresh() in GhosttyTerminalView.swift, making every refresh a recovery point for occlusion state desync between cmux and the Ghostty renderer.

  • forceRefresh() is already the established recovery method for rendering issues (called on focus changes, geometry reconciliation, surface creation, keystrokes, and cmux refresh-surfaces). It already re-asserts the display ID and Metal layer size; occlusion now joins that set.
  • setOcclusion has its own guard let surface check, consistent with the defensive re-read pattern documented in the surrounding comment (issue Crash: use-after-free in ghostty_surface_refresh during geometry reconcile after wake #432 guard).
  • The call uses view captured by the enclosing guard let view = attachedView — the ARC retain keeps it live for the duration of forceRefresh, so there is no dangling-reference risk.
  • CLAUDE.md prohibits adding allocations, file I/O, or formatting to forceRefresh(); this change adds only a C FFI call that performs a lock-free mailbox push, which satisfies that constraint.
  • The unconditional call (no change-guard like setVisibleInUI uses) is intentional: the purpose is recovery, not steady-state notification, and ghostty_surface_set_occlusion is idempotent.
  • No new files, no API changes, no observable behaviour change on already-synced surfaces.

Confidence Score: 5/5

Safe to merge — single idempotent C FFI call added to an already-guarded path, consistent with existing patterns.

No P0 or P1 findings. The change is one line, well-commented, and follows the exact same defensive pattern already used for display ID reassertion. It does not introduce allocations, file I/O, or formatting (as prohibited by CLAUDE.md for this path), and the lock-free, idempotent nature of ghostty_surface_set_occlusion is clearly stated in both the PR description and the surrounding code comments.

No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds setOcclusion(view.isVisibleInUI) to forceRefresh() for defense-in-depth occlusion reconciliation; follows existing patterns and all guards are intact.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant FR as forceRefresh()
    participant SO as setOcclusion()
    participant DID as ghostty_surface_set_display_id
    participant GFR as view.forceRefreshSurface()
    participant GR as ghostty_surface_refresh

    Caller->>FR: forceRefresh(reason:)
    FR->>FR: guard attachedView, surface, window, bounds
    FR->>SO: setOcclusion(view.isVisibleInUI) NEW
    Note over SO: lock-free mailbox push, renderer deduplicates
    SO->>SO: guard let surface = self.surface
    FR->>DID: ghostty_surface_set_display_id(currentSurface, displayID)
    FR->>GFR: view.forceRefreshSurface()
    FR->>FR: guard let surface = self.surface re-read
    FR->>GR: ghostty_surface_refresh(surface)
Loading

Reviews (2): Last reviewed commit: "fix: reconcile occlusion state in forceR..." | Re-trigger Greptile

// the renderer may think this surface is occluded. Re-asserting here
// provides a recovery path on focus changes, geometry updates, and
// manual cmux refresh-surfaces invocations.
setOcclusion(view.isVisibleInUI)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hot keystroke path — verify against CLAUDE.md guidance

CLAUDE.md explicitly calls out TerminalSurface.forceRefresh() as a typing-latency-sensitive function: "called on every keystroke. Do not add allocations, file I/O, or formatting here."

The new setOcclusion call doesn't add any of those three prohibited categories — it's a C FFI call that does a lock-free mailbox push — but it does add a call site that fires on every keystroke (the keyDown.textInput path). On that path isVisibleInUI is virtually always true, so every keystroke results in a redundant ghostty_surface_set_occlusion(surface, true).

The PR description states the call is idempotent and the renderer deduplicates, which is reassuring. Worth briefly confirming whether Ghostty's mailbox dedup happens entirely before acquiring any lock (even a spinlock) or whether there's any coordination cost, since even a compare-and-swap on the hot path can affect P99 keystroke latency in profiling.

Context Used: CLAUDE.md (source)

@sefr-sefr

Copy link
Copy Markdown
Author

Closing — pushed prematurely without testing. Will reopen after manual verification.

@sefr-sefr sefr-sefr closed this Apr 1, 2026
@sefr-sefr

Copy link
Copy Markdown
Author

Manually tested on a debug build with all three changes applied. No jitter regression from the additional setOcclusion calls. Reopening.

@sefr-sefr sefr-sefr reopened this Apr 1, 2026
@atani

atani commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Solid defense-in-depth addition. Making forceRefresh a recovery point for occlusion desync is a natural fit — it already reconciles display ID and Metal layer size, so adding occlusion follows the same pattern. Approve with minor suggestions.

Rebase needed (blocking)

Line numbers and surrounding context differ from current main. A rebase is needed for a clean merge.

Suggestions (non-blocking)

Trim the comment.
The current 5-line comment partially restates what the code does. The key insight is why this is here — the recovery aspect. Shorter version:

// Recovery: re-sync occlusion in case a prior call was dropped
// (e.g. terminalSurface was nil during a visibility transition).
setOcclusion(view.isVisibleInUI)

Verify comment syntax.
The patch shows / Reconcile occlusion: (single slash) — if that matches the actual branch, it would be a syntax error. Likely just a patch formatting artifact, but worth double-checking.

Consider consolidating with #2484 and #2485.
Same file, same bug class — a single PR would make the layered fix strategy easy to review.

Great addition to the recovery toolkit.

@teamleaderleo teamleaderleo added area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts S3: minor Wrong behavior with a workaround labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants