From 26369672f0ac3dc3f1144d6d766f5e55656fec9c Mon Sep 17 00:00:00 2001
From: Pewnack <[email protected]>
Date: Tue, 14 Apr 2026 18:46:42 +0200
Subject: [PATCH 1/4] Added FramePacer for macOS to prevent stutter in
animations.
---
src/native/apple/frameworks.rs | 59 +++++++++++++++++++
src/native/macos.rs | 102 ++++++++++++++++++++++++++++++++-
2 files changed, 160 insertions(+), 1 deletion(-)
diff --git a/src/native/apple/frameworks.rs b/src/native/apple/frameworks.rs
index d4a1c22c..c5b7985a 100644
--- a/src/native/apple/frameworks.rs
+++ b/src/native/apple/frameworks.rs
@@ -238,6 +238,65 @@ extern "C" {
pub fn MTLCopyAllDevices() -> ObjcId; //TODO: Array
}
+#[cfg(target_os = "macos")]
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct CVTime {
+ pub time_value: i64,
+ pub time_scale: i32,
+ pub flags: i32,
+ pub epoch: i64,
+}
+
+#[cfg(target_os = "macos")]
+pub type CVDisplayLinkRef = *mut c_void;
+
+#[cfg(target_os = "macos")]
+pub type CVOptionFlags = u64;
+
+#[cfg(target_os = "macos")]
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct CVTimeStamp {
+ pub version: u32,
+ pub video_time_scale: i32,
+ pub video_time: i64,
+ pub host_time: u64,
+ pub rate_scalar: f64,
+ pub video_refresh_period: i64,
+ pub smpte_time: i64,
+ pub flags: u64,
+ pub reserved: u64,
+}
+
+#[cfg(target_os = "macos")]
+pub type CVDisplayLinkOutputCallback = extern "C" fn(
+ display_link: CVDisplayLinkRef,
+ now: *const CVTimeStamp,
+ output_time: *const CVTimeStamp,
+ flags_in: CVOptionFlags,
+ flags_out: *mut CVOptionFlags,
+ display_link_context: *mut c_void,
+) -> i32;
+
+#[cfg(target_os = "macos")]
+pub const kCVTimeIsIndefinite: i32 = 1 << 0;
+
+#[cfg(target_os = "macos")]
+#[link(name = "CoreVideo", kind = "framework")]
+extern "C" {
+ pub fn CVDisplayLinkCreateWithActiveCGDisplays(display_link_out: *mut CVDisplayLinkRef) -> i32;
+ pub fn CVDisplayLinkSetOutputCallback(
+ display_link: CVDisplayLinkRef,
+ callback: CVDisplayLinkOutputCallback,
+ user_info: *mut c_void,
+ ) -> i32;
+ pub fn CVDisplayLinkStart(display_link: CVDisplayLinkRef) -> i32;
+ pub fn CVDisplayLinkStop(display_link: CVDisplayLinkRef) -> i32;
+ pub fn CVDisplayLinkRelease(display_link: CVDisplayLinkRef);
+ pub fn CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link: CVDisplayLinkRef) -> CVTime;
+}
+
// Foundation
#[repr(C)]
diff --git a/src/native/macos.rs b/src/native/macos.rs
index 29b3e2ef..3d9eb06e 100644
--- a/src/native/macos.rs
+++ b/src/native/macos.rs
@@ -15,7 +15,10 @@ use {
std::{
collections::HashMap,
os::raw::c_void,
- sync::mpsc::Receiver,
+ sync::{
+ Condvar, Mutex,
+ mpsc::Receiver,
+ },
time::{Duration, Instant},
},
};
@@ -1016,6 +1019,94 @@ impl crate::native::Clipboard for MacosClipboard {
fn set(&mut self, _data: &str) {}
}
+struct FrameSignal {
+ frame_ready: Mutex,
+ cond: Condvar,
+}
+
+extern "C" fn display_link_frame_ready_callback(
+ _display_link: CVDisplayLinkRef,
+ _now: *const CVTimeStamp,
+ _output_time: *const CVTimeStamp,
+ _flags_in: CVOptionFlags,
+ _flags_out: *mut CVOptionFlags,
+ display_link_context: *mut c_void,
+) -> i32 {
+ unsafe {
+ let frame_signal = &*(display_link_context as *const FrameSignal);
+ if let Ok(mut frame_ready) = frame_signal.frame_ready.lock() {
+ *frame_ready = true;
+ frame_signal.cond.notify_one();
+ }
+ }
+ 0
+}
+
+struct FramePacer {
+ display_link: CVDisplayLinkRef,
+ frame_signal: Box,
+}
+
+impl FramePacer {
+ fn new() -> Option {
+ unsafe {
+ let mut display_link: CVDisplayLinkRef = std::ptr::null_mut();
+ if CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link as *mut _) != 0
+ || display_link.is_null()
+ {
+ return None;
+ }
+
+ let frame_signal = Box::new(FrameSignal {
+ frame_ready: Mutex::new(false),
+ cond: Condvar::new(),
+ });
+
+ if CVDisplayLinkSetOutputCallback(
+ display_link,
+ display_link_frame_ready_callback,
+ (&*frame_signal as *const FrameSignal) as *mut c_void,
+ ) != 0
+ {
+ CVDisplayLinkRelease(display_link);
+ return None;
+ }
+
+ let _ = CVDisplayLinkStart(display_link);
+
+ Some(Self {
+ display_link,
+ frame_signal,
+ })
+ }
+ }
+
+ fn wait_next_frame(&self) {
+ let mut frame_ready = match self.frame_signal.frame_ready.lock() {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ };
+ while !*frame_ready {
+ frame_ready = match self.frame_signal.cond.wait(frame_ready) {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ };
+ }
+ *frame_ready = false;
+ }
+}
+
+impl Drop for FramePacer {
+ fn drop(&mut self) {
+ unsafe {
+ if !self.display_link.is_null() {
+ let _ = CVDisplayLinkStop(self.display_link);
+ CVDisplayLinkRelease(self.display_link);
+ }
+ }
+ }
+}
+
unsafe extern "C" fn release_data(info: *mut c_void, _: *const c_void, _: usize) {
drop(Box::from_raw(info as *mut &[u8]));
}
@@ -1305,6 +1396,11 @@ where
// Basically reimplementing msg_send![ns_app, run] here
let distant_future: ObjcId = msg_send![class!(NSDate), distantFuture];
let distant_past: ObjcId = msg_send![class!(NSDate), distantPast];
+ let frame_pacer = if conf.platform.blocking_event_loop {
+ None
+ } else {
+ FramePacer::new()
+ };
let mut done = false;
while !(done || crate::native_display().lock().unwrap().quit_ordered) {
while let Ok(request) = display.native_requests.try_recv() {
@@ -1336,5 +1432,9 @@ where
if !conf.platform.blocking_event_loop || display.update_requested {
perform_redraw(&mut display, conf.platform.apple_gfx_api, false);
}
+
+ if let Some(frame_pacer) = frame_pacer.as_ref() {
+ frame_pacer.wait_next_frame();
+ }
}
}
From d5847f619f569f40a991b7e6352acf2ccc7b80cf Mon Sep 17 00:00:00 2001
From: Pewnack <[email protected]>
Date: Tue, 14 Apr 2026 22:00:01 +0200
Subject: [PATCH 2/4] Disable VSync because we are using CVDisplayLInk and we
don't want to wait twice. Added timeouts to the frame pacer to prevent
waiting forever for a next frame.
---
src/native/macos.rs | 42 ++++++++++++++++++++++++++++++------------
1 file changed, 30 insertions(+), 12 deletions(-)
diff --git a/src/native/macos.rs b/src/native/macos.rs
index 3d9eb06e..19aefaea 100644
--- a/src/native/macos.rs
+++ b/src/native/macos.rs
@@ -997,7 +997,9 @@ unsafe fn create_opengl_view(
display.gl_context = msg_send![gl_context, initWithFormat: glpixelformat_obj shareContext: nil];
- let mut swap_interval = 1;
+ // Set the swap interval to 0 to disable vsync, because we're using CVDisplayLink for frame pacing
+ // and we don't want to have an extra vsync delay on top of that.
+ let mut swap_interval = 0;
let () = msg_send![display.gl_context,
setValues:&mut swap_interval
forParameter:NSOpenGLContextParameterSwapInterval];
@@ -1072,7 +1074,10 @@ impl FramePacer {
return None;
}
- let _ = CVDisplayLinkStart(display_link);
+ if CVDisplayLinkStart(display_link) != 0 {
+ CVDisplayLinkRelease(display_link);
+ return None;
+ }
Some(Self {
display_link,
@@ -1081,18 +1086,26 @@ impl FramePacer {
}
}
- fn wait_next_frame(&self) {
+ fn wait_next_frame(&self, timeout: Duration) -> bool {
let mut frame_ready = match self.frame_signal.frame_ready.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
- while !*frame_ready {
- frame_ready = match self.frame_signal.cond.wait(frame_ready) {
- Ok(guard) => guard,
- Err(poisoned) => poisoned.into_inner(),
- };
+ if *frame_ready {
+ *frame_ready = false;
+ return true;
+ }
+
+ let (mut frame_ready, _) = match self.frame_signal.cond.wait_timeout(frame_ready, timeout) {
+ Ok(pair) => pair,
+ Err(poisoned) => poisoned.into_inner(),
+ };
+
+ let ready = *frame_ready;
+ if ready {
+ *frame_ready = false;
}
- *frame_ready = false;
+ ready
}
}
@@ -1403,6 +1416,14 @@ where
};
let mut done = false;
while !(done || crate::native_display().lock().unwrap().quit_ordered) {
+
+ // Wait at the top for just in time rendering
+ if let Some(frame_pacer) = frame_pacer.as_ref() {
+ if !frame_pacer.wait_next_frame(Duration::from_millis(50)) {
+ std::thread::yield_now();
+ }
+ }
+
while let Ok(request) = display.native_requests.try_recv() {
display.process_request(request);
}
@@ -1433,8 +1454,5 @@ where
perform_redraw(&mut display, conf.platform.apple_gfx_api, false);
}
- if let Some(frame_pacer) = frame_pacer.as_ref() {
- frame_pacer.wait_next_frame();
- }
}
}
From 6f8b5a3408be4bc93abbf200ba18ce768ecccd00 Mon Sep 17 00:00:00 2001
From: Pewnack <[email protected]>
Date: Fri, 17 Apr 2026 22:00:04 +0200
Subject: [PATCH 3/4] Call to thread::yield_now is a bit overkill. It's only
called after the timeout expired. Waiting for the timeout already gave time
to other process/threads/
---
src/native/macos.rs | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/src/native/macos.rs b/src/native/macos.rs
index 19aefaea..b827b9d1 100644
--- a/src/native/macos.rs
+++ b/src/native/macos.rs
@@ -1086,14 +1086,14 @@ impl FramePacer {
}
}
- fn wait_next_frame(&self, timeout: Duration) -> bool {
+ fn wait_next_frame(&self, timeout: Duration) {
let mut frame_ready = match self.frame_signal.frame_ready.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if *frame_ready {
*frame_ready = false;
- return true;
+ return;
}
let (mut frame_ready, _) = match self.frame_signal.cond.wait_timeout(frame_ready, timeout) {
@@ -1101,11 +1101,9 @@ impl FramePacer {
Err(poisoned) => poisoned.into_inner(),
};
- let ready = *frame_ready;
- if ready {
+ if *frame_ready {
*frame_ready = false;
}
- ready
}
}
@@ -1419,9 +1417,7 @@ where
// Wait at the top for just in time rendering
if let Some(frame_pacer) = frame_pacer.as_ref() {
- if !frame_pacer.wait_next_frame(Duration::from_millis(50)) {
- std::thread::yield_now();
- }
+ frame_pacer.wait_next_frame(Duration::from_millis(50));
}
while let Ok(request) = display.native_requests.try_recv() {
From 99ab3cf55773f6dfa5d6270c09fda8af79cd0294 Mon Sep 17 00:00:00 2001
From: Pewnack <[email protected]>
Date: Sat, 18 Apr 2026 07:57:07 +0200
Subject: [PATCH 4/4] Checking for poisoned locks seems a bit overkill. If the
sub thread panics let's just panic here as well.
---
src/native/macos.rs | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/src/native/macos.rs b/src/native/macos.rs
index b827b9d1..744a88b3 100644
--- a/src/native/macos.rs
+++ b/src/native/macos.rs
@@ -1087,19 +1087,13 @@ impl FramePacer {
}
fn wait_next_frame(&self, timeout: Duration) {
- let mut frame_ready = match self.frame_signal.frame_ready.lock() {
- Ok(guard) => guard,
- Err(poisoned) => poisoned.into_inner(),
- };
+ let mut frame_ready = self.frame_signal.frame_ready.lock().unwrap();
if *frame_ready {
*frame_ready = false;
return;
}
- let (mut frame_ready, _) = match self.frame_signal.cond.wait_timeout(frame_ready, timeout) {
- Ok(pair) => pair,
- Err(poisoned) => poisoned.into_inner(),
- };
+ let (mut frame_ready, _) = self.frame_signal.cond.wait_timeout(frame_ready, timeout).unwrap();
if *frame_ready {
*frame_ready = false;