461 lines · 17.6 KB · Rust
RawDownload
//! Frame pacing for macOS windows, built on `CVDisplayLink`.//!//! CVDisplayLink has no safe teardown: `CVDisplayLinkStop` merely flags the//! link's io thread to exit "soon" and returns immediately, and there is no//! way to learn when (or whether) a final output callback has finished. Two//! crash classes came from ignoring this://!//! * releasing the link after `stop` let the still-running io thread read the//!   freed link's internals (segfault in `CVHWTime::reset`, fixed by leaking//!   the link in #32116);//! * releasing the dispatch source that the output callback dereferenced via//!   its context pointer let a straggler callback read freed memory (segfault//!   in `dispatch_source_merge_data`, Sentry issue ZED-7XR).//!//! Instead of leaking one link and one source per teardown (and teardown//! happens on every resize tick, activation, and occlusion change), we keep a//! single immortal `CVDisplayLink` per display in a static registry and have//! windows subscribe to it://!//! * Links are created lazily and never released, so the release race cannot//!   occur. The output callback's context is the display id (an integer, not//!   a pointer), and the only memory the callback dereferences is the static//!   registry, so a straggler callback after `stop` is a harmless no-op.//! * Each window owns one dispatch source for the life of the window. On//!   window close it is removed from the registry under the lock (making it//!   unreachable from the callback), cancelled, and genuinely released.//! * A display's link runs iff it has subscribers; windows never start or//!   stop links directly, so interleaved starts/stops of windows sharing a//!   display cannot conflict.//!//! One tradeoff of immortal entries: a link created for a given//! `CGDirectDisplayID` is reused forever, including after the display is//! unplugged and one reappears with the same id, or after mode/refresh-rate//! changes. `CVDisplayLink` looks up display timing dynamically, so a cached//! link keeps pacing correctly; if that ever proves untrue the fix is to//! also refresh entries from a display-reconfiguration callback, not to//! release links (which would reintroduce the teardown race).//!//! Lock ordering: the output callback runs on the link's io thread and takes//! the registry lock, possibly while holding CVDisplayLink-internal locks. To//! avoid a lock cycle through those (undocumented) internals, we never call a//! CoreVideo function while holding the registry lock. Registry mutations and//! link start/stop happen on the main thread only, which keeps the `running`//! flag and the link's actual state consistent without holding the lock//! across the calls.//!//! `std::sync::Mutex` rather than `parking_lot` is deliberate: on macOS it is//! currently backed by `os_unfair_lock`, whose priority donation resolves//! inversions between the high-priority io thread and the main thread. (That//! is a std implementation detail, not a guarantee; if it changes, the cost//! is added latency under contention, not incorrectness.) use anyhow::Result;use core_graphics::display::CGDirectDisplayID;use dispatch2::{    _dispatch_source_type_data_add, DispatchObject, DispatchQueue, DispatchRetained, DispatchSource,};use gpui_util::ResultExt;use std::{    collections::{BTreeMap, btree_map},    ffi::c_void,    sync::{Mutex, MutexGuard, PoisonError},}; static REGISTRY: Mutex<Registry> = Mutex::new(Registry::new()); struct Registry {    displays: BTreeMap<CGDirectDisplayID, DisplayEntry>,    next_subscriber_id: u64,} impl Registry {    const fn new() -> Self {        Registry {            displays: BTreeMap::new(),            next_subscriber_id: 0,        }    }} struct DisplayEntry {    link: sys::DisplayLink,    running: bool,    subscribers: Vec<(SubscriberId, DispatchRetained<DispatchSource>)>,} // SAFETY: Both fields wrapping raw pointers are refcounted handles to// thread-safe objects that are valid on any thread: `sys::DisplayLink` to a// CoreVideo object, and each subscriber's `DispatchRetained<DispatchSource>`// to a GCD object (which the display's io thread really does use, calling// `merge_data` from the output callback). All mutation of the entry itself// is serialized by the registry lock.unsafe impl Send for DisplayEntry {} #[derive(Copy, Clone, PartialEq, Eq)]struct SubscriberId(u64); fn lock_registry() -> MutexGuard<'static, Registry> {    // Proceeding past poison is safe here (the map's invariants hold after    // any partial mutation), and panicking instead would abort the process    // when this is reached from the extern "C" output callback.    REGISTRY.lock().unwrap_or_else(PoisonError::into_inner)} fn debug_assert_main_thread() {    #[cfg(debug_assertions)]    {        use objc::{class, msg_send, sel, sel_impl};        let is_main_thread: objc::runtime::BOOL =            unsafe { msg_send![class!(NSThread), isMainThread] };        debug_assert!(            is_main_thread == objc::runtime::YES,            "display link registry mutations must happen on the main thread; \             the registry's lock ordering and state consistency depend on it"        );    }} unsafe extern "C" fn display_link_output_callback(    _display_link_out: *mut sys::CVDisplayLink,    _current_time: *const sys::CVTimeStamp,    _output_time: *const sys::CVTimeStamp,    _flags_in: i64,    _flags_out: *mut i64,    display_id: *mut c_void,) -> i32 {    let display_id = display_id as usize as CGDirectDisplayID;    let registry = lock_registry();    if let Some(entry) = registry.displays.get(&display_id) {        for (_, frame_requests) in &entry.subscribers {            frame_requests.merge_data(1);        }    }    0} fn subscribe(    display_id: CGDirectDisplayID,    frame_requests: DispatchRetained<DispatchSource>,) -> Result<SubscriberId> {    debug_assert_main_thread();     let needs_link = !lock_registry().displays.contains_key(&display_id);    let new_link = if needs_link {        // Created outside the registry lock; see the lock ordering note above.        Some(unsafe {            sys::DisplayLink::new(                display_id,                display_link_output_callback,                display_id as usize as *mut c_void,            )?        })    } else {        None    };     let (subscriber_id, link_to_start) = {        let mut registry = lock_registry();        let registry = &mut *registry;        let subscriber_id = SubscriberId(registry.next_subscriber_id);        registry.next_subscriber_id += 1;        let entry = match (registry.displays.entry(display_id), new_link) {            // If an entry appeared since the check above, dropping `new_link`            // is safe: a never-started link has no io thread, so releasing it            // doesn't race.            (btree_map::Entry::Occupied(entry), _) => entry.into_mut(),            (btree_map::Entry::Vacant(vacant), Some(link)) => vacant.insert(DisplayEntry {                link,                running: false,                subscribers: Vec::new(),            }),            (btree_map::Entry::Vacant(_), None) => {                // Entries are never removed, so an entry observed above cannot                // have disappeared while subscriptions stay on the main thread.                anyhow::bail!("display link registry entry vanished for display {display_id}");            }        };        entry.subscribers.push((subscriber_id, frame_requests));        let link_to_start = if entry.running {            None        } else {            entry.running = true;            // Clone the refcounted handle so the CVDisplayLinkStart call can            // happen after the lock is released.            Some(entry.link.clone())        };        (subscriber_id, link_to_start)    };     if let Some(mut link) = link_to_start {        if let Err(error) = unsafe { link.start() } {            let mut registry = lock_registry();            if let Some(entry) = registry.displays.get_mut(&display_id) {                entry.running = false;                entry.subscribers.retain(|(id, _)| *id != subscriber_id);            }            return Err(error);        }    }     Ok(subscriber_id)} fn unsubscribe(display_id: CGDirectDisplayID, subscriber_id: SubscriberId) {    debug_assert_main_thread();     let link_to_stop = {        let mut registry = lock_registry();        let Some(entry) = registry.displays.get_mut(&display_id) else {            return;        };        entry.subscribers.retain(|(id, _)| *id != subscriber_id);        if entry.subscribers.is_empty() && entry.running {            entry.running = false;            Some(entry.link.clone())        } else {            None        }    };     if let Some(mut link) = link_to_stop {        // A final output callback can still fire after this returns; it finds        // no subscribers for this display and does nothing.        unsafe { link.stop().log_err() };    }} /// A per-window source of frame requests, paced by the display the window is/// on. The wrapped dispatch source coalesces vsync ticks from the display's/// io thread and invokes `callback(data)` on the main queue.pub struct WindowFrameSource {    frame_requests: DispatchRetained<DispatchSource>,    registration: Option<(CGDirectDisplayID, SubscriberId)>,} impl WindowFrameSource {    pub fn new(data: *mut c_void, callback: extern "C" fn(*mut c_void)) -> Self {        let frame_requests = unsafe {            let frame_requests = DispatchSource::new(                &raw const _dispatch_source_type_data_add as *mut _,                0,                0,                Some(DispatchQueue::main()),            );            frame_requests.set_context(data);            frame_requests.set_event_handler_f(callback);            // Resume before this source can ever be dropped: destroying a            // suspended dispatch source is undefined behavior (#50875).            frame_requests.resume();            frame_requests        };        Self {            frame_requests,            registration: None,        }    }     pub fn start(&mut self, display_id: CGDirectDisplayID) -> Result<()> {        self.stop();        let subscriber_id = subscribe(display_id, self.frame_requests.clone())?;        self.registration = Some((display_id, subscriber_id));        Ok(())    }     pub fn stop(&mut self) {        if let Some((display_id, subscriber_id)) = self.registration.take() {            unsubscribe(display_id, subscriber_id);        }    }} impl Drop for WindowFrameSource {    fn drop(&mut self) {        self.stop();        // Unsubscribing makes this source unreachable from the output        // callback, so unlike before (ZED-7XR) it is safe to actually release        // it. Cancelling first guarantees the event handler never runs again;        // its context points at the window's native view, which may be        // deallocated after this.        self.frame_requests.cancel();    }} mod sys {    //! Derived from display-link crate under the following license:    //! <https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT>    //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)    #![allow(dead_code, non_upper_case_globals)]     use anyhow::Result;    use core_graphics::display::CGDirectDisplayID;    use foreign_types::{ForeignType, foreign_type};    use std::{        ffi::c_void,        fmt::{self, Debug, Formatter},    };     #[derive(Debug)]    pub enum CVDisplayLink {}     foreign_type! {        pub unsafe type DisplayLink {            type CType = CVDisplayLink;            fn drop = CVDisplayLinkRelease;            fn clone = CVDisplayLinkRetain;        }    }     impl Debug for DisplayLink {        fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {            formatter                .debug_tuple("DisplayLink")                .field(&self.as_ptr())                .finish()        }    }     #[repr(C)]    #[derive(Clone, Copy)]    pub(crate) 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,    }     pub type CVTimeStampFlags = u64;     pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0;    pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1;    pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2;    pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3;    pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4;    pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16;    pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17;    pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags =        kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid;    pub const kCVTimeStampIsInterlaced: CVTimeStampFlags =        kCVTimeStampTopField | kCVTimeStampBottomField;     #[repr(C)]    #[derive(Clone, Copy, Default)]    pub(crate) struct CVSMPTETime {        pub subframes: i16,        pub subframe_divisor: i16,        pub counter: u32,        pub time_type: u32,        pub flags: u32,        pub hours: i16,        pub minutes: i16,        pub seconds: i16,        pub frames: i16,    }     pub type CVSMPTETimeType = u32;     pub const kCVSMPTETimeType24: CVSMPTETimeType = 0;    pub const kCVSMPTETimeType25: CVSMPTETimeType = 1;    pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2;    pub const kCVSMPTETimeType30: CVSMPTETimeType = 3;    pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4;    pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5;    pub const kCVSMPTETimeType60: CVSMPTETimeType = 6;    pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7;     pub type CVSMPTETimeFlags = u32;     pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0;    pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1;     pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn(        display_link_out: *mut CVDisplayLink,        // A pointer to the current timestamp. This represents the timestamp when the callback is called.        current_time: *const CVTimeStamp,        // A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed.        output_time: *const CVTimeStamp,        // Unused        flags_in: i64,        // Unused        flags_out: *mut i64,        // A pointer to app-defined data.        display_link_context: *mut c_void,    ) -> i32;     #[link(name = "CoreFoundation", kind = "framework")]    #[link(name = "CoreVideo", kind = "framework")]    #[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)]    unsafe extern "C" {        pub fn CVDisplayLinkCreateWithActiveCGDisplays(            display_link_out: *mut *mut CVDisplayLink,        ) -> i32;        pub fn CVDisplayLinkSetCurrentCGDisplay(            display_link: &mut DisplayLinkRef,            display_id: u32,        ) -> i32;        pub fn CVDisplayLinkSetOutputCallback(            display_link: &mut DisplayLinkRef,            callback: CVDisplayLinkOutputCallback,            user_info: *mut c_void,        ) -> i32;        pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32;        pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32;        pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink);        pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink;    }     impl DisplayLink {        /// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc)        pub unsafe fn new(            display_id: CGDirectDisplayID,            callback: CVDisplayLinkOutputCallback,            user_info: *mut c_void,        ) -> Result<Self> {            unsafe {                let mut display_link: *mut CVDisplayLink = 0 as _;                 let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);                anyhow::ensure!(code == 0, "could not create display link, code: {}", code);                 let mut display_link = DisplayLink::from_ptr(display_link);                 let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info);                anyhow::ensure!(code == 0, "could not set output callback, code: {}", code);                 let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id);                anyhow::ensure!(                    code == 0,                    "could not assign display to display link, code: {}",                    code                );                 Ok(display_link)            }        }    }     impl DisplayLinkRef {        /// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc)        pub unsafe fn start(&mut self) -> Result<()> {            unsafe {                let code = CVDisplayLinkStart(self);                anyhow::ensure!(code == 0, "could not start display link, code: {}", code);                Ok(())            }        }         /// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc)        pub unsafe fn stop(&mut self) -> Result<()> {            unsafe {                let code = CVDisplayLinkStop(self);                anyhow::ensure!(code == 0, "could not stop display link, code: {}", code);                Ok(())            }        }    }}