Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: use display link api to implement vsync on macos. #2102

Merged
merged 2 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}
}