Skip to content

Commit

Permalink
feat!: use display link api to implement vsync on macos. (#2102)
Browse files Browse the repository at this point in the history
* Use display link api to implement vsync on macos.

* Force swap interval to 0.
  • Loading branch information
crupest committed Nov 26, 2023
1 parent 59e4ed4 commit 75ba015
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use cmd_line::CmdLineSettings;
use editor::start_editor;
use error_handling::{handle_startup_errors, NeovideExitCode};
use renderer::{cursor_renderer::CursorSettings, RendererSettings};
#[cfg_attr(target_os = "windows", allow(unused_imports))]
use settings::SETTINGS;
use window::{
create_event_loop, create_window, determine_window_size, main_loop, WindowSettings, WindowSize,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/opengl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub fn build_context(window: GlWindow, srgb: bool, vsync: bool) -> Context {
// NOTE: We don't care if these fails, the driver can override the SwapInterval in any case, so it needs to work in all cases
// The OpenGL VSync is always disabled on Wayland and Windows, since they have their own
// implementation
let _ = if vsync && env::var("WAYLAND_DISPLAY").is_err() && OS != "windows" {
let _ = if vsync && env::var("WAYLAND_DISPLAY").is_err() && OS != "windows" && OS != "macos" {
surface.set_swap_interval(&context, SwapInterval::Wait(NonZeroU32::new(1).unwrap()))
} else {
surface.set_swap_interval(&context, SwapInterval::DontWait)
Expand Down
225 changes: 225 additions & 0 deletions src/renderer/vsync/macos_display_link.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
use std::{ffi::c_void, pin::Pin};

use crate::profiling::tracy_zone;

use self::core_video::CVReturn;

use cocoa::{
appkit::{NSScreen, NSWindow},
base::{id, nil},
foundation::{NSAutoreleasePool, NSDictionary, NSString},
};
use objc::{rc::autoreleasepool, *};

use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use winit::window::Window;

// Display link api reference: https://developer.apple.com/documentation/corevideo/cvdisplaylink?language=objc
#[allow(non_upper_case_globals, non_camel_case_types)]
pub mod core_video {
use std::ffi::c_void;

pub type CGDirectDisplayID = u32;

pub type CVReturn = i32;
pub const kCVReturnSuccess: CVReturn = 0;
pub const kCVReturnDisplayLinkAlreadyRunning: CVReturn = -6671;
pub const kCVReturnDisplayLinkNotRunning: CVReturn = -6672;
// pub const kCVReturnDisplayLinkCallbacksNotSet: CVReturn = -6673;

type SInt16 = i16;
type UInt32 = u32;
type uint32_t = u32;
type int32_t = i32;
type uint64_t = u64;
type int64_t = i64;
type double = f64;

#[repr(C)]
#[allow(non_snake_case)]
pub struct CVSMPTETime {
subframes: SInt16,
subframeDivisor: SInt16,
counter: UInt32,
_type: UInt32,
flags: UInt32,
hours: SInt16,
minutes: SInt16,
seconds: SInt16,
frames: SInt16,
}

#[repr(C)]
#[allow(non_snake_case)]
pub struct CVTimeStamp {
version: uint32_t,
videoTimeScale: int32_t,
videoTime: int64_t,
hostTime: uint64_t,
rateScalar: double,
videoRefreshPeriod: int64_t,
smpteTime: CVSMPTETime,
flags: uint64_t,
reserved: uint64_t,
}

pub type CVDisplayLinkRef = *mut c_void;

pub type CVDisplayLinkOutputCallback = extern "C" fn(
displayLink: CVDisplayLinkRef,
inNow: *const CVTimeStamp,
inOutputTime: *const CVTimeStamp,
flagsIn: u64,
flagsOut: *mut u64,
displayLinkContext: *mut c_void,
) -> CVReturn;

#[link(name = "CoreVideo", kind = "framework")]
extern "C" {
pub fn CVDisplayLinkCreateWithCGDisplay(
displayID: CGDirectDisplayID,
displayLinkOut: *mut CVDisplayLinkRef,
) -> CVReturn;
pub fn CVDisplayLinkRelease(displayLink: CVDisplayLinkRef);
pub fn CVDisplayLinkStart(displayLink: CVDisplayLinkRef) -> CVReturn;
// Because display link is destroyed directly, this function is unnecessary
#[allow(dead_code)]
pub fn CVDisplayLinkStop(displayLink: CVDisplayLinkRef) -> CVReturn;
pub fn CVDisplayLinkSetOutputCallback(
displayLink: CVDisplayLinkRef,
callback: CVDisplayLinkOutputCallback,
userInfo: *mut c_void,
) -> CVReturn;
}
}

pub struct MacosDisplayLinkCallbackArgs {
// some_info: ... in future
}

pub type MacosDisplayLinkCallback<UserData> = fn(&mut MacosDisplayLinkCallbackArgs, &mut UserData);

struct MacosDisplayLinkCallbackContext<UserData> {
callback: MacosDisplayLinkCallback<UserData>,
user_data: UserData,
}

pub struct MacosDisplayLink<UserData> {
display_link_ref: core_video::CVDisplayLinkRef,
// The context must be pinned since it is passed as a pointer to callback. If it moves, the pointer will be dangling.
context: Pin<Box<MacosDisplayLinkCallbackContext<UserData>>>,
}

#[allow(unused_variables, non_snake_case)]
extern "C" fn c_callback<UserData>(
displayLink: core_video::CVDisplayLinkRef,
inNow: *const core_video::CVTimeStamp,
inOutputTime: *const core_video::CVTimeStamp,
flagsIn: u64,
flagsOut: *mut u64,
displayLinkContext: *mut c_void,
) -> core_video::CVReturn {
tracy_zone!("VSyncDisplayLinkCallback");

// The display link should be dropped before vsync, so this should be safe.
let context =
unsafe { &mut *(displayLinkContext as *mut MacosDisplayLinkCallbackContext<UserData>) };

let mut args = MacosDisplayLinkCallbackArgs {};

(context.callback)(&mut args, &mut context.user_data);

core_video::kCVReturnSuccess
}

impl<UserData> MacosDisplayLink<UserData> {
pub fn new_from_display(
display_id: core_video::CGDirectDisplayID,
callback: MacosDisplayLinkCallback<UserData>,
user_data: UserData,
) -> Result<Self, CVReturn> {
let mut display_link = Self {
display_link_ref: std::ptr::null_mut(),
context: Box::<MacosDisplayLinkCallbackContext<UserData>>::pin(
MacosDisplayLinkCallbackContext {
callback,
user_data,
},
),
};

unsafe {
let result = core_video::CVDisplayLinkCreateWithCGDisplay(
display_id,
&mut display_link.display_link_ref,
);

if result != core_video::kCVReturnSuccess {
return Err(result);
}

core_video::CVDisplayLinkSetOutputCallback(
display_link.display_link_ref,
c_callback::<UserData>,
// Cast the display link to an unsafe pointer and pass to display link.
&*display_link.context as *const MacosDisplayLinkCallbackContext<UserData>
as *mut c_void,
);
}

Ok(display_link)
}

pub fn start(&self) -> Result<bool, CVReturn> {
unsafe {
let result = core_video::CVDisplayLinkStart(self.display_link_ref);

match result {
core_video::kCVReturnSuccess => Ok(true),
core_video::kCVReturnDisplayLinkAlreadyRunning => Ok(false),
_ => Err(result),
}
}
}

// Because display link is destroyed directly, this function is unnecessary
#[allow(dead_code)]
pub fn stop(&self) -> Result<bool, CVReturn> {
unsafe {
let result = core_video::CVDisplayLinkStop(self.display_link_ref);

match result {
core_video::kCVReturnSuccess => Ok(true),
core_video::kCVReturnDisplayLinkNotRunning => Ok(false),
_ => Err(result),
}
}
}
}

impl<UserData> Drop for MacosDisplayLink<UserData> {
fn drop(&mut self) {
unsafe {
core_video::CVDisplayLinkRelease(self.display_link_ref);
}
}
}

// Here is the doc about how to do this. https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc
pub fn get_display_id_of_window(window: &Window) -> core_video::CGDirectDisplayID {
let mut result = 0;
autoreleasepool(|| unsafe {
let key: id = NSString::alloc(nil)
.init_str("NSScreenNumber")
.autorelease();
if let RawWindowHandle::AppKit(handle) = window.raw_window_handle() {
let ns_window: id = handle.ns_window as id;
let display_id_ns_number = ns_window.screen().deviceDescription().valueForKey_(key);
result = msg_send![display_id_ns_number, unsignedIntValue];
} else {
// Should be impossible.
panic!("Not an AppKitWindowHandle.")
}
});
result
}
21 changes: 20 additions & 1 deletion src/renderer/vsync/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#[cfg(target_os = "macos")]
mod macos_display_link;
#[cfg(target_os = "macos")]
mod vsync_macos;
mod vsync_timer;
#[cfg(target_os = "windows")]
mod vsync_win;
Expand All @@ -11,13 +15,18 @@ use std::env;
#[cfg(target_os = "windows")]
use vsync_win::VSyncWin;

#[cfg(target_os = "macos")]
use vsync_macos::VSyncMacos;

#[allow(dead_code)]
pub enum VSync {
Opengl(),
WinitThrottling(),
Timer(VSyncTimer),
#[cfg(target_os = "windows")]
Windows(VSyncWin),
#[cfg(target_os = "macos")]
Macos(VSyncMacos),
}

impl VSync {
Expand All @@ -37,7 +46,7 @@ impl VSync {

#[cfg(target_os = "macos")]
{
VSync::Opengl()
VSync::Macos(VSyncMacos::new(context))
}
} else {
VSync::Timer(VSyncTimer::new())
Expand All @@ -49,11 +58,21 @@ impl VSync {
VSync::Timer(vsync) => vsync.wait_for_vsync(),
#[cfg(target_os = "windows")]
VSync::Windows(vsync) => vsync.wait_for_vsync(),
#[cfg(target_os = "macos")]
VSync::Macos(vsync) => vsync.wait_for_vsync(),
_ => {}
}
}

pub fn uses_winit_throttling(&self) -> bool {
matches!(self, VSync::WinitThrottling())
}

pub fn update(&mut self, #[allow(unused_variables)] context: &WindowedContext) {
match self {
#[cfg(target_os = "macos")]
VSync::Macos(vsync) => vsync.update(context),
_ => {}
}
}
}
98 changes: 98 additions & 0 deletions src/renderer/vsync/vsync_macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::sync::{Arc, Condvar, Mutex};

use log::{error, trace, warn};

use crate::renderer::WindowedContext;

use super::macos_display_link::{
core_video, get_display_id_of_window, MacosDisplayLink, MacosDisplayLinkCallbackArgs,
};

struct VSyncMacosDisplayLinkUserData {
vsync_count: Arc<(Mutex<usize>, Condvar)>,
}

fn vsync_macos_display_link_callback(
_args: &mut MacosDisplayLinkCallbackArgs,
user_data: &mut VSyncMacosDisplayLinkUserData,
) {
let (lock, cvar) = &*user_data.vsync_count;
let mut count = lock.lock().unwrap();
*count += 1;
cvar.notify_one();
}

pub struct VSyncMacos {
old_display: core_video::CGDirectDisplayID,
display_link: Option<MacosDisplayLink<VSyncMacosDisplayLinkUserData>>,
vsync_count: Arc<(Mutex<usize>, Condvar)>,
last_vsync: usize,
}

impl VSyncMacos {
pub fn new(context: &WindowedContext) -> Self {
let mut vsync = VSyncMacos {
old_display: 0,
display_link: None,
vsync_count: Arc::new((Mutex::new(0), Condvar::new())),
last_vsync: 0,
};

vsync.display_link = vsync.create_display_link(context);

vsync
}

fn create_display_link(
self: &mut Self,
context: &WindowedContext,
) -> Option<MacosDisplayLink<VSyncMacosDisplayLinkUserData>> {
self.old_display = get_display_id_of_window(context.window());

let vsync_count = self.vsync_count.clone();

match MacosDisplayLink::new_from_display(
self.old_display,
vsync_macos_display_link_callback,
VSyncMacosDisplayLinkUserData { vsync_count },
) {
Ok(display_link) => {
trace!("Succeeded to create display link.");
match display_link.start() {
Ok(did) => match did {
true => {
trace!("Display link started.");
}
false => {
warn!("Display link already started. This does not affect function. But it might be a bug.");
}
},
Err(code) => {
error!("Failed to start display link, CVReturn code: {}.", code);
}
}
Some(display_link)
}
Err(code) => {
error!("Failed to create display link, CVReturn code: {}.", code);
None
}
}
}

pub fn wait_for_vsync(&mut self) {
let (lock, cvar) = &*self.vsync_count;
let count = cvar
.wait_while(lock.lock().unwrap(), |count| *count < self.last_vsync + 1)
.unwrap();
self.last_vsync = *count;
}

pub fn update(&mut self, context: &WindowedContext) {
let new_display = get_display_id_of_window(context.window());
if new_display != self.old_display {
trace!("Window moved to a new screen, try to re-create the display link.");
self.display_link = self.create_display_link(context);
}
}
}

0 comments on commit 75ba015

Please sign in to comment.