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; }