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