concinnity_engine/app/runloop.rs
1//! The shared render/event loop that drives a live `App`. Both the compiled
2//! `cn run` runtime (`run::start_runtime`) and the interpreted `cn debug` path
3//! (in the editor crate) pump the same loop; the only difference is the per-tick
4//! hook the debug path threads through to run its DebugHook. Keeping the platform
5//! event-pump and window-activation glue in one place means it is not duplicated
6//! per entry point.
7//!
8//! On macOS the world loop must pump the Cocoa run loop on the main thread each
9//! tick so AppKit (GLFW window creation, Metal pipeline compilation, event
10//! dispatch) can process its callbacks and Metal drawable presentation fires. On
11//! all other platforms a tight Rust loop is used, which is what the Vulkan /
12//! DirectX renderers expect.
13
14use crate::app::state::App;
15use crate::ecs::StepResult;
16
17/// Install the process CTRL+C handler that cancels the app's shutdown token, so
18/// the render loop exits cleanly. Panics if a handler is already installed; only
19/// one entry point installs it per process.
20pub fn install_ctrlc_handler(app: &App) {
21 let token = app.shutdown_token();
22 let installed = ctrlc::set_handler(move || {
23 tracing::info!("CTRL+C received, cancelling all subsystems");
24 token.cancel();
25 });
26 // A host that already owns the signal (an embedding application, or a
27 // second run in one process) keeps its handler; refusing to run over it
28 // is not a reason to abort.
29 if let Err(e) = installed {
30 tracing::warn!("CTRL+C handler not installed: {e}");
31 }
32}
33
34/// Activate NSApplication so AppKit windows can be displayed. Must be called
35/// before any NSWindow is created (i.e. before GraphicsSystem::init()).
36#[cfg(target_os = "macos")]
37pub fn activate_app_macos() {
38 use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};
39 let mtm = objc2::MainThreadMarker::new()
40 .expect("activate_app_macos must be called from the main thread");
41 let ns_app = NSApplication::sharedApplication(mtm);
42 ns_app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
43 ns_app.activate();
44}
45
46/// Drive the world loop to completion. Each iteration: exit if the shutdown token
47/// is cancelled; on macOS, when `pump_events` is set (a window is present), drain
48/// the pending AppKit/CoreFoundation events so the window stays responsive and
49/// Metal drawable callbacks fire; run the per-tick `on_tick` hook; then step the
50/// world, stopping on Stop/Done. `on_tick` is where the interpreted debug path
51/// ticks its DebugHook; the runtime passes a no-op.
52///
53/// `pump_events` is only meaningful on macOS (it gates the Cocoa pump): the
54/// caller sets it from whether the world actually renders, so a headless macOS
55/// world uses the same tight loop as every other platform.
56pub fn run_loop(app: &mut App, pump_events: bool, mut on_tick: impl FnMut(&mut App)) {
57 let shutdown = app.shutdown_token();
58
59 loop {
60 if shutdown.is_cancelled() {
61 tracing::info!("Shutdown token cancelled, exiting loop");
62 return;
63 }
64
65 #[cfg(target_os = "macos")]
66 if pump_events {
67 drain_cocoa_events();
68 }
69 #[cfg(not(target_os = "macos"))]
70 let _ = pump_events;
71
72 on_tick(app);
73
74 match app.world_step() {
75 StepResult::Continue => {}
76 StepResult::Stop | StepResult::Done => return,
77 }
78 }
79}
80
81// Drain all currently-pending Cocoa events without blocking.
82// CFRunLoopRunInMode is called with returnAfterSourceHandled=true so it returns
83// as soon as one source is handled (result == kCFRunLoopRunHandledSource == 4);
84// any other result means the queue is empty, so the drain stops and control
85// returns to the world step (or, pipelined, to the render half's wait).
86#[cfg(target_os = "macos")]
87pub(crate) fn drain_cocoa_events() {
88 use core_foundation::runloop::{CFRunLoopRunInMode, kCFRunLoopDefaultMode};
89 loop {
90 // SAFETY: `kCFRunLoopDefaultMode` is a framework-owned static mode
91 // name and this thread is the one that started the run loop.
92 let result = unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true as u8) };
93 if result != 4 {
94 break;
95 }
96 }
97}