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).
This commit is contained in:
parent
b81683d822
commit
e19ee5fb2b
5 changed files with 522 additions and 20 deletions
|
|
@ -2934,7 +2934,9 @@ if test "${with_mtl}" = yes; then
|
|||
AC_LANG_POP([Objective C])
|
||||
if test "${HAVE_MTL}" = yes; then
|
||||
MTL_OBJC_OBJ="mtlterm.o mtlfns.o"
|
||||
MTL_LIBS="-framework Metal -framework QuartzCore -framework CoreText"
|
||||
dnl AVFoundation/CoreMedia/CoreVideo: inline video (AVPlayerItemVideoOutput
|
||||
dnl decoded straight into Metal textures via CVMetalTextureCache).
|
||||
MTL_LIBS="-framework Metal -framework QuartzCore -framework CoreText -framework AVFoundation -framework CoreMedia -framework CoreVideo"
|
||||
dnl Metal piggybacks on NS infrastructure; keep window_system=nextstep
|
||||
dnl so that term_header=nsterm.h and all NS macros remain available.
|
||||
fi
|
||||
|
|
@ -7492,7 +7494,10 @@ case "$opsys" in
|
|||
fi
|
||||
fi
|
||||
if test "$HAVE_MTL" = "yes"; then
|
||||
libs_nsgui="$libs_nsgui -framework Metal -framework CoreText"
|
||||
dnl AVFoundation/CoreMedia/CoreVideo: inline video (Fase H2).
|
||||
libs_nsgui="$libs_nsgui -framework Metal -framework CoreText \
|
||||
-framework AVFoundation -framework CoreMedia \
|
||||
-framework CoreVideo"
|
||||
fi
|
||||
else
|
||||
libs_nsgui=
|
||||
|
|
|
|||
62
lisp/mtl.el
62
lisp/mtl.el
|
|
@ -64,7 +64,7 @@
|
|||
;; Helper functions (must be defined before defcustom :set functions use them)
|
||||
|
||||
(defun mtl--cursor-mode-number (mode)
|
||||
"Convert cursor MODE symbol to integer for mtl-cursor-mode."
|
||||
"Convert cursor MODE symbol to integer for `mtl-cursor-mode'."
|
||||
(pcase mode
|
||||
('block 0)
|
||||
('spring 1)
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
(_ 1)))
|
||||
|
||||
(defun mtl--scroll-easing-number (easing)
|
||||
"Convert EASING symbol to integer for mtl-scroll-effect."
|
||||
"Convert EASING symbol to integer for `mtl-scroll-effect'."
|
||||
(pcase easing
|
||||
('none 0)
|
||||
('linear 1)
|
||||
|
|
@ -171,7 +171,7 @@ The NS backend still handles events, menus, and scrollbars."
|
|||
(error "mtl-enable: Metal is not available on this system"))
|
||||
(let ((f (or frame (selected-frame))))
|
||||
(unless (framep f)
|
||||
(error "mtl-enable: argument is not a frame"))
|
||||
(error "Mtl-enable: argument is not a frame"))
|
||||
(mtl-enable-for-frame f)
|
||||
;; Apply current configuration
|
||||
(mtl-cursor-mode (mtl--cursor-mode-number mtl-cursor-animation))
|
||||
|
|
@ -223,6 +223,62 @@ The NS backend still handles events, menus, and scrollbars."
|
|||
nil t))))
|
||||
(setopt mtl-scroll-easing easing))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Inline video (Fase H2)
|
||||
|
||||
(defvar mtl--video-state nil
|
||||
"Active inline video: (MARKER WIDTH HEIGHT TIMER FRAME), or nil.")
|
||||
|
||||
(defun mtl--video-sync ()
|
||||
"Track the video placeholder: move/clip the GPU rect and present a frame.
|
||||
Runs on a 30fps timer started by `mtl-video-insert'. Follows scrolling
|
||||
and window changes; hides the video while its position is off-screen."
|
||||
(when mtl--video-state
|
||||
(pcase-let ((`(,marker ,w ,h ,_timer ,frame) mtl--video-state))
|
||||
(if (not (and (frame-live-p frame) (marker-buffer marker)))
|
||||
(mtl-video-stop)
|
||||
(let* ((win (get-buffer-window (marker-buffer marker) frame))
|
||||
(vis (and win (pos-visible-in-window-p marker win t))))
|
||||
(if (not (and vis (listp vis)))
|
||||
;; Not visible: park the rect off-screen but keep decoding.
|
||||
(mtl-video-move 0 -32768 w h nil frame)
|
||||
(let* ((edges (window-inside-pixel-edges win))
|
||||
(x (+ (nth 0 edges) (nth 0 vis)))
|
||||
(y (+ (nth 1 edges) (nth 1 vis))))
|
||||
(mtl-video-move x y w h edges frame)))
|
||||
(mtl-video-tick frame))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun mtl-video-insert (file width height &optional loop)
|
||||
"Insert a WIDTH x HEIGHT placeholder at point and play video FILE over it.
|
||||
The placeholder is a space with a pixel-sized display spec; the GPU
|
||||
composites the video at its position every frame, following scrolling
|
||||
\(clipped to the window interior). With LOOP non-nil, restart playback
|
||||
at the end. One video per frame; a previous one is replaced."
|
||||
(interactive "fVideo file: \nnWidth (px): \nnHeight (px): ")
|
||||
(mtl-video-stop)
|
||||
(insert (propertize " "
|
||||
'display `(space :width (,width) :height (,height))
|
||||
'mtl-video file))
|
||||
(let ((marker (copy-marker (1- (point)))))
|
||||
;; Park off-screen; the first sync tick positions it for real.
|
||||
(unless (mtl-video-open file 0 -32768 width height loop)
|
||||
(error "mtl-video-open failed for %s" file))
|
||||
(setq mtl--video-state
|
||||
(list marker width height
|
||||
(run-at-time 0 0.033 #'mtl--video-sync)
|
||||
(selected-frame)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun mtl-video-stop ()
|
||||
"Stop and remove the inline video, canceling its sync timer."
|
||||
(interactive)
|
||||
(when mtl--video-state
|
||||
(pcase-let ((`(,_marker ,_w ,_h ,timer ,frame) mtl--video-state))
|
||||
(when (timerp timer) (cancel-timer timer))
|
||||
(when (frame-live-p frame) (mtl-video-close frame)))
|
||||
(setq mtl--video-state nil)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Startup integration
|
||||
|
||||
|
|
|
|||
118
src/mtlfns.m
118
src/mtlfns.m
|
|
@ -22,6 +22,7 @@
|
|||
#include "fontset.h"
|
||||
#include "font.h"
|
||||
#include "character.h"
|
||||
#include "coding.h"
|
||||
#include "macfont.h"
|
||||
|
||||
#include "mtlterm.h"
|
||||
|
|
@ -453,6 +454,117 @@ DEFUN ("mtl-animation-status", Fmtl_animation_status, Smtl_animation_status,
|
|||
Initialization
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Fase H2: inline video
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
DEFUN ("mtl-video-open", Fmtl_video_open, Smtl_video_open, 5, 7, 0,
|
||||
doc: /* Play video FILE over FRAME at X, Y sized WIDTH x HEIGHT pixels.
|
||||
X and Y are frame-relative logical pixels (top-left origin). The video
|
||||
is decoded by AVFoundation straight into Metal textures and composited
|
||||
over the frame content every present; redisplay underneath continues
|
||||
normally. If LOOP is non-nil, restart playback at the end.
|
||||
FRAME defaults to the selected frame. One video per frame: opening a
|
||||
new one replaces the previous. Returns t on success. */)
|
||||
(Lisp_Object file, Lisp_Object x, Lisp_Object y, Lisp_Object width,
|
||||
Lisp_Object height, Lisp_Object loop, Lisp_Object frame)
|
||||
{
|
||||
if (NILP (frame)) frame = Fselected_frame ();
|
||||
CHECK_LIVE_FRAME (frame);
|
||||
struct frame *f = XFRAME (frame);
|
||||
CHECK_STRING (file);
|
||||
CHECK_FIXNUM (x); CHECK_FIXNUM (y);
|
||||
CHECK_FIXNUM (width); CHECK_FIXNUM (height);
|
||||
|
||||
Lisp_Object expanded = Fexpand_file_name (file, Qnil);
|
||||
bool ok;
|
||||
block_input ();
|
||||
ok = mtl_video_open (f, SSDATA (ENCODE_FILE (expanded)),
|
||||
XFIXNUM (x), XFIXNUM (y),
|
||||
XFIXNUM (width), XFIXNUM (height),
|
||||
!NILP (loop));
|
||||
unblock_input ();
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
DEFUN ("mtl-video-close", Fmtl_video_close, Smtl_video_close, 0, 1, 0,
|
||||
doc: /* Stop and remove the inline video on FRAME.
|
||||
FRAME defaults to the selected frame. Returns t if a video was open. */)
|
||||
(Lisp_Object frame)
|
||||
{
|
||||
if (NILP (frame)) frame = Fselected_frame ();
|
||||
CHECK_LIVE_FRAME (frame);
|
||||
bool ok;
|
||||
block_input ();
|
||||
ok = mtl_video_close (XFRAME (frame));
|
||||
unblock_input ();
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
DEFUN ("mtl-video-pause", Fmtl_video_pause, Smtl_video_pause, 1, 2, 0,
|
||||
doc: /* Pause (PAUSED non-nil) or resume the inline video on FRAME.
|
||||
FRAME defaults to the selected frame. Returns t if a video is open. */)
|
||||
(Lisp_Object paused, Lisp_Object frame)
|
||||
{
|
||||
if (NILP (frame)) frame = Fselected_frame ();
|
||||
CHECK_LIVE_FRAME (frame);
|
||||
bool ok;
|
||||
block_input ();
|
||||
ok = mtl_video_set_paused (XFRAME (frame), !NILP (paused));
|
||||
unblock_input ();
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
DEFUN ("mtl-video-move", Fmtl_video_move, Smtl_video_move, 4, 6, 0,
|
||||
doc: /* Move/resize the inline video on FRAME to X, Y, WIDTH, HEIGHT.
|
||||
Frame-relative logical pixels. Optional CLIP is a list (LEFT TOP RIGHT
|
||||
BOTTOM), also frame-relative, that confines the video to a window's
|
||||
interior; nil removes clipping. FRAME defaults to the selected frame.
|
||||
Returns t if a video is open. */)
|
||||
(Lisp_Object x, Lisp_Object y, Lisp_Object width, Lisp_Object height,
|
||||
Lisp_Object clip, Lisp_Object frame)
|
||||
{
|
||||
if (NILP (frame)) frame = Fselected_frame ();
|
||||
CHECK_LIVE_FRAME (frame);
|
||||
CHECK_FIXNUM (x); CHECK_FIXNUM (y);
|
||||
CHECK_FIXNUM (width); CHECK_FIXNUM (height);
|
||||
bool ok;
|
||||
block_input ();
|
||||
ok = mtl_video_set_rect (XFRAME (frame), XFIXNUM (x), XFIXNUM (y),
|
||||
XFIXNUM (width), XFIXNUM (height));
|
||||
if (ok)
|
||||
{
|
||||
if (CONSP (clip))
|
||||
{
|
||||
int cl = XFIXNUM (Fnth (make_fixnum (0), clip));
|
||||
int ct = XFIXNUM (Fnth (make_fixnum (1), clip));
|
||||
int cr = XFIXNUM (Fnth (make_fixnum (2), clip));
|
||||
int cb = XFIXNUM (Fnth (make_fixnum (3), clip));
|
||||
mtl_video_set_clip (XFRAME (frame), cl, ct, cr - cl, cb - ct);
|
||||
}
|
||||
else
|
||||
mtl_video_set_clip (XFRAME (frame), 0, 0, 0, 0);
|
||||
}
|
||||
unblock_input ();
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
DEFUN ("mtl-video-tick", Fmtl_video_tick, Smtl_video_tick, 0, 1, 0,
|
||||
doc: /* Present a fresh frame of the inline video on FRAME.
|
||||
Driven by a Lisp timer in mtl.el (Emacs's event loop starves the
|
||||
CADisplayLink while idle). Returns t while a video is open, nil
|
||||
otherwise (letting the timer cancel itself). */)
|
||||
(Lisp_Object frame)
|
||||
{
|
||||
if (NILP (frame)) frame = Fselected_frame ();
|
||||
if (!FRAME_LIVE_P (XFRAME (frame))) return Qnil;
|
||||
bool ok;
|
||||
block_input ();
|
||||
ok = mtl_video_tick (XFRAME (frame));
|
||||
unblock_input ();
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
void
|
||||
syms_of_mtlfns (void)
|
||||
{
|
||||
|
|
@ -472,6 +584,12 @@ syms_of_mtlfns (void)
|
|||
defsubr (&Smtl_animations);
|
||||
defsubr (&Smtl_capture_frame);
|
||||
defsubr (&Smtl_draw_stats);
|
||||
/* Fase H2: inline video */
|
||||
defsubr (&Smtl_video_open);
|
||||
defsubr (&Smtl_video_close);
|
||||
defsubr (&Smtl_video_pause);
|
||||
defsubr (&Smtl_video_move);
|
||||
defsubr (&Smtl_video_tick);
|
||||
}
|
||||
|
||||
#endif /* HAVE_MTL */
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <CoreText/CoreText.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
|
||||
#include "dispextern.h"
|
||||
#include "frame.h"
|
||||
|
|
@ -140,6 +142,38 @@ typedef struct mtl_spring {
|
|||
|
||||
@end
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
MtlVideoPlayer — inline video playback (Fase H2).
|
||||
AVPlayer decodes; AVPlayerItemVideoOutput hands BGRA pixel buffers that
|
||||
CVMetalTextureCache wraps as Metal textures with zero copies; the
|
||||
compositor draws the current frame as a textured quad over the static
|
||||
texture each present, and the animator's CADisplayLink keeps presents
|
||||
flowing while playback is active.
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
@interface MtlVideoPlayer : NSObject
|
||||
@property (nonatomic, strong) AVPlayer *player;
|
||||
@property (nonatomic, strong) AVPlayerItemVideoOutput *output;
|
||||
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache;
|
||||
/* Keep the CoreVideo wrapper alive while its MTLTexture is in use. */
|
||||
@property (nonatomic, assign) CVMetalTextureRef currentCVTexture;
|
||||
@property (nonatomic, strong) id<MTLTexture> currentTexture;
|
||||
@property (nonatomic, assign) NSRect rect; /* logical px */
|
||||
/* Window-interior clip (logical px); NSZeroRect = no clipping. Keeps a
|
||||
half-scrolled video from bleeding over the mode line. */
|
||||
@property (nonatomic, assign) NSRect clipRect;
|
||||
@property (nonatomic, assign) BOOL loop;
|
||||
/* NSNotificationCenter block token for loop mode (retained; this file is
|
||||
compiled without ARC, so raw ivar assignments would not retain). */
|
||||
@property (nonatomic, strong) id endObserver;
|
||||
|
||||
- (instancetype)initWithURL:(NSURL *)url rect:(NSRect)rect loop:(BOOL)loop;
|
||||
/* Latest decoded frame as a Metal texture (nil before the first frame). */
|
||||
- (id<MTLTexture>)textureForNow;
|
||||
- (BOOL)isPlaying;
|
||||
- (void)shutdown;
|
||||
@end
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
MtlFrameData — per-frame Metal rendering state.
|
||||
Stored as ObjC associated object on EmacsView.
|
||||
|
|
@ -187,6 +221,10 @@ typedef struct mtl_spring {
|
|||
@property (nonatomic, assign) BOOL cycleSawClear;
|
||||
@property (nonatomic, assign) BOOL cycleSawDraw;
|
||||
|
||||
/* Fase H2: active inline video (one per frame for now), drawn by
|
||||
compositeToScreen over the static texture. */
|
||||
@property (nonatomic, strong) MtlVideoPlayer *videoPlayer;
|
||||
|
||||
/* Main Emacs render cycle (renders to staticTexture) */
|
||||
- (void)beginFrame;
|
||||
- (void)endFrame;
|
||||
|
|
@ -279,6 +317,15 @@ MtlGlyphCacheEntry *mtl_cache_glyph (CTFontRef font, uint32_t codepoint);
|
|||
/* Pre-rasterize printable ASCII for FRAME's default face (atlas warm-up). */
|
||||
extern void mtl_warm_glyph_cache (struct frame *f);
|
||||
|
||||
/* Fase H2: inline video (one player per frame). */
|
||||
extern bool mtl_video_open (struct frame *f, const char *path,
|
||||
int x, int y, int w, int h, bool loop);
|
||||
extern bool mtl_video_close (struct frame *f);
|
||||
extern bool mtl_video_set_paused (struct frame *f, bool paused);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
|
|
|||
306
src/mtlterm.m
306
src/mtlterm.m
|
|
@ -850,6 +850,20 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
}
|
||||
}
|
||||
|
||||
/* Sequence tracing for present-flow debugging (MTL_LOG_SEQ=1). */
|
||||
static BOOL
|
||||
mtl_log_seq_p (void)
|
||||
{
|
||||
static int on = -1;
|
||||
if (on < 0) on = getenv ("MTL_LOG_SEQ") != NULL;
|
||||
return on > 0;
|
||||
}
|
||||
|
||||
#define MTL_SEQ(fmt, ...) \
|
||||
do { if (mtl_log_seq_p ()) \
|
||||
fprintf (stderr, "[mtlseq %.3f] " fmt "\n", \
|
||||
CACurrentMediaTime (), ##__VA_ARGS__); } while (0)
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
@implementation MtlAnimator
|
||||
----------------------------------------------------------------------- */
|
||||
|
|
@ -1025,6 +1039,11 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
needsComposite = YES;
|
||||
}
|
||||
|
||||
/* Fase H2: while a video plays, every tick presents so the compositor
|
||||
samples the freshest decoded frame. */
|
||||
if (fd.videoPlayer && [fd.videoPlayer isPlaying])
|
||||
needsComposite = YES;
|
||||
|
||||
if (needsComposite || self.cursorDirty)
|
||||
{
|
||||
self.cursorDirty = NO;
|
||||
|
|
@ -1034,6 +1053,124 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
|
||||
@end
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
@implementation MtlVideoPlayer (Fase H2: inline video)
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
@implementation MtlVideoPlayer
|
||||
|
||||
/* NOTE: this file is compiled without ARC; always go through the property
|
||||
setters (retain semantics), never raw ivar assignment, or the AVPlayer
|
||||
graph gets autoreleased under us. */
|
||||
- (instancetype)initWithURL:(NSURL *)url rect:(NSRect)rect loop:(BOOL)loop
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
|
||||
NSDictionary *attrs = @{
|
||||
(id) kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
|
||||
(id) kCVPixelBufferMetalCompatibilityKey : @YES,
|
||||
};
|
||||
self.output = [[[AVPlayerItemVideoOutput alloc]
|
||||
initWithPixelBufferAttributes:attrs] autorelease];
|
||||
[item addOutput:self.output];
|
||||
|
||||
self.player = [AVPlayer playerWithPlayerItem:item];
|
||||
self.player.actionAtItemEnd = loop ? AVPlayerActionAtItemEndNone
|
||||
: AVPlayerActionAtItemEndPause;
|
||||
if (loop)
|
||||
self.endObserver = [[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
|
||||
object:item
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
(void) note;
|
||||
[item seekToTime:kCMTimeZero completionHandler:nil];
|
||||
}];
|
||||
|
||||
CVMetalTextureCacheRef cache = NULL;
|
||||
CVMetalTextureCacheCreate (NULL, NULL, g_device, NULL, &cache);
|
||||
self.textureCache = cache;
|
||||
self.rect = rect;
|
||||
self.clipRect = NSZeroRect;
|
||||
self.loop = loop;
|
||||
[self.player play];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isPlaying
|
||||
{
|
||||
return self.player != nil && self.player.rate != 0.0f;
|
||||
}
|
||||
|
||||
/* Wrap the newest decoded pixel buffer as a Metal texture (zero copy via
|
||||
CVMetalTextureCache). Falls back to the previous frame's texture when the
|
||||
output has nothing new, so redraws between video frames keep the picture. */
|
||||
- (id<MTLTexture>)textureForNow
|
||||
{
|
||||
if (!self.output || !self.textureCache) return self.currentTexture;
|
||||
|
||||
CMTime t = [self.output itemTimeForHostTime:CACurrentMediaTime ()];
|
||||
if ([self.output hasNewPixelBufferForItemTime:t])
|
||||
{
|
||||
CVPixelBufferRef pb = [self.output copyPixelBufferForItemTime:t
|
||||
itemTimeForDisplay:NULL];
|
||||
if (pb)
|
||||
{
|
||||
size_t w = CVPixelBufferGetWidth (pb);
|
||||
size_t h = CVPixelBufferGetHeight (pb);
|
||||
CVMetalTextureRef cvtex = NULL;
|
||||
if (CVMetalTextureCacheCreateTextureFromImage (
|
||||
NULL, self.textureCache, pb, NULL,
|
||||
MTLPixelFormatBGRA8Unorm, w, h, 0, &cvtex)
|
||||
== kCVReturnSuccess && cvtex)
|
||||
{
|
||||
/* The MTLTexture is only valid while its CV wrapper lives;
|
||||
release the previous wrapper now that it is replaced. */
|
||||
if (self.currentCVTexture)
|
||||
CFRelease (self.currentCVTexture);
|
||||
self.currentCVTexture = cvtex;
|
||||
self.currentTexture = CVMetalTextureGetTexture (cvtex);
|
||||
}
|
||||
CVPixelBufferRelease (pb);
|
||||
}
|
||||
}
|
||||
return self.currentTexture;
|
||||
}
|
||||
|
||||
- (void)shutdown
|
||||
{
|
||||
[self.player pause];
|
||||
if (self.endObserver)
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.endObserver];
|
||||
self.endObserver = nil;
|
||||
}
|
||||
self.player = nil;
|
||||
self.output = nil;
|
||||
self.currentTexture = nil;
|
||||
if (self.currentCVTexture)
|
||||
{
|
||||
CFRelease (self.currentCVTexture);
|
||||
self.currentCVTexture = NULL;
|
||||
}
|
||||
if (self.textureCache)
|
||||
{
|
||||
CVMetalTextureCacheFlush (self.textureCache, 0);
|
||||
CFRelease (self.textureCache);
|
||||
self.textureCache = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self shutdown];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
@implementation MtlFrameData
|
||||
----------------------------------------------------------------------- */
|
||||
|
|
@ -1400,20 +1537,6 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
self.needsPresent = YES;
|
||||
}
|
||||
|
||||
/* Sequence tracing for present-flow debugging (MTL_LOG_SEQ=1). */
|
||||
static BOOL
|
||||
mtl_log_seq_p (void)
|
||||
{
|
||||
static int on = -1;
|
||||
if (on < 0) on = getenv ("MTL_LOG_SEQ") != NULL;
|
||||
return on > 0;
|
||||
}
|
||||
|
||||
#define MTL_SEQ(fmt, ...) \
|
||||
do { if (mtl_log_seq_p ()) \
|
||||
fprintf (stderr, "[mtlseq %.3f] " fmt "\n", \
|
||||
CACurrentMediaTime (), ##__VA_ARGS__); } while (0)
|
||||
|
||||
- (void)compositeToScreen
|
||||
{
|
||||
if (!self.staticTexture || !g_blit_pipeline) return;
|
||||
|
|
@ -1421,7 +1544,12 @@ mtl_log_seq_p (void)
|
|||
id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
|
||||
if (!drawable) return;
|
||||
|
||||
MTL_SEQ ("PRESENT");
|
||||
MTL_SEQ ("PRESENT layer=%.0fx%.0f drawable=%lux%lu static=%lux%lu",
|
||||
self.metalLayer.frame.size.width, self.metalLayer.frame.size.height,
|
||||
(unsigned long) drawable.texture.width,
|
||||
(unsigned long) drawable.texture.height,
|
||||
(unsigned long) self.staticTexture.width,
|
||||
(unsigned long) self.staticTexture.height);
|
||||
|
||||
self.needsPresent = NO; /* about to present whatever is in the static texture */
|
||||
|
||||
|
|
@ -1458,6 +1586,65 @@ mtl_log_seq_p (void)
|
|||
[enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
|
||||
}
|
||||
|
||||
/* Fase H2: inline video overlay. Drawn over the static texture so the
|
||||
redisplay engine can keep treating the placeholder area as ordinary
|
||||
buffer background. */
|
||||
MtlVideoPlayer *vp = self.videoPlayer;
|
||||
if (vp)
|
||||
{
|
||||
id<MTLTexture> vtex = [vp textureForNow];
|
||||
if (vtex && g_image_pipeline)
|
||||
{
|
||||
/* Clip to the window interior so a half-scrolled video does not
|
||||
bleed over the mode line or a neighboring window. */
|
||||
NSRect cr = vp.clipRect;
|
||||
BOOL clipped = !NSIsEmptyRect (cr);
|
||||
if (clipped)
|
||||
{
|
||||
CGSize dsz = self.metalLayer.drawableSize;
|
||||
double scx = sz.width > 0 ? dsz.width / sz.width : 1.0;
|
||||
double scy = sz.height > 0 ? dsz.height / sz.height : 1.0;
|
||||
long tw = (long) drawable.texture.width;
|
||||
long th = (long) drawable.texture.height;
|
||||
long cx0 = lround (NSMinX (cr) * scx);
|
||||
long cy0 = lround (NSMinY (cr) * scy);
|
||||
long cx1 = lround (NSMaxX (cr) * scx);
|
||||
long cy1 = lround (NSMaxY (cr) * scy);
|
||||
cx0 = MAX (0, MIN (cx0, tw)); cy0 = MAX (0, MIN (cy0, th));
|
||||
cx1 = MAX (cx0, MIN (cx1, tw)); cy1 = MAX (cy0, MIN (cy1, th));
|
||||
MTLScissorRect sc = { (NSUInteger) cx0, (NSUInteger) cy0,
|
||||
(NSUInteger) (cx1 - cx0),
|
||||
(NSUInteger) (cy1 - cy0) };
|
||||
if (sc.width == 0 || sc.height == 0)
|
||||
sc = (MTLScissorRect) {0, 0, 1, 1};
|
||||
[enc setScissorRect:sc];
|
||||
}
|
||||
|
||||
typedef struct { float x, y, u, v, a; } ImgVert;
|
||||
NSRect vr = vp.rect;
|
||||
float x0 = NSMinX (vr), y0 = NSMinY (vr);
|
||||
float x1 = NSMaxX (vr), y1 = NSMaxY (vr);
|
||||
ImgVert verts[6] = {
|
||||
{x0,y0, 0,0,1}, {x1,y0, 1,0,1}, {x0,y1, 0,1,1},
|
||||
{x1,y0, 1,0,1}, {x1,y1, 1,1,1}, {x0,y1, 0,1,1},
|
||||
};
|
||||
[enc setRenderPipelineState:g_image_pipeline];
|
||||
[enc setVertexBytes:verts length:sizeof (verts) atIndex:0];
|
||||
[enc setVertexBuffer:self.uniformBuffer offset:0 atIndex:1];
|
||||
[enc setFragmentTexture:vtex atIndex:0];
|
||||
[enc setFragmentSamplerState:g_sampler atIndex:0];
|
||||
[enc drawPrimitives:MTLPrimitiveTypeTriangle
|
||||
vertexStart:0 vertexCount:6];
|
||||
|
||||
if (clipped)
|
||||
{
|
||||
MTLScissorRect full = { 0, 0, drawable.texture.width,
|
||||
drawable.texture.height };
|
||||
[enc setScissorRect:full];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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
|
||||
|
|
@ -2857,6 +3044,95 @@ mtl_flush_display (struct frame *f)
|
|||
[fd compositeToScreen];
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Fase H2: inline video API (called from mtlfns.m).
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
bool
|
||||
mtl_video_open (struct frame *f, const char *path, int x, int y,
|
||||
int w, int h, bool loop)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd) return false;
|
||||
|
||||
if (fd.videoPlayer)
|
||||
{
|
||||
[fd.videoPlayer shutdown];
|
||||
fd.videoPlayer = nil;
|
||||
}
|
||||
|
||||
NSString *ns_path = [NSString stringWithUTF8String:path];
|
||||
if (!ns_path || ![[NSFileManager defaultManager] fileExistsAtPath:ns_path])
|
||||
return false;
|
||||
|
||||
MtlVideoPlayer *vp =
|
||||
[[MtlVideoPlayer alloc] initWithURL:[NSURL fileURLWithPath:ns_path]
|
||||
rect:NSMakeRect (x, y, w, h)
|
||||
loop:loop];
|
||||
if (!vp) return false;
|
||||
fd.videoPlayer = vp;
|
||||
|
||||
/* The animator's CADisplayLink drives presents during playback. */
|
||||
[fd.animator startAnimating];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
mtl_video_close (struct frame *f)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.videoPlayer) return false;
|
||||
[fd.videoPlayer shutdown];
|
||||
fd.videoPlayer = nil;
|
||||
if (!g_mtl_animations_enabled)
|
||||
[fd.animator stopAnimating];
|
||||
[fd compositeToScreen]; /* repaint without the overlay */
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
mtl_video_set_paused (struct frame *f, bool paused)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.videoPlayer) return false;
|
||||
if (paused)
|
||||
[fd.videoPlayer.player pause];
|
||||
else
|
||||
[fd.videoPlayer.player play];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
mtl_video_set_rect (struct frame *f, int x, int y, int w, int h)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.videoPlayer) return false;
|
||||
fd.videoPlayer.rect = NSMakeRect (x, y, w, h);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
mtl_video_set_clip (struct frame *f, int x, int y, int w, int h)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.videoPlayer) return false;
|
||||
fd.videoPlayer.clipRect = NSMakeRect (x, y, w, h);
|
||||
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. */
|
||||
bool
|
||||
mtl_video_tick (struct frame *f)
|
||||
{
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.videoPlayer) return false;
|
||||
if ([fd.videoPlayer isPlaying] && !fd.encoder)
|
||||
[fd compositeToScreen];
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
mtl_update_begin (struct frame *f)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue