Commit graph

182482 commits

Author SHA1 Message Date
136f319cb6 README: explain what the GPU backend enables 2026-06-04 15:16:13 +02:00
2c41751831 Metal: render non-GPU frames through the native backend, plus gap fixes
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.
2026-06-04 15:05:59 +02:00
00f3bc2f3f README: re-record the sonicboom cursor demo
The previous capture was static: cursor-only motion never presented
while Emacs idled (fixed in the previous commit).
2026-06-04 11:22:35 +02:00
27a98a1c36 Metal: keep cursor animations alive while Emacs idles
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.
2026-06-04 11:05:21 +02:00
32b41ab8be README: add GPU demos (inline video, animated GIF, cursor effects)
Un-ignore .github (Emacs's .gitignore drops all dot-files) so the demo
assets can live there.
2026-06-04 09:50:51 +02:00
2afadfabfa README: note that issues will not be answered for now 2026-06-04 09:19:33 +02:00
5c8e6cd468 Add README for the emacs-gpu repository 2026-06-04 08:56:27 +02:00
d67ea32afe Clean up sources for publication
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.
2026-06-04 08:54:03 +02:00
9ce5065d7a Metal: default cursor animation is now sonicboom
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).
2026-06-04 08:54:03 +02:00
ef4beaa362 Fase I: split the GPU backend into neutral policy + driver vtable
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.
2026-06-04 08:54:03 +02:00
e19ee5fb2b Metal: inline video playback (Fase H2)
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).
2026-06-04 08:54:03 +02:00
b81683d822 Metal: defer presenting clear-only update cycles (first tab-switch flash)
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.
2026-06-04 08:54:03 +02:00
b526ebab2c Metal: draw image relief and face box for image glyph strings
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.
2026-06-04 08:54:03 +02:00
52e46fa7a8 Metal: clear bottom strip when minibuffer height changes (expose substitute)
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.
2026-06-04 08:54:03 +02:00
9c68084ac3 Metal: apply image transforms and slice UVs like the NS backend
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.
2026-06-04 08:54:03 +02:00
6c7de3c114 Metal: clip glyph strings with the engine clip rect (scissor)
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%).
2026-06-04 08:54:03 +02:00
6663cfcd5f Metal: harden fringe and clear_frame against cycle-less calls
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.
2026-06-04 08:54:03 +02:00
333c1a582e 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.
2026-06-04 08:54:03 +02:00
72a246267f Metal: draw composition glyph strings (combining accents, ligatures)
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).
2026-06-04 08:54:03 +02:00
0b23c907dd Metal: warm the glyph atlas with ASCII at enable time
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.
2026-06-04 08:54:03 +02:00
e139b04f16 Metal: draw the real zigzag wave underline (F2)
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).
2026-06-04 08:54:03 +02:00
84354c2696 Metal: use NSColor highlight/shadow for box relief colors
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%.
2026-06-04 08:54:03 +02:00
1aff29efde Metal: fix one-pixel-low glyph baseline (off-by-one in bearing_y)
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.
2026-06-04 08:54:03 +02:00
6d627b430d Metal: implement shift_glyphs_for_insert as a horizontal blit (E3)
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.
2026-06-04 08:54:03 +02:00
195342091f Metal: mild coverage gamma to match NS glyph weight (F3)
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).
2026-06-04 08:54:03 +02:00
c1f9d62b27 Metal: inset glyph-string background fill by the box line width
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%).
2026-06-04 08:54:03 +02:00
98720f3d1c Metal: neutral light fallbacks instead of baked-in Nord colors (F4)
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.
2026-06-04 08:54:03 +02:00
d91a6dc02b Metal: coalesce immediate-draw presents to stop mouse-face hover flicker
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%.
2026-06-04 08:54:03 +02:00
47aeb064a1 Metal: correct strike-through position and add overline (F2)
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.
2026-06-04 08:54:03 +02:00
4c1798c29a Metal: compute the underline position from font metrics (F2)
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%.
2026-06-04 08:54:03 +02:00
71d289939a Metal: render immediate draws (mouse-face highlight) outside the frame cycle (F1)
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%.
2026-06-04 08:54:03 +02:00
e4ece37534 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%.
2026-06-04 08:54:03 +02:00
d1cd7b2405 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.
2026-06-04 08:54:03 +02:00
df8a190e28 Metal: fix inline images rendering upside down
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.
2026-06-04 08:54:03 +02:00
0e1e6f983d Metal: torpedo cursor is instant with a self-fading comet trail
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.
2026-06-04 08:54:03 +02:00
277b69ddca Metal: animated cursor uses the real frame color, no double cursor
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.
2026-06-04 08:54:03 +02:00
4b29e4bc8f Metal: draw face box / 3D relief (F2)
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.
2026-06-04 08:54:03 +02:00
fd1815b763 Metal: window borders/dividers use real faces (D2)
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.
2026-06-04 08:54:03 +02:00
f2fc640ded Metal: implement fringe bitmaps (D3)
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.
2026-06-04 08:54:03 +02:00
0a0de5cb88 Metal: implement scroll_run (move pixels in the static texture)
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.
2026-06-04 08:54:03 +02:00
db522d9eec Metal: crisp text — advance on Emacs's integer grid, disable font smoothing
- 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.
2026-06-04 08:54:03 +02:00
9c7a7a2128 Metal: gate animations, draw cursor in texture, fix glyph baseline
- 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.
2026-06-04 08:54:03 +02:00
9c112c768d Fix text rendering: MTLLoadActionLoad + glyph ID path + rif copy
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).
2026-06-04 08:54:03 +02:00
ab0972e901 Fix crash: replace NSMapTable with CFMutableDictionaryRef for image cache
Root cause (macOS 26.5):
  NSMapTable with NSMapTableObjectPointerPersonality used the NSValue*
  wrapper POINTER ADDRESS for equality, not the wrapped struct image*
  content. This caused:
  1. Cache always missed (two NSValue objects wrapping same pointer
     are NOT equal by address)
  2. NSValue objects leaked in the map table indefinitely
  3. On macOS 26.5, NSMapTable's internal GCD-backed implementation
     confused leaked NSValue memory regions with OS_dispatch_source
     objects, causing -[OS_dispatch_source objectForKey:] crash in
     mtl_draw_glyph_string on first IMAGE_GLYPH render

