Buffer switches now cross-fade: just before redisplay paints the new
buffer (pre-redisplay-functions), the driver snapshots the static
texture and the compositor fades it out over the new content
(smoothstep, GPU-composited). Configurable with mtl-buffer-transitions
(default t) and mtl-buffer-transition-duration (default 0.15s), and
exposed as mtl-transition-start for custom effects.
Cursor animations are enabled by default now that they behave: the
burst modes (sonicboom/ripple/pixiedust) keep the proper static cursor
-- inverted glyph, exact NS parity -- and only draw their effects in
the overlay; a solid overlay body would have hidden the character
under the cursor. The body-animated modes (spring/torpedo/hollow/
beam) still draw the overlay body.
The animation pump helpers moved above the defcustoms: with the
default flipped to t, custom-initialize-reset runs the :set at load
time, which called mtl--anim-pump-start before it was defined.
README documents the user commands (status, draw-stats, cursor modes,
vsync, video, transitions).
mtl--maybe-enable-on-startup enabled the SELECTED frame instead of the
frame being created (mid-creation the new frame is not selected yet),
and an error there aborted frame creation: tooltip, child and TTY
frames must simply be skipped. Use the hook's frame argument, guard on
display-graphic-p and swallow enable errors.
The application bundle now ships a site-start.el that turns the GPU
backend on by default (EMACS_GPU_DISABLE=1 reverts to the stock
backend); the bundle-build recipe adds it after make install.
Bottom dividers arrive outside the update cycle, where a bare fill is
silently dropped (vertical dividers happened to come in-cycle, which is
why they worked): wrap both divider/border paths in the self-contained
immediate cycle used by the other outside-the-cycle draws.
Also stop the Metal resize observer from writing FRAME_PIXEL_* behind
change_frame_size's back; the view's own resize path already drives the
layout machinery.
The draw params carry the CLIPPED display width while the bit rows stay
MSB-aligned within the bitmap's own width, so a fringe narrower than
the bitmap (e.g. fringe-mode 4 with the 8px buffer-boundary angles)
rendered the bitmap's right half; the native backends draw the full
bitmap and clip, showing the left half. Export the bitmap's true width
from fringe.c (fringe_bitmap_width) and extract bits against it.
draw_bitmap in the gfx driver vtable gains the true-width parameter.
A redisplay pass runs several update cycles back-to-back (buffer window
plus echo area), and with display sync every present blocked on a
drawable: two blocking presents per keystroke halved typing throughput,
and the present itself paid for a second command buffer.
Presents now coalesce: one landing within ~8 ms of the previous commits
to the static texture and is flushed by a one-shot main-queue block
(or absorbed by the next cycle's present), and the composite pass is
encoded on the same command buffer as the cycle's draws. The policy's
flush presents through the same coalescing path.
Benchmarks (M1 Pro, font-locked xdisp.c, 120x45): machine-paced typing
goes from 30 to 106 chars/s and sustained scroll from 116 to 475
redisplays/s with vsync off, matching the stock backend's throughput
and total CPU; with vsync on (default) the same visible 60 fps now
costs ~35% less CPU than stock. README gains the numbers.
Pixel parity, GIF/video playback, the first-tab-switch deferral and the
echo-area tests are all unchanged.
With vsync on (default) presents wait for the panel refresh: redisplay
self-caps at the display rate, which is also a power win (the stock
backend happily renders hundreds of invisible frames per second under
sustained scroll). Disabling it trades that cap for NS-like latency on
machine-paced workloads.
The patched rif and terminal hooks are terminal-wide, but only frames
the user enabled the GPU backend on have driver data: tooltips, child
frames (posframe and friends) and frames created without mtl-enable
came up blank. Capture the original NS implementations before patching
(gfx_fallback) and delegate every policy entry point to them for frames
the driver is not ready on. Verified: tooltip, posframe-style child
frame and a second top-level frame all render identically to a pure NS
session while the main frame renders through Metal.
Also resolved while auditing the display feature matrix:
- Underline styles dots and dashes (FACE_UNDERLINE_DOTS/DASHES) were
drawn as a solid line; port ns_draw_dash ([segment, segment] pattern
anchored at the absolute x so it joins across glyph strings).
- :underline (:position N) larger than the descent crossed the glyphs:
NS computes the descent as unsigned, so the position underflows and
clamps to the row bottom; mirror that clamp.
- The internal border was cleared with the frame background; port
ns_clear_under_internal_border (internal-border/child-frame-border
face, face remapping, top/bottom margins).
- With cursor animations enabled the GPU cursor never blinked
(blink toggles visibility through erase_phys_cursor, which an overlay
never sees): the animation tick now mirrors the window's cursor_off_p
and hides the overlay cursor and trail during the off phase.
- Before the first cursor draw the overlay used a hardcoded pale tint;
fall back to the frame cursor color.
No regressions: baseline 141 px and all capture batteries unchanged.
Cursor-only motion takes redisplay's fast path: no render cycle opens,
and the CADisplayLink stops firing while Emacs idles, so with
animations enabled the moved cursor overlay was never presented (it
appeared frozen until the next input event) and rings/trails never
animated between events.
Three fixes: note_cursor composites immediately when no cycle is open;
the animation step is factored into tickWithDt: and exposed as
mtl-anim-tick, driven by a 30fps Lisp timer while animations are
enabled (the same mechanism that drives video playback); and burst
particles use the real cursor color instead of a hardcoded pale tint
that was invisible on light backgrounds.
Full GPL header blocks everywhere (the abbreviated form is gone), drop
the dead mtl_shaders.metal (shaders are embedded in mtlterm.m), remove
internal phase names and TODO.org references from comments, fix the
package URL in mtl.el to the emacs-gpu repository, and refresh stale
file titles.
Chosen as the default for tests and demos after trying all eight modes
side by side. Animations themselves remain opt-in
(g_mtl_animations_enabled / mtl-animations).
The redisplay-facing drawing policy -- glyph strings with backgrounds,
boxes, reliefs, underlines (incl. wave) and strike-through; composition
strings (static and shaped); images with slices and button reliefs;
cursors; fringe bitmaps; window borders and dividers; scroll blits; and
the render-cycle policy (clear-only present deferral, deferred immediate
presents, pending scroll-bar gutter clears, the minibuffer expose
substitute) -- moves to the platform-neutral gfxterm.c, written in pure C
against a small driver vtable (gfxdrv.h, ~25 ops: cycle control, fill,
clip, glyph atlas via struct font *, textures, masks, region copies,
relief/frame colors, a cursor-animation hint).
mtlterm.m shrinks by ~1000 lines to the Metal driver (pipelines, atlas
and CoreText rasterization, MtlFrameData, animator, video) plus the
mtl_gfx_driver vtable; the patched rif and terminal hooks now point at
the gfx_* policy functions.
OpenGL is NOT implemented, by design: glterm.c is a documented skeleton
of the vtable to fill in, and configure --with-gl is reserved (errors
out pointing at the integration point), so a future GL driver for
GNU/Linux and Windows plugs in under the already-pixel-perfect policy.
Verified zero regressions: baseline 141 px (0.055%), all nine capture
batteries byte-identical (text/disp/bars/bidi/wrap/img/fontlock/echo/
hscroll), GIF animation, inline video, dynamic text-scale, draw-stats
counters, and the clear-only deferral trace shows the same single
present on the first tab switch.
AVFoundation decodes into BGRA pixel buffers (AVPlayerItemVideoOutput)
that CVMetalTextureCache wraps as Metal textures with zero copies; the
compositor draws the newest frame as a textured quad over the static
texture on every present, scissor-clipped to the window interior so a
half-scrolled video does not bleed over the mode line.
Presents are driven by a 30fps Lisp timer (mtl-video-tick): Emacs's
event loop starves the CADisplayLink while idle (it stalled after two
ticks), and Lisp timers are what fire reliably; this also matches how
image-animate drives GIFs. The timer re-anchors the video to a buffer
marker each tick (mtl-video-move), so it follows scrolling and window
changes, and parks it off-screen when not visible.
Lisp API: mtl-video-open/close/pause/move/tick (mtlfns.m) and the
user-facing mtl-video-insert/mtl-video-stop in mtl.el, which insert a
pixel-sized placeholder space and track it.
MtlVideoPlayer goes through property setters everywhere: this file is
compiled without ARC, and raw ivar assignment autoreleased the AVPlayer
graph under us (crash: unrecognized selector on a reused dictionary).
Links AVFoundation/CoreMedia/CoreVideo (configure.ac libs_nsgui and
MTL_LIBS).
Switching to a tab whose buffer faces are not realized yet garbages the
frame: redisplay first runs an update cycle that only calls clear_frame,
and Metal presented that blank frame, flashing white for the ~30 ms the
follow-up cycle needs to realize faces and repaint. NS does not flash
because AppKit coalesces backing-store flushes within the event cycle.
Track per-cycle whether clear_frame ran and whether any glyph string was
drawn; a clear-only cycle commits to the static texture but defers the
present (needsPresent), which the follow-up content cycle or
flush_display then picks up. Also adds MTL_LOG_SEQ=1 tracing of the
begin/clear/draw/end/present sequence, which is what exposed the blank
present.
The IMAGE_GLYPH branch returned before any decoration: tab-bar and
tool-bar buttons (and :relief images) lost their relief, and boxed
image faces lost their box, leaving stale pixels in the box rows that
the inset background fill deliberately skips (seen as a white gap in
the tab-bar bottom edge under the new-tab button).
Port ns_draw_image_relief (button relief with tab/tool-bar margins),
draw the face box after the image like ns_draw_glyph_string, fill the
image background at full cell height (NS skips the box inset there),
honor the left box line and slice origin for the image position, and
factor the relief colors/edges into mtl_relief_colors/mtl_draw_relief.
Also give mouse-face runs box edges at their boundaries and stop
reading past first_glyph for image/composition strings in the box
code, both mirroring NS.
NS repaints uncovered regions through its drawRect: expose path; the
Metal overlay has none, so when a multi-line echo message shrank back,
stale pixels (e.g. a wrap-arrow shard in the echo fringe) survived.
Detect minibuffer pixel-height changes in update_begin and clear the
affected bottom strip inside the same update cycle, so the redraw and
the clear land in one present without flashing.
mtl_texture_for_image rasterized the NSImage at its natural size with no
transform, so :rotation came out with the wrong orientation and :scale
ignored the engine's display size. Mirror ns_dumpglyphs_image: rasterize
at the engine display size (img->width/height), concat the EmacsImage's
NSAffineTransform inside a flipped context (drawing with
respectFlipped:YES, the two flips cancel for the memory layout) and
honor the smoothing flag for interpolation. The texture cache
re-rasterizes when the display size changes.
Image glyph strings now also sample only their slice subrect of the
texture (mtl_draw_image_texture_uv), fixing insert-sliced-image, which
previously squeezed the whole image into every slice.
The NS backend clips every glyph string draw to
get_glyph_string_clip_rect via ns_focus; Metal drew unclipped. The most
visible casualty: a filled-box cursor on a tall row (e.g. a line with an
inline image) painted the cursor background over the full row height
(60px bar) instead of the character cell, because draw_phys_cursor_glyph
relies on the clip rect, narrowed to the physical cursor box, to bound
the DRAW_CURSOR background fill.
Apply the same rect as an MTLScissorRect around each glyph string draw
(applyClipRect:/clearClipRect on the static-texture encoder, mapped to
physical pixels and clamped), in both the in-cycle and immediate paths.
Also bounds overhang bleed into neighboring windows for free.
Verified: cursor on an image row now renders as a character-sized box
with the glyph inverted, identical to NS; baseline AE unchanged (0.068%).
Wrap mtl_draw_fringe_bitmap and mtl_clear_frame in the same
self-contained immediate frame used by draw_glyph_string (begin, draw,
commit with deferred present) so they are not silently dropped when
invoked outside update_begin/end (clear_garbaged_frames runs before the
update cycle; fringe updates can arrive immediately too). Also make the
fringe clear run even when the bitmap is empty (a clear-only call).
Note: a tiny stale shard can still remain in the echo-area fringe corner
after a multi-line message shrinks the minibuffer back. Root cause is
that NS repaints uncovered regions through its expose path (drawRect:
re-renders from the glyph matrices), which the Metal layer does not have
yet; tracked in TODO.org.
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.
COMPOSITE_GLYPH had no branch in mtl_draw_glyph_string, so combining
sequences (e + COMBINING ACUTE, etc.), ligatures and shaped scripts drew
nothing (blank cells). Port ns_draw_composite_glyph_string_foreground to
the Metal atlas: char2b[] holds glyph IDs indexed by the composition /
gstring index and s->font is the composition's font; static compositions
draw each glyph at its table offset, automatic ones walk the LGSTRING
applying XOFF/YOFF/WADJUST adjustments, with sequential runs advanced by
the natural font advance.
Verified: "e+acute a+grave n+tilde o+diaeresis" now renders identically
to NS (previously empty).
The first big redraw with new content (e.g. the first tab switch)
rasterized a burst of new glyphs into the atlas, which showed as a
one-time whole-frame blink; subsequent switches were fluid because the
cache was hot. Pre-rasterize printable ASCII for the frame's default
face in mtl-enable-for-frame so that cost is paid up front.
The wave fell back to a straight line, and the fallback branch was dead
anyway: FACE_UNDERLINE_WAVE is above FACE_UNDERLINE_SINGLE in the enum,
so the >= SINGLE branch swallowed it. Check wave first (like NS) and
draw the ns_draw_underwave zigzag: wave height 3, period 4, triangle
pattern 0,1,2,1 at ybase..ybase+2, one 1px cell per column, phase
anchored to the absolute column so the wave continues seamlessly across
adjacent glyph strings. Verified visually against NS (red and default).
ns_setup_relief_colors derives the relief edges via NSColor
highlightWithLevel:/shadowWithLevel:, which are appearance-dynamic (in
dark mode the system highlight is not a blend toward pure white), so the
manual mtl_shade_color blend did not match what NS renders: the
mode-line top edge came out 211 vs NS's 177. Use the same NSColor APIs,
falling back to the manual shade if they return nil.
Mode-line relief now matches NS exactly; sparse-baseline AE drops from
0.33% to 0.068%.
bearing_y used bh - 1 - raster_oy, which placed every glyph one pixel
lower than the NS backend across the whole frame. The bitmap row at
top-down index r covers CG y in [bh-1-r, bh-r), so the baseline at CG
y = raster_oy sits bh - raster_oy rows below the top edge, without the
extra -1.
Found by cross-correlating wrapped-text captures: shifting Metal text up
one pixel dropped the per-row mean error from ~40 to ~3 gray levels.
This was the dominant share of the residual diff attributed to
antialiasing: AE vs NS drops from 0.88% to 0.33% on the sparse baseline
and from 3.32% to 0.56% on dense text.
Replace the empty stub with a real implementation mirroring
ns_shift_glyphs_for_insert: move the rest of the line right by SHIFT
pixels inside the static texture, bouncing through the scratch texture
(overlapping src/dst) with the same backing-scale mapping and encoder
suspend/reopen as scrollRunFrom:.
Note: like the X backend documents (xterm.c, emacs-devel 2015-05), this
RIF hook is effectively never called on GUI frames, so this is for
completeness/correctness rather than a visible fix.
The NS (CoreGraphics) backend renders on-screen text slightly heavier
(more ink at every coverage level) due to its coverage gamma / stem
darkening, leaving plain linear-coverage Metal text a touch lighter.
Apply coverage = pow(coverage, 0.82) in the glyph fragment shader to lift
coverage uniformly and match NS's weight.
Dense-text ink ratios vs NS moved from 0.88-0.92 to 0.96-0.99, mean gray
247.6 -> 247.25 (NS 246.95), and AE improved 3.40% -> 3.32%; the "Hola
Mundo" baseline AE stays at 0.88% and is visually identical (no
over-bolding).
Match ns_maybe_dumpglyphs_background: fill the background from
s->y + box_line_width with height s->height - 2*box_line_width instead of
the full cell height. For a boxed face (e.g. the selected tab-bar tab,
or a boxed mode-line) this leaves the box edge rows untouched, so the
relief drawn afterwards is not first overwritten with the background and
then redrawn on top. For unboxed faces box_line_width is 0, so it is
identical to the previous full-height fill (AE vs NS unchanged at 0.88%).
The real draw path used Nord theme colors (0x2E3440 bg, 0xECEFF4 fg) as
fallbacks for when the frame or face is null. Those paths are effectively
unreachable, but baking a dark theme into a backend that mirrors the NS
default light theme is wrong if ever hit. Use neutral white bg / black fg
instead. The MtlView test window and the offscreen PNG render keep their
Nord palette (out of the real redisplay path), per TODO F4.
Mouse-face highlight (note_mouse_highlight -> show_mouse_face) draws
outside the update_begin/end cycle. Presenting on every immediate
draw_glyph_string flickered: one mouse move runs clear_mouse_face +
show_mouse_face, several draws, each flashing the intermediate
(un-highlighted) state.
Each immediate draw now commits to the static texture but defers the
on-screen present (endFramePresent:NO sets needsPresent); flush_display
and update_begin present once, showing the whole clear+redraw sequence
in a single composite. This keeps the pipeline clean (no encoder left
open between draws) while removing the flicker.
Verified: hover highlight still applies (gamma 168 vs 179 inactive),
AE vs NS unchanged at 0.88%.
Strike-through used s->ybase - s->height/3, which sits too high and
mis-centers when the row is taller than the string. Match NS: center a
1px line on the first glyph's body (glyph_y + (glyph_height-1)/2). Add
the overline (1px at the top of the string), which was missing.
Verified vs NS: both lines now land at the same place relative to the
glyphs.
The underline was drawn at s->ybase + s->underline_position, but
underline_position/underline_thickness are filled in by each backend
(the NS backend computes them in ns_draw_text_decoration) and were left
at 0 here, so the line landed on the baseline and cut through the bottom
of the glyphs.
Add mtl_underline_metrics, mirroring the NS computation: position from
font->underline_position (or descent/2, or the descent line per the
face), thickness from the font (min 1), clamped to stay inside the cell,
and matching a previous underlined run for continuity. Handle single,
double (second line above), and colored underlines; wave falls back to a
straight line at the same position for now.
Verified vs NS: the underline is now a full-width bar just below the
glyph body instead of through it; AE unchanged at ~0.88%.
note_mouse_highlight -> show_mouse_face draws the mouse-face highlight by
calling draw_glyph_string directly, outside the update_begin/end cycle, so
no Metal render encoder was active and mtl_draw_glyph_string returned early:
the highlight never appeared (the NS backend draws immediately via lockFocus).
Split the drawing body into mtl_draw_glyph_string_impl and make the public
mtl_draw_glyph_string open a self-contained frame (beginFrame uses LOAD, so
the static texture is preserved), draw, then endFrame to present, whenever it
is called with no encoder active. The same path also restores normal text
when the mouse leaves (clear_mouse_face).
Verified by hovering a mouse-face region with cliclick: the region row
background switches to the highlight face (209,210,210) vs (244,244,244) for
a normal row, with +6 draw_glyph_string calls and no-fd == 0; AE vs NS
unchanged at ~0.88%.
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%.
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.
mtl_texture_for_image flipped the CGBitmapContext CTM before drawing the
NSImage, but an upright draw into a bitmap context already places the
visual top of the image in the first memory row, which is what Metal's
texture row 0 (V=0, top of the quad) expects. The extra flip was the lone
inversion, so images came out upside down. Drop it.
Verified with a top-red/bottom-blue probe: red now lands on top.
The animator cached cursorMode at init, so mtl-cursor-mode (and
mtl-scroll-effect) only updated the global and a live frame kept its
old mode (the default spring), which is why the cursor still moved with
a transition. Propagate the value to live animators immediately.
In torpedo mode the cursor now snaps straight to the target cell (no
spring), and the trail samples carry their own age so the tail fades on
its own ~0.4s after the cursor stops, combining a spatial taper with a
temporal fade so it reads as a comet rather than a uniform bar.
When the animation layer is enabled, draw the cursor only in the
compositor (skip the static one in mtl_draw_window_cursor) so there is
no double cursor, and feed the animator the real FRAME_CURSOR_COLOR
instead of the hardcoded cyan used for the trail and cursor quads.
mtl_draw_glyph_string ignored face->box, so boxed faces (mode line with its
released-button relief, buttons, etc.) rendered flat. Port
ns_dumpglyphs_box_or_relief: compute the box rect and left/right edge presence
from the glyph string, then draw the four edges. FACE_SIMPLE_BOX uses box_color;
raised/sunken reliefs use highlight/shadow shades of the base (box_color or
background) at level 0.4, matching NSColor highlightWithLevel:/shadowWithLevel:.
Mode line now shows its 3D border; the bottom/right shadow matches NS. The
top/left highlight is a touch brighter than NS (subtle 1px), to refine later.
mtl_draw_vertical_window_border and mtl_draw_window_divider hardcoded 0x4C566A.
Port the NS logic: vertical border uses VERTICAL_BORDER_FACE_ID; window divider
uses WINDOW_DIVIDER_FACE_ID with distinct first/last pixel faces
(WINDOW_DIVIDER_FIRST/LAST_PIXEL_FACE_ID) for wide/tall dividers, falling back
to the frame foreground.
mtl_draw_fringe_bitmap was a stub, so truncation/continuation arrows, the
buffer-boundary and other fringe indicators never appeared. Implement it
mirroring ns_draw_fringe_bitmap: clear the fringe background (and the wider bx
area) unless overlay, then rasterize p->bits (MSB-first rows, visible window
[dh, dh+h)) into a one-shot R8 coverage texture and draw it as a colored quad
through the glyph pipeline. Color is face->foreground, or the cursor color when
cursor_p. A fresh texture per call avoids the deferred-sampling hazard of
sharing one texture across queued draws.
Verified: the truncation arrow now matches the NS backend.
mtl_scroll_run was a stub that only nudged the animator, so Emacs's scroll
optimization left stale/duplicated text: scrolling (C-v, recenter) and any
re-layout (window split, *Completions*/*Warnings* popups) painted new rows over
the old ones because the moved block was never relocated.
Now mtl_scroll_run mirrors ns_scroll_run's geometry (window_box +
WINDOW_TO_FRAME_PIXEL_Y, clamped to avoid the mode line) and moves the block
inside the static texture with a GPU blit. Since a single copy can't overlap
its own source/destination, it bounces the region through a scratch texture.
The render encoder is ended and reopened (LOAD) around the blit so subsequent
draw_glyph_string calls land on the moved pixels; Metal's hazard tracking
orders the copy after the draws. Encoder creation is factored into
openRenderEncoderClear:.
Verified with the comparison harness: scrolling and split/relayout no longer
leave ghost text.
- mtl_draw_glyph_string advanced the pen by CoreText's fractional glyph advance
instead of Emacs's layout grid. Across a line the drift accumulated, glyphs
landed at fractional positions (linear-sampling blur) and progressively
overlapped/clipped. Advance by first_glyph[i].pixel_width at integer
positions so Metal stays locked to Emacs's grid and the 1:1 blit is crisp.
- Disable CoreText font smoothing in the glyph rasterization context: the
white-on-black mask was stem-darkened, making text heavier than NS
(mean gray 220 vs 226). Now ~228, matching NS within ~1.7 levels.
- Add g_mtl_animations_enabled (default off): the 60fps compositor overlay
(cyan cursor, particles, CADisplayLink) is now opt-in via (mtl-animations t)
/ mtl-toggle-animations. With it off, compositeToScreen only blits + presents,
removing the stray cyan cursor box that tinted the frame.
- Draw the cursor directly into the static texture like the NS backend
(get_phys_cursor_glyph + get_phys_cursor_geometry, FRAME_CURSOR_COLOR,
filled/hollow/bar/hbar), and handle DRAW_CURSOR in mtl_draw_glyph_string so
the glyph under a filled cursor stays readable (inverse).
- Fix glyph vertical baseline: bearing_y stored the descent instead of the
distance from the cell top to the baseline, dropping every glyph ~one ascent
too low and overlapping the next line / mode line. Store bh-1-raster_oy.
Three critical bugs fixed:
1. MTLLoadActionClear wipes staticTexture every update_begin cycle.
Emacs calls update_begin/end multiple times per second (cursor blink,
modeline, etc.). Each call was clearing all previously drawn content.
Fix: only clear on texture (re)creation; use MTLLoadActionLoad to
PRESERVE content between update_begin cycles. Emacs's draw_glyph_string
fills backgrounds only for the regions being updated.
2. char2b contains CGGlyph IDs (not Unicode codepoints) in the macfont
backend. mtl_draw_glyph_string was calling mtl_cache_glyph() which
used CTFontGetGlyphsForCharacters (Unicode→glyph) on values that were
already glyph IDs — double conversion, wrong glyphs rendered.
Fix: new mtl_cache_glyph_id() uses CTFontDrawGlyphs directly with the
glyph ID from char2b, same as ns_draw_glyph_string_foreground does.
3. mtl_patch_terminal_rif replaced the ENTIRE terminal rif (including
NULL frame_parm_handlers). init_frame_faces/realize_basic_faces accesses
rif->frame_parm_handlers during Fx_create_frame → SIGSEGV.
Fix: copy the NS rif (preserving frame_parm_handlers and all management
functions), then override ONLY the pixel-drawing functions with Metal.
Added: mtl-capture-frame, mtl-draw-stats diagnostics (kept for debugging).