Metal: make display sync configurable (mtl-vsync)

With vsync on (default) presents wait for the panel refresh: redisplay
self-caps at the display rate, which is also a power win (the stock
backend happily renders hundreds of invisible frames per second under
sustained scroll).  Disabling it trades that cap for NS-like latency on
machine-paced workloads.
This commit is contained in:
Andros Fenollosa 2026-06-04 15:24:23 +02:00
parent 136f319cb6
commit 63cd2455a8
3 changed files with 29 additions and 0 deletions

View file

@ -606,6 +606,27 @@ frame. Returns t while animations are enabled, nil otherwise. */)
return Qt;
}
DEFUN ("mtl-vsync", Fmtl_vsync, Smtl_vsync, 1, 2, 0,
doc: /* Enable (non-nil) or disable display sync for FRAME's GPU layer.
With vsync on (default) presents wait for the display refresh: redisplay
is capped at the panel rate, which keeps CPU/GPU use minimal. With it
off, presents return immediately (lower latency, uncapped, more power).
FRAME defaults to the selected frame. */)
(Lisp_Object enable, Lisp_Object frame)
{
if (NILP (frame)) frame = Fselected_frame ();
CHECK_LIVE_FRAME (frame);
g_mtl_vsync_enabled = !NILP (enable);
MtlFrameData *fd = mtl_get_frame_data (XFRAME (frame));
if (fd && fd.metalLayer)
{
block_input ();
fd.metalLayer.displaySyncEnabled = g_mtl_vsync_enabled;
unblock_input ();
}
return enable;
}
void
syms_of_mtlfns (void)
{
@ -632,6 +653,7 @@ syms_of_mtlfns (void)
defsubr (&Smtl_video_move);
defsubr (&Smtl_video_tick);
defsubr (&Smtl_anim_tick);
defsubr (&Smtl_vsync);
}
#endif /* HAVE_MTL */

View file

@ -293,6 +293,7 @@ extern MtlScrollEasing g_mtl_scroll_easing;
extern float g_mtl_scroll_duration; /* seconds, default 0.15 */
extern NSUInteger g_mtl_trail_len; /* default 20 */
extern BOOL g_mtl_animations_enabled; /* default NO (animations are opt-in) */
extern BOOL g_mtl_vsync_enabled; /* default YES (60fps power cap) */
/* -----------------------------------------------------------------------
Global Metal display list

View file

@ -262,6 +262,11 @@ MtlScrollEasing g_mtl_scroll_easing = MTL_EASE_OUT_QUAD;
float g_mtl_scroll_duration = 0.15f;
NSUInteger g_mtl_trail_len = 20;
/* When false, presents do not wait for the display refresh
(CAMetalLayer.displaySyncEnabled): redisplay never blocks on a
drawable, trading the 60 fps power cap for NS-like latency. */
BOOL g_mtl_vsync_enabled = YES;
/* Master switch for the GPU animation layer (cursor effects, particles,
CADisplayLink @60fps). OFF by default: the goal is pixel-correct parity
with the NS backend first. When off, the cursor is drawn directly into the
@ -781,6 +786,7 @@ mtl_setup_frame (struct frame *f)
layer.device = g_device;
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
layer.framebufferOnly = YES;
layer.displaySyncEnabled = g_mtl_vsync_enabled;
layer.frame = view.bounds;
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;