Metal: render color-font glyphs (Apple Color Emoji) via BGRA textures

The glyph atlas is R8 grayscale coverage, so emoji rendered as dark
monochrome silhouettes (and emoji compositions like heart+VS16 drew
nothing before the composite fix). Detect color fonts via
kCTFontTraitColorGlyphs and rasterize their glyphs into small BGRA
textures with CTFontDrawGlyphs (which renders sbix color), drawn through
the image pipeline. Cached per (font, glyph) with the same metric
conventions as the grayscale atlas (physical-resolution baking, bearing_y
= bh - raster_oy, logical advance); the cache is cleared when the backing
scale changes. Wired into both the plain char loop and composition runs.

Verified: smileys, party popper, red heart (ZWJ/VS16 composition) and
thumbs-up now render in color, matching NS.
This commit is contained in:
Andros Fenollosa 2026-06-03 17:23:31 +02:00
parent 72a246267f
commit 333c1a582e

View file

@ -250,6 +250,8 @@ static int g_atlas_row_h = 0;
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;
/* Color-glyph (emoji) cache lives further down; cleared on scale change. */
static void mtl_color_glyph_cache_clear (void);
/* Phase 4: global animation configuration (Lisp-configurable) */
MtlCursorMode g_mtl_cursor_mode = MTL_CURSOR_SPRING;
@ -1186,6 +1188,7 @@ easing_apply (MtlScrollEasing mode, float t)
g_atlas_scale = scale;
g_atlas_next_x = g_atlas_next_y = g_atlas_row_h = 0;
glyph_cache_init ();
mtl_color_glyph_cache_clear (); /* color glyphs are scale-baked too */
}
/* Ensure staticTexture exists and matches drawable size.
@ -1929,6 +1932,111 @@ mtl_draw_image_texture (MtlFrameData *fd, id<MTLTexture> tex,
[fd.encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
}
/* -----------------------------------------------------------------------
Color glyphs (Apple Color Emoji). The main atlas is R8 grayscale
coverage, which renders emoji as dark silhouettes; color-font glyphs are
rasterized into small BGRA textures instead and drawn with the image
pipeline. Cached per (font, glyph) like the grayscale atlas.
----------------------------------------------------------------------- */
@interface MtlColorGlyph : NSObject
@property (nonatomic, strong) id<MTLTexture> tex;
@property (nonatomic, assign) int w, h; /* physical pixels */
@property (nonatomic, assign) int bearing_x, bearing_y;
@property (nonatomic, assign) float advance_x; /* logical pixels */
@end
@implementation MtlColorGlyph
@end
static NSMutableDictionary<NSNumber *, MtlColorGlyph *> *g_color_glyph_cache;
/* Called when g_atlas_scale changes: color glyphs are baked at physical
resolution too, so they must re-rasterize at the new scale. */
static void
mtl_color_glyph_cache_clear (void)
{
[g_color_glyph_cache removeAllObjects];
}
static MtlColorGlyph *
mtl_color_glyph (CTFontRef font, CGGlyph g)
{
if (!g_device || !font || g == 0) return nil;
uint64_t key = ((uint64_t)(uintptr_t)font * 6364136223846793005ULL) ^ (uint64_t)g;
if (!g_color_glyph_cache)
g_color_glyph_cache = [NSMutableDictionary new];
NSNumber *k = @(key);
MtlColorGlyph *cg = g_color_glyph_cache[k];
if (cg) return cg;
/* Same conventions as mtl_rasterize_glyph_id: physical resolution via a
scaled font copy, +2px padding, origin at floor(-bbox)+1, bearing_y =
bh - raster_oy. */
CGFloat s = g_atlas_scale;
CTFontRef rfont = (s != 1.0)
? CTFontCreateCopyWithAttributes (font, CTFontGetSize (font) * s, NULL, NULL)
: (CTFontRef) CFRetain (font);
CGRect bbox = CTFontGetBoundingRectsForGlyphs (rfont,
kCTFontOrientationDefault, &g, NULL, 1);
CGSize adv;
CTFontGetAdvancesForGlyphs (rfont, kCTFontOrientationDefault, &g, &adv, 1);
int bw = (int)ceil (bbox.size.width) + 2;
int bh = (int)ceil (bbox.size.height) + 2;
if (bw <= 2 || bh <= 2) { CFRelease (rfont); return nil; }
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB ();
size_t bpr = (size_t)bw * 4;
uint8_t *px = (uint8_t *)calloc (1, bpr * (size_t)bh);
CGContextRef ctx = CGBitmapContextCreate (px, (size_t)bw, (size_t)bh, 8, bpr, cs,
(CGBitmapInfo)(kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little));
CGColorSpaceRelease (cs);
if (!ctx) { free (px); CFRelease (rfont); return nil; }
CGPoint origin = CGPointMake (floor (-bbox.origin.x) + 1,
floor (-bbox.origin.y) + 1);
CTFontDrawGlyphs (rfont, &g, &origin, 1, ctx); /* renders color for sbix fonts */
CGContextRelease (ctx);
CFRelease (rfont);
MTLTextureDescriptor *td =
[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:(NSUInteger)bw
height:(NSUInteger)bh
mipmapped:NO];
td.usage = MTLTextureUsageShaderRead;
td.storageMode = MTLStorageModeShared;
id<MTLTexture> tex = [g_device newTextureWithDescriptor:td];
[tex replaceRegion:MTLRegionMake2D (0, 0, (NSUInteger)bw, (NSUInteger)bh)
mipmapLevel:0 withBytes:px bytesPerRow:bpr];
free (px);
int raster_oy = (int)(floor (-bbox.origin.y) + 1);
cg = [MtlColorGlyph new];
cg.tex = tex;
cg.w = bw;
cg.h = bh;
cg.bearing_x = (int)(floor (-bbox.origin.x) + 1);
cg.bearing_y = bh - raster_oy;
cg.advance_x = (float)(adv.width / s);
g_color_glyph_cache[k] = cg;
return cg;
}
/* Draw color glyph G of FONT with its origin (pen) at X and baseline at Y,
exactly like drawGlyph: does for grayscale atlas entries. */
static void
mtl_draw_color_glyph (MtlFrameData *fd, CTFontRef font, CGGlyph g,
float x, float y)
{
MtlColorGlyph *cg = mtl_color_glyph (font, g);
if (!cg) return;
CGFloat s = g_atlas_scale;
mtl_draw_image_texture (fd, cg.tex,
x - cg.bearing_x / s, y - cg.bearing_y / s,
cg.w / s, cg.h / s, 1.0f);
}
/* -----------------------------------------------------------------------
redisplay_interface all file-static, called by xdisp.c engine
----------------------------------------------------------------------- */
@ -2067,14 +2175,27 @@ static void
mtl_draw_cmp_run (struct glyph_string *s, MtlFrameData *fd, CTFontRef ctfont,
unsigned long fg, int from, int to, int x, int y)
{
BOOL color_font =
(CTFontGetSymbolicTraits (ctfont) & kCTFontTraitColorGlyphs) != 0;
float pen = (float) x;
for (int k = from; k < to; k++)
{
MtlGlyphCacheEntry *ge = mtl_cache_glyph_id (ctfont, (CGGlyph) s->char2b[k]);
if (!ge) continue;
if (ge->width > 0)
[fd drawGlyph:ge at:CGPointMake (pen, (float) y) color:fg];
pen += ge->advance_x;
CGGlyph g = (CGGlyph) s->char2b[k];
if (color_font)
{
MtlColorGlyph *cg = mtl_color_glyph (ctfont, g);
if (!cg) continue;
mtl_draw_color_glyph (fd, ctfont, g, pen, (float) y);
pen += cg.advance_x;
}
else
{
MtlGlyphCacheEntry *ge = mtl_cache_glyph_id (ctfont, g);
if (!ge) continue;
if (ge->width > 0)
[fd drawGlyph:ge at:CGPointMake (pen, (float) y) color:fg];
pen += ge->advance_x;
}
}
}
@ -2228,6 +2349,11 @@ mtl_draw_glyph_string_impl (struct glyph_string *s)
CTFontRef ctfont = mtl_ctfont_for_face (face);
if (!ctfont) { mtl_dgs_nofont_count++; return; }
/* Color fonts (Apple Color Emoji) can't go through the R8 coverage
atlas; draw them as small BGRA textures. */
BOOL color_font =
(CTFontGetSymbolicTraits (ctfont) & kCTFontTraitColorGlyphs) != 0;
/* Advance using Emacs's own integer glyph grid
(first_glyph[i].pixel_width), NOT the CoreText float advance.
Re-advancing by the font's fractional advance drifts away from the
@ -2248,16 +2374,25 @@ mtl_draw_glyph_string_impl (struct glyph_string *s)
if (glyphId)
{
MtlGlyphCacheEntry *ge = mtl_cache_glyph_id (ctfont, glyphId);
if (ge && ge->width > 0)
if (color_font)
{
mtl_dgs_drawn_count++;
/* bearing_y: distance from glyph top-left to baseline. In
our top-left coord system, glyph top = baseline_y -
bearing_y. */
[fd drawGlyph:ge at:CGPointMake ((float)pen_x,
(float)baseline_y)
color:fg];
mtl_draw_color_glyph (fd, ctfont, glyphId,
(float)pen_x, (float)baseline_y);
}
else
{
MtlGlyphCacheEntry *ge = mtl_cache_glyph_id (ctfont, glyphId);
if (ge && ge->width > 0)
{
mtl_dgs_drawn_count++;
/* bearing_y: distance from glyph top-left to baseline.
In our top-left coord system, glyph top = baseline_y -
bearing_y. */
[fd drawGlyph:ge at:CGPointMake ((float)pen_x,
(float)baseline_y)
color:fg];
}
}
}