From d1cd7b24051b757bdc2cf5dd0d8d12685ca2ace7 Mon Sep 17 00:00:00 2001 From: Andros Fenollosa Date: Tue, 2 Jun 2026 16:04:00 +0200 Subject: [PATCH] Metal: bake the glyph atlas at physical resolution (Retina/HiDPI) On a 2x display the static texture is physical-sized, but glyphs were rasterized from the font at its logical point size and then stretched to fill it, so text looked soft. Bake each glyph from a copy of the font scaled by the backing factor (g_atlas_scale) and divide the physical metrics back to logical pixels at the draw site, so the quad stays logical-sized but is textured from a high-res glyph that maps ~1:1 onto the physical texture. The atlas is dropped and rebuilt if the scale changes (window moved to a different-DPI monitor). No-op on 1x displays (scale == 1): verified AE unchanged at 0.98% vs NS. Also add Fase I to TODO.org: a neutral drawing-backend abstraction so macOS uses Metal and Linux/Windows use OpenGL. --- src/mtlterm.m | 54 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/mtlterm.m b/src/mtlterm.m index ef824e4e927..734f7bda993 100644 --- a/src/mtlterm.m +++ b/src/mtlterm.m @@ -239,6 +239,12 @@ static id g_atlas = nil; static int g_atlas_next_x = 0; static int g_atlas_next_y = 0; static int g_atlas_row_h = 0; +/* Backing scale the atlas glyphs are rasterized at (1.0 on a 1x display, 2.0 on + Retina). Glyphs are baked at physical resolution so they stay crisp on the + physical-pixel static texture; draw sites divide the physical metrics back to + logical pixels. Reset the atlas when this changes (e.g. window moves to a + monitor with a different DPI). See TODO.org Fase C-bis. */ +static CGFloat g_atlas_scale = 1.0; /* Phase 4: global animation configuration (Lisp-configurable) */ MtlCursorMode g_mtl_cursor_mode = MTL_CURSOR_SPRING; @@ -581,13 +587,22 @@ mtl_rasterize_glyph_id (CTFontRef font, CGGlyph cgGlyph, uint64_t key) if (!g_atlas) return NULL; MtlGlyphCacheEntry *entry; + CGFloat s = g_atlas_scale; - CGRect bbox = CTFontGetBoundingRectsForGlyphs (font, + /* Rasterize at physical resolution: bake the glyph from a copy of the font + scaled by the backing factor. Bbox/advance then come out in physical + pixels; draw sites divide the stored metrics by g_atlas_scale to get back to + logical pixels. On a 1x display this is a no-op (s == 1). */ + CTFontRef rfont = (s != 1.0) + ? CTFontCreateCopyWithAttributes (font, CTFontGetSize (font) * s, NULL, NULL) + : (CTFontRef) CFRetain (font); + + CGRect bbox = CTFontGetBoundingRectsForGlyphs (rfont, kCTFontOrientationDefault, &cgGlyph, NULL, 1); - /* Glyph advance */ + /* Glyph advance (physical → store logical) */ CGSize adv; - CTFontGetAdvancesForGlyphs (font, kCTFontOrientationDefault, + CTFontGetAdvancesForGlyphs (rfont, kCTFontOrientationDefault, &cgGlyph, &adv, 1); int bw = (int)ceil (bbox.size.width) + 2; @@ -596,8 +611,9 @@ mtl_rasterize_glyph_id (CTFontRef font, CGGlyph cgGlyph, uint64_t key) { /* Space or zero-size glyph: cache as empty with correct advance */ entry = glyph_cache_insert (key); + CFRelease (rfont); if (!entry) return NULL; - entry->advance_x = (float)adv.width; + entry->advance_x = (float)(adv.width / s); entry->width = entry->height = 0; return entry; } @@ -635,8 +651,9 @@ mtl_rasterize_glyph_id (CTFontRef font, CGGlyph cgGlyph, uint64_t key) CGContextSetGrayFillColor (ctx, 1.0, 1.0); CGPoint origin = CGPointMake (floor (-bbox.origin.x) + 1, floor (-bbox.origin.y) + 1); - CTFontDrawGlyphs (font, &cgGlyph, &origin, 1, ctx); + CTFontDrawGlyphs (rfont, &cgGlyph, &origin, 1, ctx); CGContextRelease (ctx); + CFRelease (rfont); /* Upload to atlas */ MTLRegion region = MTLRegionMake2D ((NSUInteger)g_atlas_next_x, @@ -663,7 +680,7 @@ mtl_rasterize_glyph_id (CTFontRef font, CGGlyph cgGlyph, uint64_t key) entry->height = bh; entry->bearing_x = (int)(floor (-bbox.origin.x) + 1); entry->bearing_y = bh - 1 - raster_oy; - entry->advance_x = (float)adv.width; + entry->advance_x = (float)(adv.width / s); g_atlas_next_x += bw + 1; if (bh > g_atlas_row_h) g_atlas_row_h = bh; @@ -1098,6 +1115,18 @@ easing_apply (MtlScrollEasing mode, float t) { CGSize dsz = self.metalLayer.drawableSize; + /* Track the backing scale so the glyph atlas is baked at physical resolution. + If it changes (window moved to a different-DPI monitor), drop the atlas so + glyphs re-rasterize at the new scale. */ + NSSize fsz = self.metalLayer.frame.size; + CGFloat scale = fsz.width > 0 ? dsz.width / fsz.width : 1.0; + if (scale > 0 && fabs (scale - g_atlas_scale) > 0.01) + { + g_atlas_scale = scale; + g_atlas_next_x = g_atlas_next_y = g_atlas_row_h = 0; + glyph_cache_init (); + } + /* Ensure staticTexture exists and matches drawable size. Bug fix: multiple update_begin/end cycles happen per Emacs redisplay pass (cursor blink, modeline, etc.). Using MTLLoadActionClear on every beginFrame @@ -1158,10 +1187,15 @@ easing_apply (MtlScrollEasing mode, float t) float fr, fg, fb; unpack_color (fgcolor, &fr, &fg, &fb); - float x0 = (float)(origin.x - ge->bearing_x); - float y0 = (float)(origin.y - ge->bearing_y); - float x1 = x0 + ge->width; - float y1 = y0 + ge->height; + /* The atlas glyph is baked at physical resolution (g_atlas_scale); the draw + site works in logical pixels, so divide the physical metrics back down. The + quad ends up logical-sized but textured from a high-res glyph, which maps + ~1:1 onto the physical static texture and stays crisp on Retina. */ + CGFloat s = g_atlas_scale; + float x0 = (float)(origin.x - ge->bearing_x / s); + float y0 = (float)(origin.y - ge->bearing_y / s); + float x1 = x0 + ge->width / s; + float y1 = y0 + ge->height / s; float u0 = (float)ge->atlas_x / MTL_ATLAS_WIDTH; float v0 = (float)ge->atlas_y / MTL_ATLAS_HEIGHT;