Skip to content

fix(render): hold terminal repaints after resize and skip rendering while occluded - #346

Merged
forketyfork merged 3 commits into
mainfrom
fix/toggle-lag-settle-hold
Jul 20, 2026
Merged

fix(render): hold terminal repaints after resize and skip rendering while occluded#346
forketyfork merged 3 commits into
mainfrom
fix/toggle-lag-settle-hold

Conversation

@forketyfork

Copy link
Copy Markdown
Owner

Follow-up to #299: the resize fixes from PRs #303#312 made grid/full toggles resize the focused terminal for real, which surfaced a set of latency and repaint problems with agent sessions. This PR addresses all of them.

Problem

Toggling between grid and full view with a long-history codex session showed several seconds of scrolling text, and the app sporadically froze for about a second in the middle of a frame. Instrumented runs against real sessions traced this to four independent causes:

  1. Codex reacts to any PTY resize by erasing the scrollback (ESC[3J) and re-printing its transcript tail, twice, paced over 3–5 seconds through its streaming path (openai/codex PR #18575). Architect rendered every intermediate state of that repaint.
  2. When the window is fully occluded, macOS stops returning CAMetalLayer drawables; each render attempt then blocked the main thread, including input handling and PTY draining, for the full ~1 s nextDrawable timeout.
  3. In Full view, background sessions kept anyDirty permanently true (their presented_epoch never catches up), so the app composited and presented full-window frames at the maximum rate for pixels nobody sees.
  4. The collapsed overlay badges (⌘O/⌘T/⌘?) and the per-tile hotkey labels created and destroyed an SDL texture every frame; each destroy forces a Metal command-queue flush that can block on drawable acquisition.

Solution

  • Resize-settle hold: after a terminal resize, the renderer keeps showing the pre-resize content (stretched into the new rect, including during expand/collapse animations) until the session's output has been quiet for 400 ms, capped at 5 s. A response grace defers the release while the app is still debouncing the SIGWINCH, and a repaint wave landing during or shortly after the sweep re-engages the hold (at most twice per resize), so codex's second rebuild wave stays hidden. Holds longer than 500 ms show a busy shimmer.
  • Sweep reveal: on release, the shimmer band makes one linear pass at the same speed as the wait shimmer, with minimal overscan so it enters the tile immediately; the old content stays crisp ahead of the front and the new layout appears behind it.
  • Occlusion gate: rendering is skipped while SDL_WINDOW_OCCLUDED is set; PTY output keeps draining at full speed, and the expose event triggers an immediate repaint.
  • Visible-only dirty check: anyDirty only considers sessions visible in the current view mode.
  • Cached static textures: new glyph_badge.zig shared by the three overlays; the cwd-bar hotkey label is cached per session. The quit overlay's shimmer moved to gfx/shimmer.zig and is reused by the hold and the reveal.

docs/perf-debugging.md documents the investigation playbook (headless repro through the control socket, codex resize behavior, measured costs, Metal pitfalls), and scripts/perf/ contains the codex emulator and control-socket client used to verify all of this. 15 new unit tests cover the hold state machine, re-arm logic, visibility gating, and sweep geometry.

Test plan

  • Open several codex sessions with long transcripts, toggle grid/full repeatedly: no seconds-long scrolling; the tile holds its content (shimmer after ~0.5 s) and finishes with a single band sweep in both directions.
  • Toggle with a plain shell focused: the switch feels immediate (release within ~0.5 s, no shimmer).
  • Resize the window by dragging with mixed shell/agent sessions: tiles hold and settle without flicker, sweeps run per tile.
  • Cover the Architect window with another window while agents stream, then uncover it: output has kept up (no backlog), the window repaints immediately, and no ~1 s freezes occur afterwards.
  • Quit with running agents: the quit overlay shimmer looks unchanged.

…hile occluded

Issue: Grid/full view toggles with long-history codex sessions showed
seconds of scrolling text, the app sporadically froze for about a second
mid-frame, and a covered window stalled input and PTY processing.

Solution: Codex erases its scrollback and re-prints its transcript tail
(twice, paced over seconds) after any PTY resize, so the renderer now keeps
the pre-resize content on screen until the repaint settles, then reveals
the new layout with a shimmer sweep at the wait band's speed. Rendering is
skipped while the window is occluded because macOS stops returning Metal
drawables and each render attempt blocks the main thread for the full
nextDrawable timeout. Full view no longer presents frames for invisible
background sessions, and static UI badges cache their textures instead of
forcing a Metal queue flush on every frame.
@forketyfork
forketyfork requested a review from Copilot July 20, 2026 13:43
@forketyfork
forketyfork marked this pull request as ready for review July 20, 2026 13:43
The test hardcoded timestamps that assumed a 500ms sweep; when the sweep duration changed to match the wait shimmer's 1400ms cycle, the first assertion started failing. Derive all timestamps from the settle constants like the neighbouring tests do.

Copilot AI 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.

Pull request overview

This PR is a performance/UX-focused follow-up that reduces terminal repaint latency and avoids main-thread stalls during grid↔full toggles and window resizes, especially for long-running agent TUIs (e.g., codex) and for the macOS occlusion/drawable starvation case.

Changes:

  • Add a per-session “resize-settle hold” + sweep-reveal transition that keeps pre-resize content on screen until output settles (with optional shimmer), then reveals the new layout smoothly.
  • Suppress rendering while the SDL window is occluded and avoid unnecessary full-frame rendering by only treating visible sessions as dirty.
  • Eliminate per-frame create/destroy of small UI textures by caching static glyph badges and CWD-bar hotkey labels; factor shimmer rendering into a shared gfx module and document perf-debugging workflows.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/ui/components/worktree_overlay.zig Switch collapsed overlay hint rendering to cached GlyphBadge to avoid per-frame texture churn.
src/ui/components/recent_folders_overlay.zig Same GlyphBadge adoption for the “⌘O” collapsed overlay hint.
src/ui/components/quit_blocking_overlay.zig Replace custom shimmer implementation with shared gfx/shimmer.zig.
src/ui/components/help_overlay.zig Use GlyphBadge for the collapsed “⌘?” hint and deinit cached resources.
src/ui/components/glyph_badge.zig New reusable cached texture helper for small static overlay badges.
src/ui/components/cwd_bar.zig Cache the per-tile hotkey label texture to avoid per-frame create/destroy stalls.
src/session/state.zig Implement resize-settle hold state machine + timing thresholds and unit tests.
src/render/renderer.zig Add settle-hold + sweep-reveal rendering path and visible-only dirty gating + tests.
src/gfx/shimmer.zig New shared shimmer implementation, including a sweep-reveal helper and tests.
src/c.zig Re-export SDL_GetWindowFlags and SDL_WINDOW_OCCLUDED for occlusion gating.
src/app/runtime.zig Gate rendering while occluded; plumb timestamps into resize; use visible-only dirty check; add tests.
src/app/layout.zig Start resize-settle holds on terminal cell-size changes; update related tests.
scripts/perf/spawn_session.py Add control-socket helper to spawn sessions for perf repro.
scripts/perf/fake_codex.py Add codex behavior emulator for resize repaint waves/perf testing.
README.md Document the new “calm resize repaints” behavior as a user-facing feature.
docs/perf-debugging.md New perf debugging playbook and measured findings for the investigation.
docs/ARCHITECTURE.md Document occlusion gating, visible-only dirty rules, texture caching, and resize-settle hold design.
CLAUDE.md Update developer/agent guidance to include perf-debugging doc and occlusion/texture caching invariants.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22912aed76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/session/state.zig
Addresses the review note on PR #346: a session whose foreground process never reacts to the SIGWINCH held past the 500ms shimmer threshold into the 700ms response grace, flashing the shimmer for ~200ms on every resize. The shimmer signals a repaint in flight, so it is now gated on output having been seen since the resize; silent sessions hold and release without any flash.
@forketyfork
forketyfork merged commit 3e97215 into main Jul 20, 2026
4 checks passed
@forketyfork
forketyfork deleted the fix/toggle-lag-settle-hold branch July 20, 2026 14:09
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.

2 participants