diff --git a/Cargo.lock b/Cargo.lock index f6d6e19b..3fe0ee22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,26 @@ dependencies = [ "virtue", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools", + "log 0.4.27", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -363,6 +383,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -408,6 +437,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.40" @@ -1731,7 +1771,7 @@ checksum = "42e1e32c7d57a2f29223b24a0e6be6723087bec394fd65a638e4d049324205fb" dependencies = [ "encoding_rs", "librashader-common", - "nom", + "nom 8.0.0", "serde", "thiserror 2.0.12", ] @@ -1743,7 +1783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57373632c122fad07da917fae6049febfdb1b2c206511cf4c1fad0f97f93e634" dependencies = [ "librashader-common", - "nom", + "nom 8.0.0", "nom_locate", "num-traits", "once_cell", @@ -1935,6 +1975,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2027,6 +2073,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -2044,7 +2100,7 @@ checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", "memchr", - "nom", + "nom 8.0.0", ] [[package]] @@ -2499,6 +2555,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2799,6 +2865,7 @@ version = "0.3.0" dependencies = [ "ahash", "atomic-waker", + "bindgen", "bitflags 2.9.1", "bytemuck", "calloop", diff --git a/README.md b/README.md index d1e8a268..e3559e1e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@

+> **⚠️ Development Notice**: We are currently in the process of releasing Rio 0.3.0, which includes major performance improvements and architectural changes. The main branch is under active development and may be unstable. For stable usage, please use the [latest release](https://github.com/raphamorim/rio/releases). + ## About Documentation: https://rioterm.com diff --git a/docs/docs/releases.md b/docs/docs/releases.md index e92cacc8..80d00de8 100644 --- a/docs/docs/releases.md +++ b/docs/docs/releases.md @@ -9,6 +9,16 @@ language: 'en' ### Performance Optimizations +- **Major**: Implemented efficient CVDisplayLink-based VSync synchronization for macOS + - Perfect frame timing aligned with display hardware refresh cycles + - Eliminates screen tearing and stuttering through hardware VSync synchronization + - Adaptive refresh rate support: automatically handles 60Hz, 120Hz, ProMotion displays + - Multi-display support: adapts when windows move between displays with different refresh rates + - Grand Central Dispatch (GCD) integration for thread-safe cross-thread communication + - **Smart rendering**: Only renders when content actually changes using dirty flag system + - Power efficient: skips unnecessary redraws when content is static, reducing CPU usage + - Professional rendering quality with smooth, tear-free visual updates + - CVDisplayLink runs on dedicated background thread, never blocking UI operations - **Major**: Implemented a new text run caching system replacing line-based caching - Up to 96% reduction in text shaping overhead for repeated content - Individual text runs (words, operators, keywords) cached and reused across frames diff --git a/rio-window/Cargo.toml b/rio-window/Cargo.toml index 68a4a4d6..bebde807 100644 --- a/rio-window/Cargo.toml +++ b/rio-window/Cargo.toml @@ -33,6 +33,9 @@ wayland-csd-adwaita-notitle = ["sctk-adwaita"] [build-dependencies] cfg_aliases = "0.2.0" +[target.'cfg(target_os = "macos")'.build-dependencies] +bindgen = "0.70.1" + [dependencies] bitflags = { workspace = true } cursor-icon = "1.1.0" diff --git a/rio-window/build.rs b/rio-window/build.rs index 034288e2..bff19f39 100644 --- a/rio-window/build.rs +++ b/rio-window/build.rs @@ -36,4 +36,36 @@ fn main() { println!("cargo:rustc-check-cfg=cfg(orbital_platform)"); println!("cargo:rustc-check-cfg=cfg(unreleased_changelogs)"); + + #[cfg(target_os = "macos")] + generate_dispatch_bindings(); +} + +#[cfg(target_os = "macos")] +fn generate_dispatch_bindings() { + use std::{env, path::PathBuf}; + + println!("cargo:rustc-link-lib=framework=System"); + println!("cargo:rerun-if-changed=src/platform_impl/macos/dispatch.h"); + + let bindings = bindgen::Builder::default() + .header("src/platform_impl/macos/dispatch.h") + .allowlist_var("_dispatch_main_q") + .allowlist_var("_dispatch_source_type_data_add") + .allowlist_function("dispatch_source_create") + .allowlist_function("dispatch_source_merge_data") + .allowlist_function("dispatch_source_set_event_handler_f") + .allowlist_function("dispatch_set_context") + .allowlist_function("dispatch_resume") + .allowlist_function("dispatch_suspend") + .allowlist_function("dispatch_source_cancel") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .layout_tests(false) + .generate() + .expect("unable to generate dispatch bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("dispatch_sys.rs")) + .expect("couldn't write dispatch bindings"); } diff --git a/rio-window/src/platform_impl/macos/appkit/window.rs b/rio-window/src/platform_impl/macos/appkit/window.rs index 5f5d8f17..8bc9d68d 100644 --- a/rio-window/src/platform_impl/macos/appkit/window.rs +++ b/rio-window/src/platform_impl/macos/appkit/window.rs @@ -239,6 +239,9 @@ extern_methods!( #[method(setDelegate:)] pub(crate) fn setDelegate(&self, delegate: Option<&NSObject>); + #[method_id(delegate)] + pub(crate) fn delegate(&self) -> Option>; + #[method(sendEvent:)] pub(crate) unsafe fn sendEvent(&self, event: &NSEvent); diff --git a/rio-window/src/platform_impl/macos/dispatch.h b/rio-window/src/platform_impl/macos/dispatch.h new file mode 100644 index 00000000..10fec8c2 --- /dev/null +++ b/rio-window/src/platform_impl/macos/dispatch.h @@ -0,0 +1,2 @@ +#include +#include \ No newline at end of file diff --git a/rio-window/src/platform_impl/macos/dispatcher.rs b/rio-window/src/platform_impl/macos/dispatcher.rs new file mode 100644 index 00000000..25700e85 --- /dev/null +++ b/rio-window/src/platform_impl/macos/dispatcher.rs @@ -0,0 +1,19 @@ +//! GCD (Grand Central Dispatch) bindings and utilities +//! +//! This module provides safe Rust bindings for macOS Grand Central Dispatch, +//! for VSync synchronization in Rio's rendering pipeline. + +use std::ptr::addr_of; + +/// Generated GCD dispatch bindings +pub(crate) mod dispatch_sys { + #![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)] + include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); +} + +pub use dispatch_sys::*; + +/// Get the main dispatch queue (equivalent to dispatch_get_main_queue()) +pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t { + addr_of!(_dispatch_main_q) as *const _ as dispatch_queue_t +} diff --git a/rio-window/src/platform_impl/macos/display_link.rs b/rio-window/src/platform_impl/macos/display_link.rs new file mode 100644 index 00000000..cb49911e --- /dev/null +++ b/rio-window/src/platform_impl/macos/display_link.rs @@ -0,0 +1,341 @@ +//! CVDisplayLink Integration for VSync Synchronization +//! +//! This module implements CVDisplayLink integration to provide precise VSync timing +//! for Rio's rendering system, using Grand Central Dispatch for thread-safe communication. +//! +//! ## Why CVDisplayLink vs NSTimer/Event-Driven Rendering? +//! +//! ### Traditional Approach (Rio's previous method): +//! ``` +//! // Event-driven rendering - fires whenever something changes +//! self.request_redraw(); // Irregular timing, not VSync aligned +//! +//! // Or NSTimer-based rendering +//! Timer::new(16.67ms, || self.request_redraw()); // Close but not precise +//! ``` +//! +//! ### CVDisplayLink Approach (Rio's new method): +//! ``` +//! // VSync-synchronized rendering - fires exactly when display is ready +//! CVDisplayLink -> GCD dispatch -> main thread callback -> request_frame() +//! ``` +//! +//! ## Key Benefits: +//! +//! 1. **Hardware VSync Synchronization**: Perfect timing with display refresh +//! 2. **Adaptive Refresh Rate**: 60Hz, 120Hz, ProMotion support +//! 3. **Multi-Display Support**: Automatic adaptation when moving windows +//! 4. **Thread Safety**: GCD handles cross-thread communication safely +//! 5. **Power Efficiency**: Only fires when display refreshes + +use std::ffi::c_void; +use std::ptr; + +use core_graphics::display::CGDirectDisplayID; + +use super::dispatcher::{dispatch_get_main_queue, dispatch_sys::*}; +use super::ffi::CVDisplayLinkRelease; +use super::window::WindowId; + +/// CVDisplayLink callback function type +pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn( + display_link: CVDisplayLinkRef, + current_time: *const CVTimeStamp, + output_time: *const CVTimeStamp, + flags_in: i64, + flags_out: *mut i64, + user_info: *mut c_void, +) -> i32; + +/// CVTimeStamp structure +#[repr(C)] +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: CVSMPTETime, + pub flags: u64, + pub reserved: u64, +} + +/// CVSMPTETime structure +#[repr(C)] +pub struct CVSMPTETime { + pub subframes: i16, + pub subframe_divisor: i16, + pub counter: u32, + pub type_: u32, + pub flags: u32, + pub hours: i16, + pub minutes: i16, + pub seconds: i16, + pub frames: i16, +} + +/// Use existing CVDisplayLinkRef from ffi.rs +use super::ffi::CVDisplayLinkRef; + +#[link(name = "CoreVideo", kind = "framework")] +extern "C" { + fn CVDisplayLinkCreateWithActiveCGDisplays( + display_link_out: *mut CVDisplayLinkRef, + ) -> i32; + fn CVDisplayLinkSetCurrentCGDisplay( + display_link: CVDisplayLinkRef, + display_id: CGDirectDisplayID, + ) -> i32; + fn CVDisplayLinkSetOutputCallback( + display_link: CVDisplayLinkRef, + callback: CVDisplayLinkOutputCallback, + user_info: *mut c_void, + ) -> i32; + fn CVDisplayLinkStart(display_link: CVDisplayLinkRef) -> i32; + fn CVDisplayLinkStop(display_link: CVDisplayLinkRef) -> i32; +} + +/// User data for the display link callback +#[repr(C)] +#[derive(Debug)] +pub struct DisplayLinkUserData { + pub window_id: WindowId, + pub dispatch_source: dispatch_source_t, + pub view_ptr: *mut std::ffi::c_void, // Pointer to the view for direct access +} + +/// DisplayLink wrapper using GCD-based approach +#[derive(Debug)] +pub struct DisplayLink { + display_link: CVDisplayLinkRef, + dispatch_source: dispatch_source_t, + user_data: Box, + is_running: std::cell::Cell, +} + +unsafe impl Send for DisplayLink {} +unsafe impl Sync for DisplayLink {} + +impl DisplayLink { + /// Create a new DisplayLink using GCD-based approach + /// + /// This provides VSync synchronization with thread-safe communication + /// via Grand Central Dispatch instead of CFRunLoopSource. + pub fn new( + display_id: CGDirectDisplayID, + window_id: WindowId, + view_ptr: *mut c_void, + callback: unsafe extern "C" fn(*mut c_void), + ) -> Result { + unsafe { + // Create GCD dispatch source for main queue communication + let dispatch_source = dispatch_source_create( + &_dispatch_source_type_data_add, + 0, + 0, + dispatch_get_main_queue(), + ); + + if dispatch_source.is_null() { + return Err("Failed to create GCD dispatch source"); + } + + // Create user data + let user_data = Box::new(DisplayLinkUserData { + window_id, + dispatch_source, + view_ptr, + }); + + // Set up GCD event handler + dispatch_set_context( + super::dispatcher::dispatch_sys::dispatch_object_t { + _ds: dispatch_source, + }, + &*user_data as *const _ as *mut c_void, + ); + dispatch_source_set_event_handler_f(dispatch_source, Some(callback)); + + // Create CVDisplayLink + let mut display_link: CVDisplayLinkRef = ptr::null_mut(); + let result = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link); + if result != 0 { + dispatch_source_cancel(dispatch_source); + return Err("Failed to create CVDisplayLink"); + } + + // Set the display + let result = CVDisplayLinkSetCurrentCGDisplay(display_link, display_id); + if result != 0 { + CVDisplayLinkRelease(display_link); + dispatch_source_cancel(dispatch_source); + return Err("Failed to set display for CVDisplayLink"); + } + + // Set the VSync callback + let user_data_ptr = &*user_data as *const DisplayLinkUserData as *mut c_void; + let result = CVDisplayLinkSetOutputCallback( + display_link, + display_link_callback, + user_data_ptr, + ); + + if result != 0 { + CVDisplayLinkRelease(display_link); + dispatch_source_cancel(dispatch_source); + return Err("Failed to set callback for CVDisplayLink"); + } + + tracing::info!( + "CVDisplayLink created with GCD for window {:?} on display {}", + window_id, + display_id + ); + + Ok(DisplayLink { + display_link, + dispatch_source, + user_data, + is_running: std::cell::Cell::new(false), + }) + } + } + + /// Start VSync-synchronized rendering + pub fn start(&self) -> Result<(), &'static str> { + if self.is_running.get() { + tracing::debug!( + "Display link already running for window {:?}", + self.user_data.window_id + ); + return Ok(()); + } + + unsafe { + // Resume GCD dispatch source + dispatch_resume(super::dispatcher::dispatch_sys::dispatch_object_t { + _ds: self.dispatch_source, + }); + + // Start CVDisplayLink + let result = CVDisplayLinkStart(self.display_link); + if result != 0 { + dispatch_suspend(super::dispatcher::dispatch_sys::dispatch_object_t { + _ds: self.dispatch_source, + }); + Err("Failed to start CVDisplayLink") + } else { + self.is_running.set(true); + tracing::info!( + "CVDisplayLink started - VSync callbacks active for window {:?}", + self.user_data.window_id + ); + Ok(()) + } + } + } + + /// Stop VSync-synchronized rendering + pub fn stop(&self) -> Result<(), &'static str> { + if !self.is_running.get() { + tracing::debug!( + "Display link already stopped for window {:?}", + self.user_data.window_id + ); + return Ok(()); + } + + unsafe { + // Stop CVDisplayLink + let result = CVDisplayLinkStop(self.display_link); + + // Suspend GCD dispatch source + dispatch_suspend(super::dispatcher::dispatch_sys::dispatch_object_t { + _ds: self.dispatch_source, + }); + + if result != 0 { + Err("Failed to stop CVDisplayLink") + } else { + self.is_running.set(false); + tracing::info!( + "CVDisplayLink stopped for window {:?}", + self.user_data.window_id + ); + Ok(()) + } + } + } +} + +impl Drop for DisplayLink { + fn drop(&mut self) { + unsafe { + // Stop first + let _ = self.stop(); + + // Cancel GCD dispatch source + dispatch_source_cancel(self.dispatch_source); + + // Release CVDisplayLink + CVDisplayLinkRelease(self.display_link); + } + } +} + +/// CVDisplayLink callback - runs on dedicated CVDisplayLink thread +/// +/// This callback fires at exactly the right time for VSync, then uses +/// GCD to safely communicate with the main thread. +unsafe extern "C" fn display_link_callback( + _display_link: CVDisplayLinkRef, + current_time: *const CVTimeStamp, + output_time: *const CVTimeStamp, + _flags_in: i64, + _flags_out: *mut i64, + user_info: *mut c_void, +) -> i32 { + if user_info.is_null() { + return 0; + } + + unsafe { + let user_data = &*(user_info as *const DisplayLinkUserData); + + // Extract timing information for debugging + if tracing::enabled!(tracing::Level::TRACE) { + let _current_host_time = (*current_time).host_time; + let _output_host_time = (*output_time).host_time; + let refresh_period = (*output_time).video_refresh_period; + let refresh_rate = if refresh_period > 0 { + (*output_time).video_time_scale as f64 / refresh_period as f64 + } else { + 60.0 + }; + + tracing::trace!( + "VSync callback on CVDisplayLink thread: window={:?}, refresh_rate={:.1}Hz", + user_data.window_id, + refresh_rate + ); + } + + // Signal main thread via GCD - this is the key to the approach + dispatch_source_merge_data(user_data.dispatch_source, 1); + } + + 0 // Success +} + +/// Extension trait for WindowDelegate to support display link integration +pub trait DisplayLinkSupport { + /// Set up the display link for this window + fn setup_display_link(&self) -> Result<(), &'static str>; + + /// Start VSync-synchronized rendering + fn start_display_link(&self) -> Result<(), &'static str>; + + /// Stop VSync-synchronized rendering + fn stop_display_link(&self) -> Result<(), &'static str>; +} diff --git a/rio-window/src/platform_impl/macos/mod.rs b/rio-window/src/platform_impl/macos/mod.rs index fe331025..389a289f 100644 --- a/rio-window/src/platform_impl/macos/mod.rs +++ b/rio-window/src/platform_impl/macos/mod.rs @@ -4,6 +4,8 @@ mod util; mod app; mod app_delegate; mod cursor; +mod dispatcher; +mod display_link; mod event; mod event_handler; mod event_loop; diff --git a/rio-window/src/platform_impl/macos/view.rs b/rio-window/src/platform_impl/macos/view.rs index 0eab9e3c..48e10c9c 100644 --- a/rio-window/src/platform_impl/macos/view.rs +++ b/rio-window/src/platform_impl/macos/view.rs @@ -23,6 +23,7 @@ use super::event::{ ralt_pressed, scancode_to_physicalkey, }; use super::window::WinitWindow; +use super::window_delegate::WindowDelegate; use super::DEVICE_ID; use crate::dpi::{LogicalPosition, LogicalSize}; use crate::event::{ @@ -1200,3 +1201,34 @@ fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Retained Option> { + if view_ptr.is_null() { + return None; + } + + unsafe { + // Cast the view pointer back to WinitView + let view = view_ptr as *const WinitView; + let view = &*view; + + // Get the window from the view + if let Some(window) = view.ivars()._ns_window.load() { + // Get the delegate from the window + if let Some(delegate) = window.delegate() { + // Cast the delegate back to WindowDelegate + // SAFETY: We know this is a WindowDelegate because we set it + let delegate_ptr = Retained::as_ptr(&delegate) as *mut WindowDelegate; + Retained::retain(delegate_ptr) + } else { + None + } + } else { + None + } + } +} diff --git a/rio-window/src/platform_impl/macos/window_delegate.rs b/rio-window/src/platform_impl/macos/window_delegate.rs index 130452bd..74b2e86a 100644 --- a/rio-window/src/platform_impl/macos/window_delegate.rs +++ b/rio-window/src/platform_impl/macos/window_delegate.rs @@ -24,6 +24,7 @@ use objc2_foundation::{ use super::app_delegate::ApplicationDelegate; use super::cursor::cursor_from_icon; +use super::display_link::{DisplayLink, DisplayLinkSupport}; use super::monitor::{self, flip_window_screen_coordinates, get_display_id}; use super::view::WinitView; use super::window::WinitWindow; @@ -125,6 +126,11 @@ pub(crate) struct State { is_simple_fullscreen: Cell, saved_style: Cell>, background_color: RefCell>, + + // Display link for VSync timing + display_link: RefCell>, + // Track when rendering is needed (dirty state) + needs_redraw: Cell, } declare_class!( @@ -153,6 +159,12 @@ declare_class!( #[method(windowWillClose:)] fn window_will_close(&self, _: Option<&AnyObject>) { trace_scope!("windowWillClose:"); + + // Stop the display link before closing + if let Err(e) = self.stop_display_link() { + tracing::warn!("Failed to stop display link: {}", e); + } + // `setDelegate:` retains the previous value and then autoreleases it autoreleasepool(|_| { // Since El Capitan, we need to be careful that delegate methods can't @@ -341,6 +353,16 @@ declare_class!( fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) { trace_scope!("windowDidChangeOcclusionState:"); let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible); + + // Manage display link based on window visibility for power efficiency + if visible { + if let Err(e) = self.start_display_link() { + tracing::warn!("Failed to start display link when window became visible: {}", e); + } + } else if let Err(e) = self.stop_display_link() { + tracing::warn!("Failed to stop display link when window became occluded: {}", e); + } + self.queue_event(WindowEvent::Occluded(!visible)); } @@ -353,6 +375,22 @@ declare_class!( self.window().setFrame_display(screen.frame(), true); } } + + // Reinitialize display link for the new screen to ensure proper VSync timing + // This is crucial for multi-display setups with different refresh rates + tracing::info!("Window moved to different screen, reinitializing display link"); + if let Err(e) = self.stop_display_link() { + tracing::warn!("Failed to stop display link before screen change: {}", e); + } + + // Reinitialize with new display + if let Err(e) = self.setup_display_link() { + tracing::warn!("Failed to setup display link for new screen: {}", e); + } else if let Err(e) = self.start_display_link() { + tracing::warn!("Failed to start display link for new screen: {}", e); + } else { + tracing::info!("Display link reinitialized for new screen"); + } } } @@ -710,6 +748,8 @@ impl WindowDelegate { is_simple_fullscreen: Cell::new(false), saved_style: Cell::new(None), background_color: unsafe { NSColor::blackColor().into() }, + display_link: RefCell::new(None), + needs_redraw: Cell::new(false), }); let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; @@ -769,6 +809,9 @@ impl WindowDelegate { delegate.set_maximized(attrs.maximized); } + // Initialize display link for VSync timing + delegate.initialize_display_link(); + Ok(delegate) } @@ -888,7 +931,23 @@ impl WindowDelegate { } pub fn request_redraw(&self) { - self.ivars().app_delegate.queue_redraw(self.window().id()); + // Mark window as needing redraw instead of immediately queuing + // The display link will handle the actual redraw on next VSync + self.ivars().needs_redraw.set(true); + tracing::trace!("Window {:?} marked as needing redraw", self.id()); + } + + pub fn initialize_display_link(&self) { + if let Err(e) = self.setup_display_link() { + tracing::warn!("Failed to setup display link: {}", e); + } else if let Err(e) = self.start_display_link() { + tracing::warn!("Failed to start display link: {}", e); + } else { + tracing::info!( + "Display link initialized successfully for window {:?}", + self.id() + ); + } } #[inline] @@ -1968,3 +2027,103 @@ fn configure_window_colorspace( tracing::warn!("Window has no content view for colorspace configuration"); } } + +impl DisplayLinkSupport for WindowDelegate { + fn setup_display_link(&self) -> Result<(), &'static str> { + tracing::info!("Setting up display link for window {:?}", self.id()); + + // Get the display ID for the current monitor + let display_id = match self.current_monitor_inner() { + Some(monitor) => { + // Get the actual display ID from the monitor + let display_id = monitor.native_identifier(); + tracing::info!( + "Using display ID {} from current monitor for window {:?}", + display_id, + self.id() + ); + display_id + } + None => { + tracing::warn!("Could not get current monitor, using main display"); + use core_graphics::display::CGDisplay; + CGDisplay::main().id + } + }; + + // Get window ID + let window_id = self.id(); + + // Create GCD callback for VSync-timed redraws + unsafe extern "C" fn vsync_callback(context: *mut std::ffi::c_void) { + if context.is_null() { + return; + } + + unsafe { + let user_data = + &*(context as *const super::display_link::DisplayLinkUserData); + + // Get the view directly from the user data + let view = user_data.view_ptr; + + // Get window delegate from the view (similar to Zed's approach) + use super::view::get_window_delegate; + if let Some(window_delegate) = get_window_delegate(view) { + // Check if window needs redraw (dirty state) + if window_delegate.ivars().needs_redraw.get() { + // Clear dirty flag and trigger redraw + window_delegate.ivars().needs_redraw.set(false); + window_delegate + .ivars() + .app_delegate + .queue_redraw(user_data.window_id); + tracing::trace!( + "VSync redraw triggered for dirty window {:?}", + user_data.window_id + ); + } else { + tracing::trace!( + "VSync callback skipped - window {:?} not dirty", + user_data.window_id + ); + } + } else { + tracing::warn!( + "VSync callback could not get window delegate from view for window {:?}", + user_data.window_id + ); + } + } + } + + // Get the view pointer for direct access + let view = self.view(); + let view_ptr = Retained::as_ptr(&view) as *mut std::ffi::c_void; + + // Create the display link with GCD-based communication + let display_link = + DisplayLink::new(display_id, window_id, view_ptr, vsync_callback)?; + + // Store the display link + *self.ivars().display_link.borrow_mut() = Some(display_link); + + Ok(()) + } + + fn start_display_link(&self) -> Result<(), &'static str> { + if let Some(display_link) = self.ivars().display_link.borrow().as_ref() { + display_link.start() + } else { + Err("Display link not initialized") + } + } + + fn stop_display_link(&self) -> Result<(), &'static str> { + if let Some(display_link) = self.ivars().display_link.borrow().as_ref() { + display_link.stop() + } else { + Err("Display link not initialized") + } + } +}