From 27a98a1c36414de5a0176c7e6dbc4f4a5fe16116 Mon Sep 17 00:00:00 2001 From: Andros Fenollosa Date: Thu, 4 Jun 2026 11:05:21 +0200 Subject: [PATCH] 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. --- lisp/mtl.el | 21 ++++++++++++++++++++- src/mtlfns.m | 26 ++++++++++++++++++++++++++ src/mtlterm.h | 4 ++++ src/mtlterm.m | 21 +++++++++++++++++++-- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/lisp/mtl.el b/lisp/mtl.el index 9b4ccd2c3ff..290ddca599b 100644 --- a/lisp/mtl.el +++ b/lisp/mtl.el @@ -146,7 +146,8 @@ cursor effects, particles and the 60fps compositor." :set (lambda (sym val) (set-default sym val) (when (fboundp 'mtl-animations) - (mtl-animations val))) + (mtl-animations val) + (when val (mtl--anim-pump-start)))) :group 'mtl) (defcustom mtl-enable-on-startup nil @@ -179,13 +180,31 @@ The NS backend still handles events, menus, and scrollbars." (mtl-scroll-duration mtl-scroll-duration) (mtl-trail-length mtl-trail-length) (mtl-animations mtl-animations-enabled) + (when mtl-animations-enabled (mtl--anim-pump-start)) (message "Metal GPU enabled on frame: %s (device: %s, animations: %s)" f (mtl-device-name) (if mtl-animations-enabled "on" "off")))) +(defvar mtl--anim-timer nil + "30fps timer driving cursor animations while they are enabled.") + +(defun mtl--anim-pump () + "Advance GPU cursor animations; cancel the timer when they turn off." + (unless (and (fboundp 'mtl-anim-tick) (mtl-anim-tick)) + (when (timerp mtl--anim-timer) (cancel-timer mtl--anim-timer)) + (setq mtl--anim-timer nil))) + +(defun mtl--anim-pump-start () + "Start the animation pump timer (idempotent). +Emacs's event loop starves the CADisplayLink while idle, so without +this Lisp timer the cursor effects only animate during user input." + (unless (timerp mtl--anim-timer) + (setq mtl--anim-timer (run-at-time 0 0.033 #'mtl--anim-pump)))) + (defun mtl-toggle-animations () "Toggle the Metal GPU animation layer on or off." (interactive) (setopt mtl-animations-enabled (not mtl-animations-enabled)) + (when mtl-animations-enabled (mtl--anim-pump-start)) (message "Metal animations %s" (if mtl-animations-enabled "enabled" "disabled"))) (defun mtl-status () diff --git a/src/mtlfns.m b/src/mtlfns.m index 25d22802c0f..8e0305997e6 100644 --- a/src/mtlfns.m +++ b/src/mtlfns.m @@ -581,6 +581,31 @@ otherwise (letting the timer cancel itself). */) return ok ? Qt : Qnil; } +DEFUN ("mtl-anim-tick", Fmtl_anim_tick, Smtl_anim_tick, 0, 2, 0, + doc: /* Advance the GPU cursor animations one step and present. +DT is the step in seconds (default 0.033). Driven by a Lisp timer while +animations are enabled: Emacs's event loop starves the CADisplayLink +when idle, so rings/trails would freeze between input events otherwise +(same mechanism as `mtl-video-tick'). FRAME defaults to the selected +frame. Returns t while animations are enabled, nil otherwise. */) + (Lisp_Object dt, Lisp_Object frame) +{ + if (NILP (frame)) frame = Fselected_frame (); + if (!FRAME_LIVE_P (XFRAME (frame))) return Qnil; + if (!g_mtl_animations_enabled) return Qnil; + MtlFrameData *fd = mtl_get_frame_data (XFRAME (frame)); + if (!fd || !fd.animator) return Qnil; + + float step = 0.033f; + if (NUMBERP (dt)) + step = (float) XFLOATINT (dt); + block_input (); + if (!fd.encoder) + [fd.animator tickWithDt:step]; + unblock_input (); + return Qt; +} + void syms_of_mtlfns (void) { @@ -606,6 +631,7 @@ syms_of_mtlfns (void) defsubr (&Smtl_video_pause); defsubr (&Smtl_video_move); defsubr (&Smtl_video_tick); + defsubr (&Smtl_anim_tick); } #endif /* HAVE_MTL */ diff --git a/src/mtlterm.h b/src/mtlterm.h index 740b0eb0cca..ba1c1e1be09 100644 --- a/src/mtlterm.h +++ b/src/mtlterm.h @@ -148,6 +148,10 @@ typedef struct mtl_spring { /* Called every animation tick (CADisplayLink target) */ - (void)animationTick:(CADisplayLink *)link; +/* One animation step + composite, drivable from a Lisp timer (the display + link starves while Emacs idles). */ +- (void)tickWithDt:(float)dt; + /* Spawn particles at cursor for pixiedust/sonicboom/ripple modes */ - (void)spawnParticlesAtX:(float)x y:(float)y; diff --git a/src/mtlterm.m b/src/mtlterm.m index f6b37b1009d..83a1e769449 100644 --- a/src/mtlterm.m +++ b/src/mtlterm.m @@ -972,13 +972,24 @@ mtl_log_seq_p (void) p->vy = sinf(angle) * speed; p->age = 0.0f; p->size = (self.cursorMode == MTL_CURSOR_SONICBOOM) ? 6.0f : 3.0f; - p->color = 0x88C0D0; /* Nord frost */ + /* Real cursor color: a hardcoded pale tint was invisible on light + backgrounds. */ + p->color = self.cursorColor ? self.cursorColor : 0x88C0D0; } } - (void)animationTick:(CADisplayLink *)link { - float dt = (float)link.duration; + [self tickWithDt:(float) link.duration]; +} + +/* One animation step + composite. Factored out of the CADisplayLink + callback so a Lisp-level timer can drive it too: Emacs's event loop + starves the display link while idle (it stops firing after a couple of + ticks), so timer-driven cursor movements would spawn rings/trails that + never animate. Same medicine as video playback (mtl-video-tick). */ +- (void)tickWithDt:(float)dt +{ MtlFrameData *fd = mtl_get_frame_data (self.emacsFrame); if (!fd || !fd.metalLayer) return; @@ -2528,6 +2539,12 @@ mtl_drv_note_cursor (struct frame *f, int x, int y, int w, int h, return false; fd.animator.cursorColor = color; [fd.animator setCursorX:x y:y width:w height:h]; + /* Cursor-only motion takes redisplay's fast path: no render cycle gets + opened, so nothing would present the moved overlay (the display link + does not fire while Emacs idles). Composite now so the cursor is + never left painted at its old position. */ + if (!fd.encoder) + [fd compositeToScreen]; return true; }