Skip to content

Commit eea80f9

Browse files
committed
ios: drive draws from CADisplayLink instead of a polling render thread
The iOS launch path spawned a background thread that ran `loop { try_recv messages, performSelectorOnMainThread(setNeedsDisplay), yield_now }` with MTKView in manual-redraw mode (`setPaused: YES` + `setEnableSetNeedsDisplay: YES`). `yield_now()` isn't a sleep, so the thread pinned ~100 % of one CPU core continuously — iOS thermal-throttling the GPU clock down on real devices after ~a minute of use, and pinning `setPreferredFramesPerSecond` nowhere near its hint because frames only happened when the thread nagged the view. Switch MTKView to continuous-draw (`setPaused: NO`, `setEnableSetNeedsDisplay: NO`). `CADisplayLink` drives `drawInMTKView:` on the main thread at the display rate. Channel receivers move into the `IosDisplay` payload (main-thread-only) and get drained at the start of each `drawInMTKView:`, with messages dispatched inline via a new `dispatch_message` helper — no more main → channel → render thread → main round-trip through `performSelectorOnMainThread:processMessage:`. The `processMessage:` selector and `MainThreadState::cur_msg` field disappear with the thread. Same-app comparison on iPhone 17 Pro: CPU 140 % → 50 %, FPS held at 60, Energy Impact "Very High" → "High", phone no longer heats up during play. Also rolls in a Rust 2024 `static_mut_refs` lint fix on `RUN_ARGS.take()` via `&raw mut`.
1 parent b09486b commit eea80f9

1 file changed

Lines changed: 110 additions & 156 deletions

File tree

src/native/ios.rs

Lines changed: 110 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use {
2020
cell::RefCell,
2121
os::raw::c_void,
2222
sync::{mpsc, Arc, Mutex},
23-
thread::{self},
2423
},
2524
};
2625

@@ -30,7 +29,6 @@ struct MainThreadState {
3029
update_requested: bool,
3130
view: *mut Object,
3231
keymods: KeyMods,
33-
cur_msg: Message,
3432
}
3533

