Skip to content

Clear the dirty region in the CoreGraphics renderer (fixes restricted-region scroll artifacts)#582

Merged
migueldeicaza merged 1 commit into
migueldeicaza:mainfrom
tavvet:fix-restricted-region-scroll-repaint
Jul 1, 2026
Merged

Clear the dirty region in the CoreGraphics renderer (fixes restricted-region scroll artifacts)#582
migueldeicaza merged 1 commit into
migueldeicaza:mainfrom
tavvet:fix-restricted-region-scroll-repaint

Conversation

@tavvet

@tavvet tavvet commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Scrolling or editing inside a restricted DECSTBM scroll region (a fixed header and/or footer, as in nano, vim, less, htop) leaves stale rows and a sub-cell "ghost" on the region's last row on the CoreGraphics renderer (Metal disabled). The Metal path masks it by clearing the whole target each frame.

Reproducible: open nano on a file with lines wider than the window and hold PageDown/PageUp; or do vim line edits in a fixed-footer layout.

Root cause — the renderer, not the scroll primitives

The scroll/edit primitives report a correct dirty range. The CG renderer:

  1. never clears the dirty region's background before painting — drawTerminalContents fills background per-cell-run only, and only for cells that carry an explicit background; default-background cells rely on transparent backing-store pixels showing the layer's backgroundColor. AppKit clears the backing store only on a full-view redraw, so a partial repaint (a restricted region, IL/DL) keeps stale glyphs/backgrounds.
  2. only invalidates the sub-cell bottom remainder when rowEnd == rows-1 — a restricted region ends at scrollBottom < rows-1, so the sliver below it is never repainted → the ghost.

(There is no pixel blit; unchanged pixels persist via AppKit's dirty-rect coalescing over the layer backing store.)

Fix — macOS CG path only; Metal unaffected

In AppleTerminalView:

  1. context.clear(dirtyRect) before the per-row paint — clears the invalidated region to transparent (a clear, not a fill, so a translucent background is preserved), giving the per-cell paint a clean slate.
  2. Extend a mid-screen region's invalidated region down by one cell, so the bottom remainder is cleared too (previously only rowEnd == rows-1 got this).

This fixes every row-mutating primitive (scroll, reverseIndex, cmdScrollUp/Down, cmdInsertLines/DeleteLines) at the source. With the renderer clearing any dirtied region, the primitives only need to flag the rows they changed — so refreshScrolledRegion flags just [scrollTop, scrollBottom] (keeping the full-screen + scrollback cheap path) instead of inflating to the whole viewport.

The Metal renderer is untouched: it already loadAction = .clears the whole target each frame, and the region extension only feeds the non-Metal setNeedsDisplay.

Testing

Verified in nano / vim / less / htop on the CG renderer — no stale rows or bottom ghost while paging or editing — with a translucent background intact, and normal full-screen output scrolling unaffected (cheap path preserved). The package builds cleanly.


This supersedes an earlier per-primitive workaround on this branch (which inflated each scroll op's dirty range to the whole viewport) with the renderer-level root fix.

@tavvet tavvet force-pushed the fix-restricted-region-scroll-repaint branch 3 times, most recently from 9fca016 to f6c4776 Compare June 27, 2026 16:51
@tavvet tavvet changed the title Fix stale rows when scrolling a restricted region (CoreGraphics renderer) Fix stale rows when scrolling or editing a restricted region (CoreGraphics renderer) Jun 27, 2026
…-region artifacts)

Scrolling or editing inside a restricted DECSTBM scroll region (a fixed header/
footer, as in nano/vim/less/htop) left stale rows and a sub-cell "ghost" on the
region's last row on the CoreGraphics renderer (Metal masks it by clearing the
whole target each frame).

The defect is in the renderer, not the scroll primitives: drawTerminalContents
fills only cells that carry an explicit background — default-background cells rely
on transparent backing-store pixels showing the layer's background color — and
AppKit clears the backing store only on a full-view redraw. So a partial repaint
(a restricted region, IL/DL) keeps stale pixels, and the sub-cell remainder below
the band is only invalidated when rowEnd == rows-1.

Fix in AppleTerminalView (macOS CG path only, Metal unaffected):
- clear the invalidated region to transparent before the per-row paint (preserves
  a translucent background), and
- extend a mid-screen region's invalidation down one cell so its bottom remainder
  is cleared too.

This fixes every row-mutating primitive at the source. The scroll primitives then
only need to flag the rows they changed, so refreshScrolledRegion flags just
[scrollTop, scrollBottom] instead of the whole viewport, keeping the
full-screen+scrollback cheap path. Verified in nano/vim/less/htop, translucency
intact; the package builds cleanly.
@tavvet tavvet force-pushed the fix-restricted-region-scroll-repaint branch from f6c4776 to d8cda94 Compare June 27, 2026 17:58
@tavvet tavvet changed the title Fix stale rows when scrolling or editing a restricted region (CoreGraphics renderer) Clear the dirty region in the CoreGraphics renderer (fixes restricted-region scroll artifacts) Jun 27, 2026
@migueldeicaza

Copy link
Copy Markdown
Owner

Thank you, let me review this - it looks pretty clean.

@migueldeicaza

Copy link
Copy Markdown
Owner

This looks good!

@migueldeicaza migueldeicaza merged commit d5ee56e into migueldeicaza:main Jul 1, 2026
@tavvet tavvet deleted the fix-restricted-region-scroll-repaint branch July 2, 2026 05:00
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