fix(render): hold terminal repaints after resize and skip rendering while occluded - #346
Conversation
…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.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
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.
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:
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.CAMetalLayerdrawables; each render attempt then blocked the main thread, including input handling and PTY draining, for the full ~1 snextDrawabletimeout.anyDirtypermanently true (theirpresented_epochnever catches up), so the app composited and presented full-window frames at the maximum rate for pixels nobody sees.Solution
SDL_WINDOW_OCCLUDEDis set; PTY output keeps draining at full speed, and the expose event triggers an immediate repaint.anyDirtyonly considers sessions visible in the current view mode.glyph_badge.zigshared by the three overlays; the cwd-bar hotkey label is cached per session. The quit overlay's shimmer moved togfx/shimmer.zigand is reused by the hold and the reveal.docs/perf-debugging.mddocuments the investigation playbook (headless repro through the control socket, codex resize behavior, measured costs, Metal pitfalls), andscripts/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