Fixes ZED-7XR CVDisplayLink has no safe teardown: `CVDisplayLinkStop` only flags the io thread to exit "soon" and returns, `CVDisplayLinkRelease` frees the link immediately, and there is no way to learn when a final output callback has finished. This produced two distinct use-after-free crash classes on the display-link thread: the io thread reading the freed link's internals (`CVHWTime::reset`, worked around by leaking the link in #32116), and a straggler output callback dereferencing the freed dispatch source through its context pointer (`dispatch_source_merge_data`, ZED-7XR). Because we tore down a `DisplayLink` on every resize tick, window activation, and occlusion change, the existing workaround also leaked a stopped CVDisplayLink per teardown, unboundedly. This replaces per-window links with a static registry holding one immortal `CVDisplayLink` per display. Windows subscribe to their display's link with the dispatch source that paces their frames; the output callback's context is a display id rather than a pointer, so the only memory it ever dereferences is the registry itself, under its lock. Links start and stop at subscriber-count edges (so multiple windows on one display can't interleave starts/stops incorrectly), and a window's dispatch source lives for the life of the window, is removed from the registry on close, and is then genuinely released. Both crash classes become unreachable — the link because it is never released, the source because a straggler callback either finds it live in the registry or doesn't find it at all — and there are no leaks and no `mem::forget`. Since we never hold the registry lock across CoreVideo calls, no lock cycle through CVDisplayLink's undocumented internal locks is possible; the module doc comment spells out the invariants. This supersedes #60481, which fixed ZED-7XR by leaking the dispatch source on teardown. Release Notes: - Fixed a rare crash on macOS when closing, resizing, or switching between windows, and eliminated a memory leak in display-link teardown
462 lines
18 KiB
Rust
462 lines
18 KiB
Rust
//! 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(())
|
|
}
|
|
}
|
|
}
|
|
}
|