use std::time::{Duration, Instant};
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
const RUN_GAP: Duration = Duration::from_millis(20);
const HELD_MAX: Duration = Duration::from_millis(50);
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
const IDLE_TICKS: u32 = 3;
#[cfg(target_os = "macos")]
mod link {
use super::IDLE_TICKS;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObject};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::NSView;
use objc2_foundation::{NSObjectProtocol, NSRunLoop, NSRunLoopCommonModes};
use objc2_quartz_core::CADisplayLink;
use std::cell::Cell;
use std::sync::Arc;
use winit::window::Window;
pub(super) struct State {
pub(super) wanted: Cell<bool>,
pub(super) fired: Cell<bool>,
idle: Cell<u32>,
window: Arc<Window>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "KuiFramePacer"]
#[ivars = State]
pub(super) struct Target;
impl Target {
#[unsafe(method(tick:))]
fn tick(&self, link: &CADisplayLink) {
let s = self.ivars();
if s.wanted.take() {
s.idle.set(0);
s.fired.set(true);
s.window.request_redraw();
} else {
let idle = s.idle.get() + 1;
s.idle.set(idle);
if idle >= IDLE_TICKS {
link.setPaused(true);
}
}
}
}
unsafe impl NSObjectProtocol for Target {}
);
pub(super) struct Link {
pub(super) target: Retained<Target>,
link: Retained<CADisplayLink>,
}
impl Link {
pub(super) fn new(window: &Arc<Window>) -> Option<Link> {
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
let mtm = MainThreadMarker::new()?;
let RawWindowHandle::AppKit(h) = window.window_handle().ok()?.as_raw() else {
return None;
};
let view = unsafe { Retained::retain(h.ns_view.as_ptr().cast::<NSView>()) }?;
let responds: bool = unsafe {
msg_send![&*view, respondsToSelector: sel!(displayLinkWithTarget:selector:)]
};
if !responds {
return None;
}
let target = Target::alloc(mtm).set_ivars(State {
wanted: Cell::new(false),
fired: Cell::new(false),
idle: Cell::new(0),
window: window.clone(),
});
let target: Retained<Target> = unsafe { msg_send![super(target), init] };
let link = unsafe {
view.displayLinkWithTarget_selector(
&*(Retained::as_ptr(&target) as *const AnyObject),
sel!(tick:),
)
};
link.setPaused(true);
unsafe { link.addToRunLoop_forMode(&NSRunLoop::mainRunLoop(), NSRunLoopCommonModes) };
Some(Link { target, link })
}
pub(super) fn state(&self) -> &State {
self.target.ivars()
}
pub(super) fn run(&self) {
self.state().idle.set(0);
self.link.setPaused(false);
}
}
impl Drop for Link {
fn drop(&mut self) {
self.link.invalidate();
}
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn in_run(
last_present: Option<Instant>,
last_size: (u32, u32),
now: Instant,
size: (u32, u32),
) -> bool {
size == last_size && last_present.is_some_and(|at| now.duration_since(at) < RUN_GAP)
}
pub(crate) struct Pacer {
#[cfg(target_os = "macos")]
link: Option<link::Link>,
last_present: Option<Instant>,
last_size: (u32, u32),
held_since: Option<Instant>,
}
impl Pacer {
pub(crate) fn new(window: &std::sync::Arc<winit::window::Window>, run_loop: bool) -> Pacer {
let wanted =
run_loop && std::env::var("KUI_FRAME_PACING").map_or(true, |v| v.trim() != "0");
#[cfg(not(target_os = "macos"))]
let _ = (window, wanted);
Pacer {
#[cfg(target_os = "macos")]
link: if wanted {
link::Link::new(window)
} else {
None
},
last_present: None,
last_size: (0, 0),
held_since: None,
}
}
pub(crate) fn admit(&mut self, now: Instant, size: (u32, u32)) -> bool {
#[cfg(target_os = "macos")]
if let Some(link) = &self.link {
let s = link.state();
if s.fired.take() {
self.held_since = None;
return true;
}
let running = in_run(self.last_present, self.last_size, now, size);
if running {
s.wanted.set(true);
self.held_since.get_or_insert(now);
link.run();
return false;
}
}
let _ = (now, size);
self.held_since = None;
true
}
pub(crate) fn holding(&self) -> bool {
self.held_since.is_some()
}
pub(crate) fn presented(&mut self, now: Instant, size: (u32, u32)) {
self.last_present = Some(now);
self.last_size = size;
}
pub(crate) fn overdue(&mut self, now: Instant) -> Option<(Instant, bool)> {
let since = self.held_since?;
let at = since + HELD_MAX;
if now >= at {
#[cfg(target_os = "macos")]
if let Some(link) = &self.link {
let s = link.state();
s.wanted.set(false);
s.fired.set(true);
}
self.held_since = None;
return Some((at, true));
}
Some((at, false))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_run_is_frames_close_together_at_one_size() {
let t0 = Instant::now();
let at = |ms| t0 + Duration::from_millis(ms);
assert!(in_run(Some(t0), (800, 600), at(8), (800, 600)));
assert!(
!in_run(Some(t0), (800, 600), at(25), (800, 600)),
"from idle"
);
assert!(
!in_run(Some(t0), (800, 600), at(8), (801, 600)),
"a live resize"
);
assert!(!in_run(None, (0, 0), t0, (800, 600)), "the first frame");
}
#[test]
fn a_held_frame_is_holding_until_it_is_overdue() {
let t0 = Instant::now();
let mut p = Pacer {
#[cfg(target_os = "macos")]
link: None,
last_present: Some(t0),
last_size: (800, 600),
held_since: None,
};
assert!(!p.holding());
p.held_since = Some(t0 + Duration::from_millis(2));
assert!(p.holding());
let (at, due) = p.overdue(t0 + Duration::from_millis(10)).unwrap();
assert!(!due && at == t0 + Duration::from_millis(2) + HELD_MAX);
assert!(p.holding(), "still held: the deadline is not up");
let (_, due) = p.overdue(t0 + Duration::from_millis(60)).unwrap();
assert!(due);
assert!(!p.holding(), "let go once it is drawn anyway");
}
#[test]
fn without_a_link_every_redraw_draws() {
let mut p = Pacer {
#[cfg(target_os = "macos")]
link: None,
last_present: None,
last_size: (0, 0),
held_since: None,
};
let t0 = Instant::now();
p.presented(t0, (800, 600));
assert!(p.admit(t0 + Duration::from_millis(4), (800, 600)));
assert!(p.overdue(t0 + Duration::from_secs(1)).is_none());
}
}