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..744a88b3 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}, }, }; @@ -994,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]; @@ -1016,6 +1021,97 @@ 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; + } + + if CVDisplayLinkStart(display_link) != 0 { + CVDisplayLinkRelease(display_link); + return None; + } + + Some(Self { + display_link, + frame_signal, + }) + } + } + + fn wait_next_frame(&self, timeout: Duration) { + let mut frame_ready = self.frame_signal.frame_ready.lock().unwrap(); + if *frame_ready { + *frame_ready = false; + return; + } + + let (mut frame_ready, _) = self.frame_signal.cond.wait_timeout(frame_ready, timeout).unwrap(); + + if *frame_ready { + *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,8 +1401,19 @@ 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) { + + // Wait at the top for just in time rendering + if let Some(frame_pacer) = frame_pacer.as_ref() { + frame_pacer.wait_next_frame(Duration::from_millis(50)); + } + while let Ok(request) = display.native_requests.try_recv() { display.process_request(request); } @@ -1336,5 +1443,6 @@ where if !conf.platform.blocking_event_loop || display.update_requested { perform_redraw(&mut display, conf.platform.apple_gfx_api, false); } + } }