ferroui_ios/
display_link_timer.rs1use crate::interop::{kCFRunLoopCommonModes, CFRunLoopGetCurrent, CFRunLoopPerformBlock, CFRunLoopWakeUp};
7use crate::platform::Platform;
8use block2::RcBlock;
9use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
10use ferroui_base::rendering::{IRenderTimer, RenderTimerTick};
11use objc2::rc::{autoreleasepool, Retained};
12use objc2::{define_class, msg_send, sel, AllocAnyThread, DefinedClass, MainThreadMarker};
13use objc2_foundation::{
14 NSBundle, NSNotification, NSNotificationCenter, NSNotificationName, NSNumber, NSObject, NSObjectProtocol,
15 NSRunLoop, NSRunLoopCommonModes, NSString,
16};
17use objc2_quartz_core::{CADisplayLink, CAFrameRateRange};
18use objc2_ui_kit::{
19 UIApplicationDidEnterBackgroundNotification, UIApplicationWillEnterForegroundNotification, UIDevice, UIScreen,
20 UIUserInterfaceIdiom,
21};
22use std::cell::RefCell;
23use std::ffi::c_void;
24use std::ptr::NonNull;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
27use std::thread::Thread;
28use std::time::Instant;
29
30struct LinkState {
32 tick: Option<RenderTimerTick>,
33 in_background: bool,
34 paused: bool,
35 run_loop: Option<usize>,
37}
38
39struct TimerState {
41 lock: Mutex<LinkState>,
44 ticks: AtomicU64,
46 st: Instant,
47}
48
49impl TimerState {
50 fn lock(&self) -> MutexGuard<'_, LinkState> {
51 self.lock.lock().unwrap_or_else(PoisonError::into_inner)
52 }
53
54 fn on_link_tick(&self) {
55 self.ticks.fetch_add(1, Ordering::SeqCst);
56 let tick = {
58 let state = self.lock();
59 if state.paused {
60 None
61 } else {
62 state.tick.clone()
63 }
64 };
65 if let Some(tick) = tick {
66 tick(self.st.elapsed());
67 }
68 }
69}
70
71ferroui_base::ferro_thread_local! {
72 static LINK: RefCell<Option<Retained<CADisplayLink>>> = const { RefCell::new(None) };
74}
75
76fn update_link_state(this: &Arc<TimerState>, state: &mut LinkState) {
81 let paused = state.in_background || state.tick.is_none();
82 if state.paused != paused {
83 state.paused = paused;
84 if let Some(run_loop) = state.run_loop {
85 schedule_apply_link_state(this.clone(), run_loop);
86 }
87 }
88}
89
90fn schedule_apply_link_state(this: Arc<TimerState>, run_loop: usize) {
91 let block = RcBlock::new(move || apply_link_state(&this));
92 unsafe {
98 CFRunLoopPerformBlock(
99 run_loop as *mut c_void,
100 kCFRunLoopCommonModes,
101 &*block as *const block2::Block<dyn Fn()> as *const c_void,
102 );
103 CFRunLoopWakeUp(run_loop as *mut c_void);
104 }
105}
106
107fn apply_link_state(this: &TimerState) {
109 let paused = this.lock().paused;
110 LINK.with(|link| {
111 if let Some(link) = link.borrow().as_ref() {
112 link.setPaused(paused);
113 }
114 });
115}
116
117define_class!(
118 #[unsafe(super(NSObject))]
121 #[name = "FerroDisplayLinkTarget"]
122 #[ivars = Arc<TimerState>]
123 struct DisplayLinkTarget;
124
125 impl DisplayLinkTarget {
126 #[unsafe(method(onLinkTick:))]
127 fn on_link_tick(&self, _link: &CADisplayLink) {
128 autoreleasepool(|_| self.ivars().on_link_tick());
131 }
132 }
133
134 unsafe impl NSObjectProtocol for DisplayLinkTarget {}
135);
136
137impl DisplayLinkTarget {
138 fn new(state: Arc<TimerState>) -> Retained<Self> {
139 let this = Self::alloc().set_ivars(state);
140 unsafe { msg_send![super(this), init] }
143 }
144}
145
146pub struct DisplayLinkTimer {
149 state: Arc<TimerState>,
150 timer_thread: Thread,
151}
152
153impl DisplayLinkTimer {
154 pub fn new() -> Arc<DisplayLinkTimer> {
160 let state = Arc::new(TimerState {
161 lock: Mutex::new(LinkState { tick: None, in_background: false, paused: true, run_loop: None }),
162 ticks: AtomicU64::new(0),
163 st: Instant::now(),
164 });
165 let enable_high_refresh_rate = Self::enable_high_refresh_rate();
166
167 let thread_state = state.clone();
173 let spawned = std::thread::Builder::new().name("DisplayLinkTimer".to_string()).spawn(move || {
174 let target = DisplayLinkTarget::new(thread_state.clone());
175 unsafe {
181 let link = CADisplayLink::displayLinkWithTarget_selector(&target, sel!(onLinkTick:));
182 link.setPaused(true);
183 if enable_high_refresh_rate {
184 Self::set_preferred_frame_rate_range(&link);
185 }
186 let run_loop = NSRunLoop::currentRunLoop();
187 link.addToRunLoop_forMode(&run_loop, NSRunLoopCommonModes);
188 LINK.with(|slot| *slot.borrow_mut() = Some(link));
189 thread_state.lock().run_loop = Some(CFRunLoopGetCurrent() as usize);
192 apply_link_state(&thread_state);
193 run_loop.run();
194 }
195 });
196 let timer_thread = match spawned {
197 Ok(handle) => handle.thread().clone(),
198 Err(error) => panic!("Unable to start the thread of the render timer: {error}"),
199 };
200
201 let timer = Arc::new(DisplayLinkTimer { state, timer_thread });
202 if enable_high_refresh_rate {
203 timer.warn_if_limited_to_60_hz();
204 }
205
206 let (entered_background, will_enter_foreground) =
208 unsafe { (UIApplicationDidEnterBackgroundNotification, UIApplicationWillEnterForegroundNotification) };
209 Self::observe(entered_background, timer.state.clone(), true);
210 Self::observe(will_enter_foreground, timer.state.clone(), false);
211
212 timer
213 }
214
215 pub fn timer_thread(&self) -> &Thread {
218 &self.timer_thread
219 }
220
221 fn enable_high_refresh_rate() -> bool {
224 Platform::options().is_some_and(|options| options.enable_high_refresh_rate)
225 }
226
227 fn set_preferred_frame_rate_range(link: &CADisplayLink) {
233 if link.respondsToSelector(sel!(setPreferredFrameRateRange:)) {
234 link.setPreferredFrameRateRange(CAFrameRateRange { minimum: 60.0, maximum: 120.0, preferred: 120.0 });
235 }
236 }
237
238 fn warn_if_limited_to_60_hz(&self) {
241 let Some(mtm) = MainThreadMarker::new() else {
242 return;
243 };
244 if UIDevice::currentDevice(mtm).userInterfaceIdiom() != UIUserInterfaceIdiom::Phone {
245 return;
246 }
247 if UIScreen::mainScreen(mtm).maximumFramesPerSecond() <= 60 {
248 return;
249 }
250 let key = NSString::from_str("CADisableMinimumFrameDurationOnPhone");
251 let allowed = NSBundle::mainBundle()
252 .objectForInfoDictionaryKey(&key)
253 .and_then(|value| value.downcast::<NSNumber>().ok())
254 .is_some_and(|value| value.boolValue());
255 if !allowed {
256 if let Some(logger) = Logger::try_get(LogEventLevel::Warning, LogArea::IOS_PLATFORM) {
257 logger.log(
258 Some(self as &dyn std::any::Any),
259 "Rendering is limited to 60 Hz on this ProMotion display. Set CADisableMinimumFrameDurationOnPhone \
260 to true in Info.plist, or set IosPlatformOptions::enable_high_refresh_rate to false.",
261 );
262 }
263 }
264 }
265
266 fn set_in_background(state: &Arc<TimerState>, in_background: bool) {
267 let mut link_state = state.lock();
268 link_state.in_background = in_background;
269 update_link_state(state, &mut link_state);
270 }
271
272 fn observe(name: &NSNotificationName, state: Arc<TimerState>, in_background: bool) {
273 let block = RcBlock::new(move |_notification: NonNull<NSNotification>| {
274 Self::set_in_background(&state, in_background);
275 });
276 let observer = unsafe {
281 NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock(
282 Some(name),
283 None,
284 None,
285 &block,
286 )
287 };
288 std::mem::forget(observer);
291 }
292}
293
294impl DisplayLinkTimer {
295 pub fn ticks(&self) -> u64 {
298 self.state.ticks.load(Ordering::SeqCst)
299 }
300
301 pub fn has_tick(&self) -> bool {
303 self.state.lock().tick.is_some()
304 }
305}
306
307impl IRenderTimer for DisplayLinkTimer {
308 fn tick(&self) -> Option<RenderTimerTick> {
309 self.state.lock().tick.clone()
310 }
311
312 fn set_tick(&self, value: Option<RenderTimerTick>) {
313 let mut link_state = self.state.lock();
314 link_state.tick = value;
315 update_link_state(&self.state, &mut link_state);
316 }
317
318 fn runs_in_background(&self) -> bool {
319 true
320 }
321}