1use std::panic::AssertUnwindSafe;
10use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
11use std::sync::{Mutex, OnceLock};
12use std::thread::ThreadId;
13use std::time::{Duration, Instant};
14
15use crate::lock_or_recover;
16
17type Job = Box<dyn FnOnce() + Send>;
18type TickFn = Box<dyn FnMut(f32) + Send>;
19
20static QUEUE: Mutex<Vec<Job>> = Mutex::new(Vec::new());
21static TICKERS: Mutex<Vec<(u64, TickFn)>> = Mutex::new(Vec::new());
22static REMOVED: Mutex<Vec<u64>> = Mutex::new(Vec::new());
24static NEXT_TICKER: AtomicU64 = AtomicU64::new(1);
25static RUNNING: AtomicUsize = AtomicUsize::new(0);
27static GAME_THREAD: OnceLock<ThreadId> = OnceLock::new();
28
29fn log(level: u8, message: &str) {
30 if crate::api::is_api_initialized() {
31 unsafe {
33 crate::ffi_dispatch::logging_log(level, message.as_ptr(), message.len() as u32);
34 }
35 }
36}
37
38pub fn is_game_thread() -> bool {
40 GAME_THREAD.get().is_some_and(|id| *id == std::thread::current().id())
41}
42
43pub fn run_on_game_thread(f: impl FnOnce() + Send + 'static) {
46 lock_or_recover(&QUEUE).push(Box::new(f));
47}
48
49pub fn spawn<T: Send + 'static>(
56 work: impl FnOnce() -> T + Send + 'static,
57 done: impl FnOnce(T) + Send + 'static,
58) {
59 RUNNING.fetch_add(1, Ordering::SeqCst);
60 let started = std::thread::Builder::new().name("rusteal-task".into()).spawn(move || {
61 let result = std::panic::catch_unwind(AssertUnwindSafe(work));
62 match result {
63 Ok(value) => run_on_game_thread(move || done(value)),
64 Err(_) => run_on_game_thread(|| {
65 log(crate::LOG_ERROR, "[Rusteal] task::spawn: the work panicked")
66 }),
67 }
68 RUNNING.fetch_sub(1, Ordering::SeqCst);
69 });
70 if started.is_err() {
71 RUNNING.fetch_sub(1, Ordering::SeqCst);
72 log(crate::LOG_ERROR, "[Rusteal] task::spawn: could not start a thread");
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79#[must_use = "keep the handle to remove the hook, or drop it to keep the hook forever"]
80pub struct TickHandle(u64);
81
82impl TickHandle {
83 pub fn remove(self) {
85 let mut tickers = lock_or_recover(&TICKERS);
86 let before = tickers.len();
87 tickers.retain(|(id, _)| *id != self.0);
88 if tickers.len() == before {
89 lock_or_recover(&REMOVED).push(self.0);
91 }
92 }
93}
94
95pub fn on_tick(f: impl FnMut(f32) + Send + 'static) -> TickHandle {
98 let id = NEXT_TICKER.fetch_add(1, Ordering::Relaxed);
99 lock_or_recover(&TICKERS).push((id, Box::new(f)));
100 TickHandle(id)
101}
102
103#[doc(hidden)]
106pub fn mark_game_thread() {
107 let _ = GAME_THREAD.set(std::thread::current().id());
108}
109
110#[doc(hidden)]
113pub fn tick(delta_seconds: f32) {
114 mark_game_thread();
115
116 let jobs = std::mem::take(&mut *lock_or_recover(&QUEUE));
117 for job in jobs {
118 if std::panic::catch_unwind(AssertUnwindSafe(job)).is_err() {
119 log(crate::LOG_ERROR, "[Rusteal] a closure run on the game thread panicked");
120 }
121 }
122
123 let mut running = std::mem::take(&mut *lock_or_recover(&TICKERS));
125 for (_, hook) in running.iter_mut() {
126 if std::panic::catch_unwind(AssertUnwindSafe(|| hook(delta_seconds))).is_err() {
127 log(crate::LOG_ERROR, "[Rusteal] a tick hook panicked");
128 }
129 }
130 let removed = std::mem::take(&mut *lock_or_recover(&REMOVED));
131 running.retain(|(id, _)| !removed.contains(id));
132 let mut tickers = lock_or_recover(&TICKERS);
133 running.append(&mut tickers);
134 *tickers = running;
135}
136
137#[doc(hidden)]
140pub fn shutdown() {
141 let started = Instant::now();
142 let mut warned = false;
143 while RUNNING.load(Ordering::SeqCst) > 0 {
144 if !warned && started.elapsed() > Duration::from_secs(1) {
145 warned = true;
146 log(crate::LOG_WARNING, &format!(
147 "[Rusteal] waiting for {} background task(s) before unloading",
148 RUNNING.load(Ordering::SeqCst)
149 ));
150 }
151 std::thread::sleep(Duration::from_millis(1));
152 }
153 lock_or_recover(&QUEUE).clear();
154 lock_or_recover(&TICKERS).clear();
155 lock_or_recover(&REMOVED).clear();
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::sync::Arc;
162
163 #[test]
165 fn work_comes_back_on_the_ticking_thread() {
166 let seen = Arc::new(Mutex::new(Vec::<String>::new()));
167
168 let log = seen.clone();
169 spawn(|| 6 * 7, move |answer| log.lock().unwrap().push(format!("done {answer}")));
170 let log = seen.clone();
171 let hook = on_tick(move |dt| log.lock().unwrap().push(format!("tick {dt}")));
172
173 let deadline = Instant::now() + Duration::from_secs(5);
174 while !seen.lock().unwrap().iter().any(|s| s == "done 42") {
175 assert!(Instant::now() < deadline, "the work never came back");
176 tick(0.5);
177 std::thread::sleep(Duration::from_millis(1));
178 }
179 assert!(is_game_thread());
180 assert!(seen.lock().unwrap().iter().any(|s| s == "tick 0.5"));
181
182 hook.remove();
183 seen.lock().unwrap().clear();
184 tick(0.25);
185 assert!(seen.lock().unwrap().is_empty(), "a removed hook ran");
186
187 let log = seen.clone();
188 run_on_game_thread(move || log.lock().unwrap().push("queued".into()));
189 shutdown();
190 tick(0.1);
191 assert!(seen.lock().unwrap().is_empty(), "shutdown kept the queue");
192 }
193}