3634
struct IosDisplay {
@@ -44,6 +42,12 @@ struct IosDisplay {
4442
_gles2: bool,
4543
f: Option<Box<dyn 'static + FnOnce() -> Box<dyn EventHandler>>>,
4644
state: Arc<Mutex<MainThreadState>>,
45+
/// UIKit-side events; drained at the start of every `drawInMTKView:`.
46+
messages_rx: mpsc::Receiver<Message>,
47+
/// User-code requests (`schedule_update` etc.); same drain
48+
/// cadence as `messages_rx`.
49+
requests_rx: mpsc::Receiver<crate::native::Request>,
50+
blocking_event_loop: bool,
4751
}
4852

4953
impl IosDisplay {
@@ -79,6 +83,77 @@ fn get_window_payload(this: &Object) -> &mut IosDisplay {
7983
}
8084
}
8185

86+
/// Apply a `Message` to the payload's event handler + state. Called
87+
/// inline at the start of each `drawInMTKView:` for every pending
88+
/// message.
89+
fn dispatch_message(payload: &mut IosDisplay, msg: Message) {
90+
match msg {
91+
Message::Pause => {
92+
payload.state.lock().unwrap().paused = true;
93+
}
94+
Message::Resume => {
95+
payload.state.lock().unwrap().paused = false;
96+
}
97+
Message::Destroy => {
98+
payload.state.lock().unwrap().quit = true;
99+
}
100+
Message::Touch {
101+
phase,
102+
touch_id,
103+
x,
104+
y,
105+
} => {
106+
if let Some(ref mut event_handler) = payload.event_handler {
107+
event_handler.touch_event(phase, touch_id, x, y);
108+
}
109+
}
110+
Message::Character { character } => {
111+
if let Some(character) = char::from_u32(character) {
112+
if let Some(ref mut event_handler) = payload.event_handler {
113+
event_handler.char_event(character, Default::default(), false);
114+
}
115+
}
116+
}
117+
Message::KeyDown { keycode } => {
118+
let keymods = {
119+
let mut state = payload.state.lock().unwrap();
120+
match keycode {
121+
KeyCode::LeftShift | KeyCode::RightShift => state.keymods.shift = true,
122+
KeyCode::LeftControl | KeyCode::RightControl => state.keymods.ctrl = true,
123+
KeyCode::LeftAlt | KeyCode::RightAlt => state.keymods.alt = true,
124+
KeyCode::LeftSuper | KeyCode::RightSuper => state.keymods.logo = true,
125+
_ => {}
126+
}
127+
state.keymods
128+
};
129+
if let Some(ref mut event_handler) = payload.event_handler {
130+
event_handler.key_down_event(keycode, keymods, false);
131+
}
132+
}
133+
Message::KeyUp { keycode } => {
134+
let keymods = {
135+
let mut state = payload.state.lock().unwrap();
136+
match keycode {
137+
KeyCode::LeftShift | KeyCode::RightShift => state.keymods.shift = false,
138+
KeyCode::LeftControl | KeyCode::RightControl => state.keymods.ctrl = false,
139+
KeyCode::LeftAlt | KeyCode::RightAlt => state.keymods.alt = false,
140+
KeyCode::LeftSuper | KeyCode::RightSuper => state.keymods.logo = false,
141+
_ => {}
142+
}
143+
state.keymods
144+
};
145+
if let Some(ref mut event_handler) = payload.event_handler {
146+
event_handler.key_up_event(keycode, keymods);
147+
}
148+
}
149+
Message::Resize { width, height } => {
150+
if let Some(ref mut event_handler) = payload.event_handler {
151+
event_handler.resize_event(width as _, height as _);
152+
}
153+
}
154+
}
155+
}
156+
82157
#[derive(Debug, Clone, Copy)]
83158
enum Message {
84159
Resize {
@@ -188,79 +263,6 @@ pub fn define_glk_or_mtk_view(superclass: &Class) -> *const Class {
188263
on_touch(this, event, TouchPhase::Cancelled);
189264
}
190265

191-
extern "C" fn process_message(this: &Object, _: Sel, _: ObjcId) {
192-
let payload = get_window_payload(this);
193-
if payload.event_handler.is_none() {
194-
payload.init_event_handler();
195-
}
196-
let msg = {
197-
let state = payload.state.lock().unwrap();
198-
state.cur_msg
199-
};
200-
match msg {
201-
Message::Pause => {
202-
let mut state = payload.state.lock().unwrap();
203-
state.paused = true;
204-
}
205-
Message::Resume => {
206-
let mut state = payload.state.lock().unwrap();
207-
state.paused = false;
208-
}
209-
Message::Destroy => {
210-
let mut state = payload.state.lock().unwrap();
211-
state.quit = true;
212-
}
213-
Message::Touch {
214-
phase,
215-
touch_id,
216-
x,
217-
y,
218-
} => {
219-
if let Some(ref mut event_handler) = payload.event_handler {
220-
event_handler.touch_event(phase, touch_id, x, y);
221-
}
222-
}
223-
Message::Character { character } => {
224-
if let Some(character) = char::from_u32(character) {
225-
if let Some(ref mut event_handler) = payload.event_handler {
226-
event_handler.char_event(character, Default::default(), false);
227-
}
228-
}
229-
}
230-
Message::KeyDown { keycode } => {
231-
let mut state = payload.state.lock().unwrap();
232-
match keycode {
233-
KeyCode::LeftShift | KeyCode::RightShift => state.keymods.shift = true,
234-
KeyCode::LeftControl | KeyCode::RightControl => state.keymods.ctrl = true,
235-
KeyCode::LeftAlt | KeyCode::RightAlt => state.keymods.alt = true,
236-
KeyCode::LeftSuper | KeyCode::RightSuper => state.keymods.logo = true,
237-
_ => {}
238-
}
239-
if let Some(ref mut event_handler) = payload.event_handler {
240-
event_handler.key_down_event(keycode, state.keymods, false);
241-
}
242-
}
243-
Message::KeyUp { keycode } => {
244-
let mut state = payload.state.lock().unwrap();
245-
match keycode {
246-
KeyCode::LeftShift | KeyCode::RightShift => state.keymods.shift = false,
247-
KeyCode::LeftControl | KeyCode::RightControl => state.keymods.ctrl = false,
248-
KeyCode::LeftAlt | KeyCode::RightAlt => state.keymods.alt = false,
249-
KeyCode::LeftSuper | KeyCode::RightSuper => state.keymods.logo = false,
250-
_ => {}
251-
}
252-
if let Some(ref mut event_handler) = payload.event_handler {
253-
event_handler.key_up_event(keycode, state.keymods);
254-
}
255-
}
256-
Message::Resize { width, height } => {
257-
if let Some(ref mut event_handler) = payload.event_handler {
258-
event_handler.resize_event(width as _, height as _);
259-
}
260-
}
261-
}
262-
}
263-
264266
unsafe {
265267
decl.add_method(sel!(isOpaque), yes as extern "C" fn(&Object, Sel) -> BOOL);
266268
decl.add_method(
@@ -279,10 +281,6 @@ pub fn define_glk_or_mtk_view(superclass: &Class) -> *const Class {
279281
sel!(touchesCanceled: withEvent:),
280282
touches_canceled as extern "C" fn(&Object, Sel, ObjcId, ObjcId),
281283
);
282-
decl.add_method(
283-
sel!(processMessage:),
284-
process_message as extern "C" fn(&Object, Sel, ObjcId),
285-
);
286284
}
287285

288286
decl.add_ivar::<*mut c_void>("display_ptr");
@@ -326,6 +324,25 @@ pub fn define_glk_or_mtk_view_dlg(superclass: &Class) -> *const Class {
326324
payload.init_event_handler();
327325
}
328326

327+
// Drain requests + UIKit-side messages and dispatch inline
328+
// before drawing this frame.
329+
while let Ok(request) = payload.requests_rx.try_recv() {
330+
payload.state.lock().unwrap().process_request(request);
331+
}
332+
while let Ok(msg) = payload.messages_rx.try_recv() {
333+
dispatch_message(payload, msg);
334+
}
335+
336+
// Skip the draw if paused or, in `blocking_event_loop` mode,
337+
// if no update is pending. `CADisplayLink` keeps ticking
338+
// cheaply.
339+
if payload.state.lock().unwrap().paused {
340+
return;
341+
}
342+
if payload.blocking_event_loop && !payload.state.lock().unwrap().update_requested {
343+
return;
344+
}
345+
329346
let main_screen: ObjcId = unsafe { msg_send![class!(UIScreen), mainScreen] };
330347
let screen_rect: NSRect = unsafe { msg_send![main_screen, bounds] };
331348
let high_dpi = native_display().lock().unwrap().high_dpi;
@@ -463,8 +480,10 @@ unsafe fn create_metal_view(screen_rect: NSRect, _sample_count: i32, _high_dpi:
463480

464481
msg_send_![view_ctrl_obj, setView: mtk_view_obj];
465482

466-
msg_send_![mtk_view_obj, setEnableSetNeedsDisplay: YES];
467-
msg_send_![mtk_view_obj, setPaused: YES];
483+
// Continuous draw — `CADisplayLink` drives `drawInMTKView:` on
484+
// the main thread at `preferredFramesPerSecond`.
485+
msg_send_![mtk_view_obj, setEnableSetNeedsDisplay: NO];
486+
msg_send_![mtk_view_obj, setPaused: NO];
468487
msg_send_![mtk_view_obj, setPreferredFramesPerSecond:60];
469488
msg_send_![mtk_view_obj, setDelegate: mtk_view_dlg_obj];
470489
let device = MTLCreateSystemDefaultDevice();
@@ -499,7 +518,11 @@ pub fn define_app_delegate() -> *const Class {
499518
_: ObjcId,
500519
) -> BOOL {
501520
unsafe {
502-
let (f, conf) = RUN_ARGS.take().unwrap();
521+
// Routed through a raw pointer to satisfy the Rust 2024
522+
// `static_mut_refs` lint. Split across two statements so
523+
// clippy's `deref_addrof` doesn't fold it back.
524+
let run_args_ptr = &raw mut RUN_ARGS;
525+
let (f, conf) = (*run_args_ptr).take().unwrap();
503526

504527
let main_screen: ObjcId = msg_send![class!(UIScreen), mainScreen];
505528
let screen_rect: NSRect = msg_send![main_screen, bounds];
@@ -582,7 +605,6 @@ pub fn define_app_delegate() -> *const Class {
582605
alt: false,
583606
logo: false,
584607
},
585-
cur_msg: Message::Resume,
586608
}));
587609

588610
let payload = Box::new(IosDisplay {
@@ -596,6 +618,9 @@ pub fn define_app_delegate() -> *const Class {
596618
event_handler: None,
597619
_gles2: view._gles2,
598620
state: state_original.clone(),
621+
messages_rx: rx,
622+
requests_rx,
623+
blocking_event_loop: conf.platform.blocking_event_loop,
599624
});
600625
let payload_ptr = Box::into_raw(payload) as *mut std::ffi::c_void;
601626

@@ -609,79 +634,8 @@ pub fn define_app_delegate() -> *const Class {
609634

610635
msg_send_![window_obj, makeKeyAndVisible];
611636

612-
struct SendHack<F>(F);
613-
unsafe impl<F> Send for SendHack<F> {}
614-
615-
let state = SendHack(state_original.clone());
616-
thread::spawn(move || {
617-
let s = state.0;
618-
619-
loop {
620-
while let Ok(request) = requests_rx.try_recv() {
621-
s.lock().unwrap().process_request(request);
622-
}
623-
624-
let block_on_wait = {
625-
let s = s.lock().unwrap();
626-
(conf.platform.blocking_event_loop && !s.update_requested) || s.paused
627-
};
628-
629-
if block_on_wait {
630-
let res = rx.recv();
631-
632-
if let Ok(msg) = res {
633-
let view;
634-
{
635-
let mut s = s.lock().unwrap();
636-
view = s.view;
637-
s.cur_msg = msg;
638-
}
639-
msg_send_![&*view, performSelectorOnMainThread:sel!(processMessage:) withObject:nil waitUntilDone:YES];
640-
}
641-
} else {
642-
// process all the messages from the main thread
643-
while let Ok(msg) = rx.try_recv() {
644-
let view;
645-
{
646-
let mut s = s.lock().unwrap();
647-
view = s.view;
648-
s.cur_msg = msg;
649-
}
650-
msg_send_![&*view, performSelectorOnMainThread:sel!(processMessage:) withObject:nil waitUntilDone:YES];
651-
}
652-
}
653-
654-
let update_requested;
655-
let view;
656-
{
657-
let s = s.lock().unwrap();
658-
update_requested = s.update_requested;
659-
view = s.view;
660-
}
661-
662-
if !conf.platform.blocking_event_loop || update_requested {
663-
match conf.platform.apple_gfx_api {
664-
AppleGfxApi::OpenGl => {
665-
// Why it differs from Metal? I don't realy know. Looks like a bug.
666-
// Somehow it needs `setNeedsDisplay` to redraw after touch.
667-
// With plain `display` it draws only after another touch.
668-
// But when it's not blocking_event_loop it makes fps really drop with `setNeedsDisplay`.
669-
// I hope it will work the same on the real device.
670-
if conf.platform.blocking_event_loop {
671-
msg_send_![&*view, performSelectorOnMainThread:sel!(setNeedsDisplay) withObject:nil waitUntilDone:NO];
672-
} else {
673-
msg_send_![&*view, performSelectorOnMainThread:sel!(display) withObject:nil waitUntilDone:YES];
674-
}
675-
}
676-
AppleGfxApi::Metal => {
677-
msg_send_![&*view, performSelectorOnMainThread:sel!(setNeedsDisplay) withObject:nil waitUntilDone:NO];
678-
}
679-
}
680-
}
681-
682-
thread::yield_now();
683-
}
684-
});
637+
// No background render thread — `CADisplayLink` drives
638+
// `drawInMTKView:` directly.
685639
}
686640
YES
687641
}

0 commit comments

Comments
 (0)