Files
ghostty-rs/crates/ghostty-renderer/src/display_link.rs
T
ddidderr 452b782149 feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"):
- renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend
  (all renderer/metal/* files, shaders.metal verbatim, runtime library
  compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga;
  OpenGL remains the backend elsewhere.
- font: CoreText face, shaper and discovery with build.rs backend
  selection (coretext default; coretext-freetype / coretext-harfbuzz /
  coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple.
- input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes.
- os: macos, mach, cf_release_thread, NSProcessInfo args and macOS
  branches; config: macOS defaults, keybinds, Application Support paths.
- termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit
  handling, os_log logging.

Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings
clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot
run here. The embedded runtime/C API (embedded.zig) is the next step.

Also includes the cross-platform integration edits shared by the crash,
verification, Windows and macOS work (Cargo target-gating, thread
crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg
gates), a cfg fix so ghostty-config builds on wasm32, and an allow for
the intentional recursion in the crash overflow test.

Test Plan:
- cargo test --workspace (3224 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check --target aarch64-apple-darwin -p ghostty-renderer (with
  the documented cross env)
2026-10-12 06:02:39 +02:00

198 lines
6.3 KiB
Rust

//! The CVDisplayLink used to drive the rendering loop in sync with the
//! display (`pkg/macos/video/display_link.zig`, used by `generic.zig`).
//!
//! This is only functional on macOS. On every other platform
//! [`DisplayLink`] is an uninhabited type, so a `None` is all the generic
//! renderer ever has (Zig: `DisplayLink = void`).
use std::sync::Arc;
/// The error of display link operations (`DisplayLink.Error`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("invalid operation")]
InvalidOperation,
}
/// Called on every vsync from the display link's thread.
pub type Callback = Arc<dyn Fn() + Send + Sync>;
#[cfg(target_os = "macos")]
pub use self::macos::DisplayLink;
#[cfg(not(target_os = "macos"))]
pub use self::unsupported::DisplayLink;
#[cfg(not(target_os = "macos"))]
mod unsupported {
use std::convert::Infallible;
use super::{Callback, Error};
/// There is no display link on this platform.
pub struct DisplayLink(Infallible);
impl DisplayLink {
pub fn create_with_active_cg_displays() -> Option<DisplayLink> {
None
}
pub fn start(&self) -> Result<(), Error> {
match self.0 {}
}
pub fn stop(&self) -> Result<(), Error> {
match self.0 {}
}
pub fn is_running(&self) -> bool {
match self.0 {}
}
pub fn set_current_cg_display(&self, _display_id: u32) -> Result<(), Error> {
match self.0 {}
}
pub fn set_output_callback(&self, _callback: Callback) -> Result<(), Error> {
match self.0 {}
}
}
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code, deprecated)]
mod macos {
use std::{
ffi::c_void,
ptr::NonNull,
sync::{Mutex, PoisonError},
};
use objc2_core_foundation::CFRetained;
use objc2_core_video::{CVDisplayLink, CVOptionFlags, CVReturn, CVTimeStamp, kCVReturnSuccess};
use super::{Callback, Error};
/// A CVDisplayLink.
pub struct DisplayLink {
link: CFRetained<CVDisplayLink>,
/// The callback handed to CoreVideo as `user_info` (a leaked
/// `Box<Callback>`), freed when the link is dropped or the callback is
/// replaced.
callback: Mutex<Option<NonNull<Callback>>>,
}
// SAFETY: CVDisplayLink is a thread-safe CoreVideo object; the callback
// pointer is only freed with the lock held, after the link is stopped.
unsafe impl Send for DisplayLink {}
// SAFETY: see above.
unsafe impl Sync for DisplayLink {}
fn check(r: CVReturn) -> Result<(), Error> {
if r == kCVReturnSuccess {
Ok(())
} else {
Err(Error::InvalidOperation)
}
}
impl DisplayLink {
/// `createWithActiveCGDisplays`.
pub fn create_with_active_cg_displays() -> Option<DisplayLink> {
let mut out: *mut CVDisplayLink = std::ptr::null_mut();
// SAFETY: `out` is a valid out pointer.
let r =
unsafe { CVDisplayLink::create_with_active_cg_displays(NonNull::from(&mut out)) };
if r != kCVReturnSuccess {
return None;
}
let ptr = NonNull::new(out)?;
// SAFETY: the function returns a +1 retained link.
let link = unsafe { CFRetained::from_raw(ptr) };
Some(DisplayLink {
link,
callback: Mutex::new(None),
})
}
pub fn start(&self) -> Result<(), Error> {
check(self.link.start())
}
pub fn stop(&self) -> Result<(), Error> {
check(self.link.stop())
}
pub fn is_running(&self) -> bool {
self.link.is_running()
}
pub fn set_current_cg_display(&self, display_id: u32) -> Result<(), Error> {
check(self.link.set_current_cg_display(display_id))
}
/// Set the callback called on every vsync.
///
/// Note: this purposely throws away a ton of arguments we don't need.
pub fn set_output_callback(&self, callback: Callback) -> Result<(), Error> {
let boxed = NonNull::from(Box::leak(Box::new(callback)));
// SAFETY: `trampoline` matches the callback signature and
// `user_info` stays valid until the link is dropped.
let r = unsafe {
self.link
.set_output_callback(Some(trampoline), boxed.as_ptr().cast::<c_void>())
};
if let Err(e) = check(r) {
// SAFETY: we just leaked this box and CoreVideo rejected it.
drop(unsafe { Box::from_raw(boxed.as_ptr()) });
return Err(e);
}
let old = self
.callback
.lock()
.unwrap_or_else(PoisonError::into_inner)
.replace(boxed);
if let Some(old) = old {
// Make sure the old callback can't be running anymore.
let _ = self.link.stop();
// SAFETY: the pointer came from `Box::leak` above and the
// link no longer references it (replaced + stopped).
drop(unsafe { Box::from_raw(old.as_ptr()) });
}
Ok(())
}
}
impl Drop for DisplayLink {
fn drop(&mut self) {
let _ = self.link.stop();
if let Some(cb) = self
.callback
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.take()
{
// SAFETY: the link is stopped and released right after.
drop(unsafe { Box::from_raw(cb.as_ptr()) });
}
}
}
unsafe extern "C-unwind" fn trampoline(
_display_link: NonNull<CVDisplayLink>,
_in_now: NonNull<CVTimeStamp>,
_in_output_time: NonNull<CVTimeStamp>,
_flags_in: CVOptionFlags,
_flags_out: NonNull<CVOptionFlags>,
user_info: *mut c_void,
) -> CVReturn {
// SAFETY: `user_info` is the leaked `Box<Callback>` set in
// `set_output_callback`, alive until the link is stopped.
if let Some(cb) = unsafe { user_info.cast::<Callback>().as_ref() } {
cb();
}
kCVReturnSuccess
}
}