Fix: CFMutableDictionaryRef with NULL key callbacks uses raw pointer
equality (struct image* directly as key) with CF retain for values.
No NSValue wrapper, no ObjC runtime quirks.

Also fixed: NSWindowDidChangeScreenNotification observer was created
with addObserverForName:object:queue:usingBlock: but the returned
observer TOKEN was dropped. On macOS 26.5 the token is internally an
OS_dispatch_source; when the autoreleased token was freed, the memory
could be reused by adjacent static variables. Fixed by replacing the
notification with a KVO observer on the 'window' key path using the
existing MtlResizeObserver (stored as associated object, safe lifetime).

CGBitmapInfo fix: use kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
instead of Big-endian RGBA. Metal BGRA8Unorm expects little-endian byte order,
avoiding a separate R<->B swap pass.
2026-06-04 08:54:03 +02:00
1b9b622f14 Add Metal GPU display backend (emacs-gl, Phases 0-6)
New backend --with-mtl that adds Apple Metal GPU rendering on top of
the existing NS (Cocoa) backend on macOS.

Architecture:
- CAMetalLayer sublayer on EmacsView, stored as ObjC associated object
- mtl-enable-for-frame patches terminal->rif, update_begin/end hooks
- Emacs renders to staticTexture; CADisplayLink composites at 60fps

Files added:
- src/mtlterm.h  -- types: MtlFrameData, MtlAnimator, MtlView, MtlWindow
- src/mtlterm.m  -- full implementation (~2300 lines)
- src/mtlfns.m   -- Lisp interface (mtl-enable-for-frame, mtl-cursor-mode, etc.)
- src/mtl_shaders.metal -- MSL shaders (glyph, rect, blit, particle, image)
- lisp/mtl.el    -- user-facing Elisp configuration module

Files modified:
- configure.ac   -- --with-mtl option, Metal/Metal.h detection
- src/Makefile.in -- MTL_OBJ / MTL_OBJC_OBJ variables
- src/emacs.c    -- calls syms_of_mtlfns() on startup

Features implemented:
Phase 1: skeleton (NSWindow + CAMetalLayer, shader compilation)
Phase 2: text rendering (CoreText glyph atlas, R8Unorm, CHAR_GLYPH)
Phase 3: input handling (keyboard/mouse/resize/multi-monitor)
Phase 4: GPU animations at 60fps via CADisplayLink:
         - 8 cursor modes: block/spring/torpedo/sonicboom/ripple/pixiedust/hollow/beam
         - Spring physics (critically-damped, omega=8, ~150ms settle)
         - 5 scroll easing functions (ease-out-quad default)
Phase 5: inline images (NSImage -> MTLTexture, RGBA pipeline, IMAGE_GLYPH)
Phase 6: Elisp config module (mtl-enable, mtl-mode, defcustom)

Usage:
  ./configure --with-ns --with-mtl CC="xcrun clang"
  make
  ;; In Emacs:
  (require 'mtl)
  (mtl-enable)
2026-06-04 08:54:03 +02:00
David Ponce
e6e79b1e09 Don't unconditionally append the :ascent property in wid-edit
* lisp/wid-edit.el (widget-toggle-value-create): Don't
duplicate the :ascent property if already set.  (Bug#81167)
2026-06-04 09:06:52 +03:00
Xiyue Deng
d852d36c77 Decrypt plstore when needed in 'plstore-delete'
When a plstore has entries with secret keys, processing the plstore
file would require decryption first.  However, unlike other functions
like 'plstore-get', 'plstore-put', etc., 'plstore-delete' does not
check for secret keys and decrypt the file, which would corrupt the
file when deleting any entries with secret keys.
This patch adds checking for secret keys and decrypt the file when
needed before removing the entry with name.
* lisp/plstore.el (plstore--has-secret-keys): New.
* lisp/plstore.el (plstore-delete): Check for secret keys of the entry
and decrypt plstore before performing the deletion.  (Bug#81061)
2026-06-04 08:37:25 +03:00
Dmitry Gutov
c9dfe2abe6 Avoid background fills on parent during child frame resize/move/hide
* src/xterm.c (x_suspend_background_fills)
(x_restore_background_fills): New functions.
(x_set_window_size, x_set_window_size_and_position)
(x_make_frame_invisible): Use them (bug#80961).
2026-06-04 06:42:19 +03:00
Pip Cet
08336b9a55 Always return a list from send-to--collect-items
* lisp/send-to.el (send-to--collect-items): Return singleton list, not
a string directly.
2026-06-03 18:15:22 +00:00
Pip Cet
44fa1595ce * lisp/send-to.el (send-to--resolve-handler): Add autoload. 2026-06-03 18:15:22 +00:00