Metal: clear the scroll bar gutter in the static texture (D1)

The NS scroll bars are transparent NSScroller overlays, so the gutter
behind them shows whatever the static texture holds. ns_set_*_scroll_bar
clears that area via ns_clear_frame_area, which targets CoreGraphics and
not the Metal texture, so on a window split the old text in the new
gutter column bled through until the next full redraw.

Wrap the vertical/horizontal scroll bar hooks: they run in redisplay's
layout phase (before update_begin, no render encoder yet), so queue the
scroll bar area rect and flush it to the frame background at the start of
the next frame, then delegate to NS to position the scroller.

Verified with a split probe: gutter text density dropped 0.167 -> 0.011
while the text columns stayed at 0.17; AE vs NS unchanged at ~0.88%.
This commit is contained in:
Andros Fenollosa 2026-06-02 16:11:00 +02:00
parent d1cd7b2405
commit e4ece37534
2 changed files with 81 additions and 0 deletions

View file

@ -164,6 +164,11 @@ typedef struct mtl_spring {
/* Phase 4: animator */
@property (nonatomic, strong) MtlAnimator *animator;
/* D1: scroll bar gutter rects (NSValue-wrapped NSRect, logical pixels) queued
by the scroll bar hooks during the layout phase and flushed to background at
the start of the next frame, when a render encoder is active. */
@property (nonatomic, strong) NSMutableArray *pendingClears;
/* Main Emacs render cycle (renders to staticTexture) */
- (void)beginFrame;
- (void)endFrame;

View file

@ -1156,6 +1156,19 @@ easing_apply (MtlScrollEasing mode, float t)
self.cmdBuf = [g_queue commandBuffer];
[self openRenderEncoderClear:needsClear];
/* D1: flush any scroll bar gutter rects queued by the scroll bar hooks during
the layout phase (no encoder was active then). Clear them to the frame
background so stale text behind the transparent NSScroller is wiped. A full
clear (needsClear) already covers everything, so skip in that case. */
if (!needsClear && self.pendingClears.count)
{
struct frame *f = self.emacsFrame;
unsigned long bg = f ? ns_color_to_pixel (FRAME_BACKGROUND_COLOR (f)) : 0x2E3440;
for (NSValue *v in self.pendingClears)
[self fillRect:[v rectValue] color:bg];
}
[self.pendingClears removeAllObjects];
}
- (void)fillRect:(NSRect)rect color:(unsigned long)color
@ -2054,6 +2067,56 @@ mtl_clear_under_internal_border (struct frame *f)
mtl_clear_frame_area (f, w-b, 0, b, h);
}
/* D1: the NS scroll bars are transparent NSScroller overlays, so the gutter
behind them shows whatever is in the static texture. ns_set_*_scroll_bar
clears that area with ns_clear_frame_area, which targets CoreGraphics, not the
Metal texture, so on a window split the old text in the new gutter column
bleeds through until the next full redraw. Wrap the hooks to queue the scroll
bar area for clearing. The hooks run in redisplay's layout phase, before
update_begin, so there is no active render encoder yet: we record the rect and
flush it to background at the start of the next frame (see beginFrame). Then
delegate to NS to position the scroller. Mirrors ns_set_*_scroll_bar. */
static void (*mtl_orig_set_vsb_hook) (struct window *, int, int, int) = NULL;
static void (*mtl_orig_set_hsb_hook) (struct window *, int, int, int) = NULL;
static void
mtl_queue_clear (struct frame *f, int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) return;
MtlFrameData *fd = mtl_get_frame_data (f);
if (!fd) return;
if (!fd.pendingClears) fd.pendingClears = [NSMutableArray array];
[fd.pendingClears addObject:[NSValue valueWithRect:NSMakeRect (x, y, w, h)]];
}
static void
mtl_set_vertical_scroll_bar (struct window *window,
int portion, int whole, int position)
{
struct frame *f = XFRAME (WINDOW_FRAME (window));
int window_y, window_height;
window_box (window, ANY_AREA, 0, &window_y, 0, &window_height);
mtl_queue_clear (f, WINDOW_SCROLL_BAR_AREA_X (window), window_y,
WINDOW_SCROLL_BAR_AREA_WIDTH (window), window_height);
if (mtl_orig_set_vsb_hook)
mtl_orig_set_vsb_hook (window, portion, whole, position);
}
static void
mtl_set_horizontal_scroll_bar (struct window *window,
int portion, int whole, int position)
{
struct frame *f = XFRAME (WINDOW_FRAME (window));
int window_x, window_width;
window_box (window, ANY_AREA, &window_x, 0, &window_width, 0);
mtl_queue_clear (f, window_x, WINDOW_SCROLL_BAR_AREA_Y (window),
window_width, WINDOW_SCROLL_BAR_AREA_HEIGHT (window));
if (mtl_orig_set_hsb_hook)
mtl_orig_set_hsb_hook (window, portion, whole, position);
}
static void
mtl_flush_display (struct frame *f)
{
@ -2504,6 +2567,19 @@ mtl_patch_terminal_rif (struct frame *f)
term->clear_frame_hook = mtl_clear_frame;
term->frame_up_to_date_hook = mtl_frame_up_to_date;
/* D1: wrap the scroll bar hooks so the gutter is cleared in the Metal static
texture (not just CoreGraphics) when windows are split or re-laid out. */
if (term->set_vertical_scroll_bar_hook != mtl_set_vertical_scroll_bar)
{
mtl_orig_set_vsb_hook = term->set_vertical_scroll_bar_hook;
term->set_vertical_scroll_bar_hook = mtl_set_vertical_scroll_bar;
}
if (term->set_horizontal_scroll_bar_hook != mtl_set_horizontal_scroll_bar)
{
mtl_orig_set_hsb_hook = term->set_horizontal_scroll_bar_hook;
term->set_horizontal_scroll_bar_hook = mtl_set_horizontal_scroll_bar;
}
/* Install a KVO observer so the Metal layer tracks EmacsView size changes.
When the user resizes the window, the CAMetalLayer drawableSize updates
automatically without requiring an explicit Emacs resize event. */