Metal: buffer-switch crossfades, animations on by default
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).
This commit is contained in:
parent
35b5c6d093
commit
1a7d722f31
5 changed files with 198 additions and 28 deletions
27
README.md
27
README.md
|
|
@ -99,8 +99,9 @@ The binary is `src/emacs` (or install the app bundle from `nextstep/`).
|
|||
|
||||
## Enabling the GPU backend
|
||||
|
||||
Emacs starts with the regular Cocoa backend; switch a frame to Metal
|
||||
with:
|
||||
The release app bundle enables it automatically (set the environment
|
||||
variable `EMACS_GPU_DISABLE=1` to start with the stock Cocoa backend
|
||||
instead). In a source build, switch a frame to Metal with:
|
||||
|
||||
```elisp
|
||||
(add-to-list 'load-path "/path/to/emacs-gpu/lisp")
|
||||
|
|
@ -108,12 +109,26 @@ with:
|
|||
(mtl-enable)
|
||||
```
|
||||
|
||||
Extras once enabled:
|
||||
## Commands and options
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `M-x mtl-status` | Show backend state: GPU device, animations, cursor mode |
|
||||
| `M-: (mtl-draw-stats)` | Renderer counters; `glyphs-drawn` growing proves the GPU is painting |
|
||||
| `M-x mtl-toggle-animations` | Toggle GPU cursor effects (on by default) |
|
||||
| `M-x mtl-set-cursor` | Pick the cursor effect: `sonicboom` (default), `torpedo` (comet trail), `spring`, `ripple`, `pixiedust`, `hollow`, `beam`, `block` |
|
||||
| `M-: (mtl-vsync nil)` | Uncap presents from the display refresh (lower latency, more power) |
|
||||
| `M-: (mtl-video-insert "clip.mp4" 480 270 t)` | Play a video inline at point; follows scrolling |
|
||||
| `M-x mtl-video-stop` | Stop the inline video |
|
||||
|
||||
Cursor effects trigger on cursor jumps (`M-<`, `M->`, isearch hits),
|
||||
not on single-character movement.
|
||||
|
||||
Buffer switches cross-fade by default; tune or disable with:
|
||||
|
||||
```elisp
|
||||
(mtl-video-insert "video.mp4" 480 270 t) ; inline video, follows scrolling
|
||||
(mtl-animations t) ; GPU cursor effects (experimental)
|
||||
(mtl-draw-stats) ; renderer counters
|
||||
(setq mtl-buffer-transition-duration 0.15) ; seconds
|
||||
(setq mtl-buffer-transitions nil) ; turn it off
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
|
|
|||
72
lisp/mtl.el
72
lisp/mtl.el
|
|
@ -87,6 +87,22 @@
|
|||
('ease-in-out-cubic 5)
|
||||
(_ 2)))
|
||||
|
||||
(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))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Customizable variables
|
||||
|
||||
|
|
@ -136,7 +152,7 @@ Lower values are snappier; higher values are more fluid."
|
|||
:set #'set-default
|
||||
:group 'mtl)
|
||||
|
||||
(defcustom mtl-animations-enabled nil
|
||||
(defcustom mtl-animations-enabled t
|
||||
"If non-nil, enable the Metal GPU animation layer.
|
||||
When nil (the default), the cursor is drawn directly into the static
|
||||
texture like the NS backend and no compositor overlay is drawn, which is
|
||||
|
|
@ -181,25 +197,10 @@ The NS backend still handles events, menus, and scrollbars."
|
|||
(mtl-trail-length mtl-trail-length)
|
||||
(mtl-animations mtl-animations-enabled)
|
||||
(when mtl-animations-enabled (mtl--anim-pump-start))
|
||||
(add-hook 'pre-redisplay-functions #'mtl--transition-watch)
|
||||
(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)
|
||||
|
|
@ -242,6 +243,43 @@ this Lisp timer the cursor effects only animate during user input."
|
|||
nil t))))
|
||||
(setopt mtl-scroll-easing easing))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Buffer-switch transitions
|
||||
|
||||
(defcustom mtl-buffer-transitions t
|
||||
"When non-nil, cross-fade the old content when a window changes buffer.
|
||||
The previous frame content fades out over
|
||||
`mtl-buffer-transition-duration' seconds while the new buffer appears
|
||||
underneath. Rendered entirely by the GPU compositor."
|
||||
:type 'boolean
|
||||
:group 'mtl)
|
||||
|
||||
(defcustom mtl-buffer-transition-duration 0.15
|
||||
"Seconds a buffer-switch cross-fade takes."
|
||||
:type 'number
|
||||
:group 'mtl)
|
||||
|
||||
(defvar mtl--transition-armed nil
|
||||
"Non-nil while a snapshot was already taken for the ongoing redisplay.")
|
||||
|
||||
(defun mtl--transition-watch (window)
|
||||
"Start a cross-fade when WINDOW is about to display another buffer.
|
||||
Runs from `pre-redisplay-functions', before the new content is painted,
|
||||
so the GPU snapshot still holds the old pixels."
|
||||
(when (and mtl-buffer-transitions
|
||||
(not (window-minibuffer-p window))
|
||||
(fboundp 'mtl-transition-start))
|
||||
(let ((old (window-parameter window 'mtl--last-buffer))
|
||||
(new (window-buffer window)))
|
||||
(when (and old (not (eq old new)) (not mtl--transition-armed))
|
||||
(setq mtl--transition-armed t)
|
||||
(run-at-time 0 nil (lambda () (setq mtl--transition-armed nil)))
|
||||
(ignore-errors
|
||||
(when (mtl-transition-start (float mtl-buffer-transition-duration)
|
||||
(window-frame window))
|
||||
(mtl--anim-pump-start))))
|
||||
(set-window-parameter window 'mtl--last-buffer new))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Inline video
|
||||
|
||||
|
|
|
|||
23
src/mtlfns.m
23
src/mtlfns.m
|
|
@ -592,9 +592,10 @@ frame. Returns t while animations are enabled, nil otherwise. */)
|
|||
{
|
||||
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;
|
||||
if (!g_mtl_animations_enabled && !fd.transitionTexture && !fd.videoPlayer)
|
||||
return Qnil;
|
||||
|
||||
float step = 0.033f;
|
||||
if (NUMBERP (dt))
|
||||
|
|
@ -627,6 +628,25 @@ FRAME defaults to the selected frame. */)
|
|||
return enable;
|
||||
}
|
||||
|
||||
DEFUN ("mtl-transition-start", Fmtl_transition_start, Smtl_transition_start,
|
||||
1, 2, 0,
|
||||
doc: /* Crossfade the current frame content over the next redraw.
|
||||
Snapshot what FRAME shows now and fade it out over DURATION seconds
|
||||
while the new content appears underneath. Driven by mtl.el's
|
||||
buffer-switch hook; callable directly for custom effects. FRAME
|
||||
defaults to the selected frame. Returns t if the snapshot was taken. */)
|
||||
(Lisp_Object duration, Lisp_Object frame)
|
||||
{
|
||||
if (NILP (frame)) frame = Fselected_frame ();
|
||||
CHECK_LIVE_FRAME (frame);
|
||||
CHECK_NUMBER (duration);
|
||||
bool ok;
|
||||
block_input ();
|
||||
ok = mtl_transition_start (XFRAME (frame), (float) XFLOATINT (duration));
|
||||
unblock_input ();
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
void
|
||||
syms_of_mtlfns (void)
|
||||
{
|
||||
|
|
@ -654,6 +674,7 @@ syms_of_mtlfns (void)
|
|||
defsubr (&Smtl_video_tick);
|
||||
defsubr (&Smtl_anim_tick);
|
||||
defsubr (&Smtl_vsync);
|
||||
defsubr (&Smtl_transition_start);
|
||||
}
|
||||
|
||||
#endif /* HAVE_MTL */
|
||||
|
|
|
|||
|
|
@ -231,6 +231,12 @@ typedef struct mtl_spring {
|
|||
@property (nonatomic, assign) CFTimeInterval lastPresentTime;
|
||||
@property (nonatomic, assign) BOOL presentScheduled;
|
||||
|
||||
/* Buffer-switch transition: a snapshot of the previous content cross-
|
||||
fades over the new one (drawn by the compositor while active). */
|
||||
@property (nonatomic, strong) id<MTLTexture> transitionTexture;
|
||||
@property (nonatomic, assign) CFTimeInterval transitionStart;
|
||||
@property (nonatomic, assign) float transitionDuration;
|
||||
|
||||
/* Coalesced present: present now, unless one landed very recently (then
|
||||
schedule a deferred one). Use for "make the frame visible" paths. */
|
||||
- (void)presentCoalesced;
|
||||
|
|
@ -345,6 +351,10 @@ extern bool mtl_video_set_rect (struct frame *f, int x, int y, int w, int h);
|
|||
extern bool mtl_video_set_clip (struct frame *f, int x, int y, int w, int h);
|
||||
extern bool mtl_video_tick (struct frame *f);
|
||||
|
||||
/* Buffer-switch crossfade: snapshot current content, fade it out over
|
||||
DURATION seconds while the new content shows underneath. */
|
||||
extern bool mtl_transition_start (struct frame *f, float duration);
|
||||
|
||||
extern bool mtl_render_offscreen_png (const char *path, int w, int h,
|
||||
void (^draw)(id<MTLRenderCommandEncoder>));
|
||||
extern bool mtl_render_text_png (const char *path);
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ BOOL g_mtl_vsync_enabled = YES;
|
|||
with the NS backend first. When off, the cursor is drawn directly into the
|
||||
static texture (like NS) and no compositor overlay is drawn. Toggle from
|
||||
Lisp with (mtl-animations t). */
|
||||
BOOL g_mtl_animations_enabled = NO;
|
||||
BOOL g_mtl_animations_enabled = YES;
|
||||
|
||||
/* Phase 4: additional global pipeline state */
|
||||
static id<MTLRenderPipelineState> g_blit_pipeline = nil;
|
||||
|
|
@ -1087,6 +1087,10 @@ mtl_log_seq_p (void)
|
|||
if (fd.videoPlayer && [fd.videoPlayer isPlaying])
|
||||
needsComposite = YES;
|
||||
|
||||
/* Same while a buffer-switch crossfade is in flight. */
|
||||
if (fd.transitionTexture)
|
||||
needsComposite = YES;
|
||||
|
||||
if (needsComposite || self.cursorDirty)
|
||||
{
|
||||
self.cursorDirty = NO;
|
||||
|
|
@ -1748,6 +1752,33 @@ mtl_log_seq_p (void)
|
|||
}
|
||||
}
|
||||
|
||||
/* Buffer-switch crossfade: the old content fades out over the new. */
|
||||
if (self.transitionTexture && self.transitionDuration > 0)
|
||||
{
|
||||
float p = (float) ((CACurrentMediaTime () - self.transitionStart)
|
||||
/ self.transitionDuration);
|
||||
if (p >= 1.0f)
|
||||
self.transitionTexture = nil; /* done */
|
||||
else
|
||||
{
|
||||
float a = 1.0f - p;
|
||||
a = a * a * (3.0f - 2.0f * a); /* smoothstep del fade-out */
|
||||
typedef struct { float x, y, u, v, al; } ImgVert;
|
||||
float x1 = (float) sz.width, y1 = (float) sz.height;
|
||||
ImgVert verts[6] = {
|
||||
{0,0, 0,0,a}, {x1,0, 1,0,a}, {0,y1, 0,1,a},
|
||||
{x1,0, 1,0,a}, {x1,y1, 1,1,a}, {0,y1, 0,1,a},
|
||||
};
|
||||
[enc setRenderPipelineState:g_image_pipeline];
|
||||
[enc setVertexBytes:verts length:sizeof (verts) atIndex:0];
|
||||
[enc setVertexBuffer:self.uniformBuffer offset:0 atIndex:1];
|
||||
[enc setFragmentTexture:self.transitionTexture atIndex:0];
|
||||
[enc setFragmentSamplerState:g_nearest_sampler atIndex:0];
|
||||
[enc drawPrimitives:MTLPrimitiveTypeTriangle
|
||||
vertexStart:0 vertexCount:6];
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation overlay (cursor effects, trail, particles) is opt-in. When off,
|
||||
the cursor lives in the static texture (drawn by mtl_draw_window_cursor),
|
||||
so the compositor only blits and presents. This is what kills the stray
|
||||
|
|
@ -1804,8 +1835,14 @@ mtl_log_seq_p (void)
|
|||
}
|
||||
|
||||
/* 3. Cursor (spring-interpolated position). Hidden during the
|
||||
blink-off phase; effects in flight keep animating. */
|
||||
if (!anim.cursorHidden)
|
||||
blink-off phase; effects in flight keep animating. Only the
|
||||
body-animated modes draw it here: the burst modes rely on the
|
||||
static (inverted glyph) cursor in the texture. */
|
||||
if (!anim.cursorHidden
|
||||
&& (anim.cursorMode == MTL_CURSOR_SPRING
|
||||
|| anim.cursorMode == MTL_CURSOR_TORPEDO
|
||||
|| anim.cursorMode == MTL_CURSOR_HOLLOW
|
||||
|| anim.cursorMode == MTL_CURSOR_BEAM))
|
||||
{
|
||||
float x0=cx, y0=cy, x1=cx+cw, y1=cy+ch;
|
||||
float fr=ccr,fg=ccg,fb=ccb;
|
||||
|
|
@ -2663,7 +2700,14 @@ mtl_drv_note_cursor (struct frame *f, int x, int y, int w, int h,
|
|||
never left painted at its old position. */
|
||||
if (!fd.encoder)
|
||||
[fd compositeToScreen];
|
||||
return true;
|
||||
/* Only the modes that ANIMATE the cursor body draw it in the overlay;
|
||||
for the burst modes (sonicboom/ripple/pixiedust) the policy keeps
|
||||
drawing the proper static cursor (inverted glyph) and the overlay
|
||||
adds just the effects -- a solid overlay body would hide the
|
||||
character under the cursor. */
|
||||
MtlCursorMode mode = fd.animator.cursorMode;
|
||||
return (mode == MTL_CURSOR_SPRING || mode == MTL_CURSOR_TORPEDO
|
||||
|| mode == MTL_CURSOR_HOLLOW || mode == MTL_CURSOR_BEAM);
|
||||
}
|
||||
|
||||
static struct gfx_driver mtl_gfx_driver =
|
||||
|
|
@ -2815,6 +2859,48 @@ mtl_video_set_clip (struct frame *f, int x, int y, int w, int h)
|
|||
return true;
|
||||
}
|
||||
|
||||
/* Buffer-switch crossfade: copy the CURRENT static texture into the
|
||||
transition snapshot and arm the fade. Called from Lisp just before
|
||||
redisplay paints the new buffer (pre-redisplay-functions), so the
|
||||
snapshot still holds the old content. */
|
||||
bool
|
||||
mtl_transition_start (struct frame *f, float duration)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.staticTexture || fd.encoder || duration <= 0)
|
||||
return false;
|
||||
|
||||
id<MTLTexture> src = fd.staticTexture;
|
||||
id<MTLTexture> snap = fd.transitionTexture;
|
||||
if (!snap || snap.width != src.width || snap.height != src.height)
|
||||
{
|
||||
MTLTextureDescriptor *td = [MTLTextureDescriptor
|
||||
texture2DDescriptorWithPixelFormat:src.pixelFormat
|
||||
width:src.width
|
||||
height:src.height
|
||||
mipmapped:NO];
|
||||
td.usage = MTLTextureUsageShaderRead;
|
||||
td.storageMode = MTLStorageModePrivate;
|
||||
snap = [g_device newTextureWithDescriptor:td];
|
||||
fd.transitionTexture = snap;
|
||||
[snap release];
|
||||
}
|
||||
|
||||
id<MTLCommandBuffer> cmd = [g_queue commandBuffer];
|
||||
id<MTLBlitCommandEncoder> blit = [cmd blitCommandEncoder];
|
||||
[blit copyFromTexture:src sourceSlice:0 sourceLevel:0
|
||||
sourceOrigin:MTLOriginMake (0, 0, 0)
|
||||
sourceSize:MTLSizeMake (src.width, src.height, 1)
|
||||
toTexture:snap destinationSlice:0 destinationLevel:0
|
||||
destinationOrigin:MTLOriginMake (0, 0, 0)];
|
||||
[blit endEncoding];
|
||||
[cmd commit];
|
||||
|
||||
fd.transitionStart = CACurrentMediaTime ();
|
||||
fd.transitionDuration = duration;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Present a fresh composite if a video is active. Called from a Lisp-level
|
||||
timer (mtl.el): Emacs's event loop starves the CADisplayLink while idle,
|
||||
so Lisp timers are what reliably drives playback presents. */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue