#[cfg(not(alloc_frugal))]
use crate::compat::Condvar;
use crate::compat::HashMap;
use crate::compat::Instant;
use crate::compat::Mutex;
use crate::compat::MutexGuard;
use crate::compat::OnceLock;
use crate::compat::{lock, Box, MiniToString, String, Vec};
use alloc::collections::VecDeque;
use alloc::sync::Arc;
use core::sync::atomic::{AtomicU64, Ordering};
use core::time::Duration;
pub(crate) const DEFAULT_EMBEDDED_TARGET_FPS: u32 = 60;
const MIN_EMBEDDED_TARGET_FPS: u32 = 1;
const MAX_EMBEDDED_TARGET_FPS: u32 = 240;
fn clamp_embedded_target_fps(fps: u32) -> u32 {
fps.clamp(MIN_EMBEDDED_TARGET_FPS, MAX_EMBEDDED_TARGET_FPS)
}
fn frame_interval_for_fps(fps: u32) -> Duration {
Duration::from_nanos(1_000_000_000 / fps as u64)
}
type EmbeddedTaskFn = Box<dyn FnOnce(u64) + Send + 'static>;
struct EmbeddedTask {
id: u64,
label: String,
action: Option<EmbeddedTaskFn>,
}
impl EmbeddedTask {
fn new(id: u64, label: String, action: EmbeddedTaskFn) -> Self {
Self { id, label, action: Some(action) }
}
fn run(mut self, frame_index: u64) {
if let Some(action) = self.action.take() {
action(frame_index);
}
}
}
#[cfg(not(alloc_frugal))]
fn run_task(task: EmbeddedTask, frame_index: u64) {
let id = task.id;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
task.run(frame_index);
}));
if let Err(_payload) = result {
log::error!("[embedded] task {id} panicked while running on frame {frame_index}; the remaining tasks of this frame still run");
}
}
#[cfg(alloc_frugal)]
fn run_task(task: EmbeddedTask, frame_index: u64) {
task.run(frame_index);
}
struct RunningGuard<'a> {
shared: &'a EmbeddedEngineShared,
}
impl Drop for RunningGuard<'_> {
fn drop(&mut self) {
self.shared.lock_state().running = false;
}
}
#[derive(Default)]
struct EmbeddedRuntimeState {
initialized: bool,
running: bool,
target_fps: u32,
windows: HashMap<u64, EmbeddedWindowRecord>,
buttons: HashMap<u64, EmbeddedButtonRecord>,
pending_tasks: VecDeque<EmbeddedTask>,
}
impl EmbeddedRuntimeState {
fn new() -> Self {
Self {
initialized: false,
running: false,
target_fps: DEFAULT_EMBEDDED_TARGET_FPS,
windows: HashMap::new(),
buttons: HashMap::new(),
pending_tasks: VecDeque::new(),
}
}
}
pub(crate) struct EmbeddedEngineShared {
next_widget_id: AtomicU64,
next_task_id: AtomicU64,
frame_count: AtomicU64,
state: Mutex<EmbeddedRuntimeState>,
#[cfg(not(alloc_frugal))]
wake_signal: Condvar,
}
impl EmbeddedEngineShared {
fn new() -> Self {
Self {
next_widget_id: AtomicU64::new(1),
next_task_id: AtomicU64::new(1),
frame_count: AtomicU64::new(0),
state: Mutex::new(EmbeddedRuntimeState::new()),
#[cfg(not(alloc_frugal))]
wake_signal: Condvar::new(),
}
}
fn lock_state(&self) -> MutexGuard<'_, EmbeddedRuntimeState> {
lock(&self.state)
}
fn set_target_fps(&self, fps: u32) -> u32 {
let mut state = self.lock_state();
state.target_fps = clamp_embedded_target_fps(fps);
#[cfg(not(alloc_frugal))]
self.wake_signal.notify_all();
state.target_fps
}
fn target_fps(&self) -> u32 {
self.lock_state().target_fps
}
pub(crate) fn init(&self) {
let mut state = self.lock_state();
if state.initialized {
return;
}
state.initialized = true;
}
#[cfg(not(alloc_frugal))]
pub(crate) fn run_loop(&self) {
{
let mut state = self.lock_state();
if state.running {
return;
}
state.running = true;
}
let _running_guard = RunningGuard { shared: self };
loop {
let frame_start = Instant::now();
let (tasks, target_fps, still_running) = {
let mut state = self.lock_state();
let still_running = state.running;
let target_fps = state.target_fps;
let tasks = state.pending_tasks.drain(..).collect::<Vec<_>>();
(tasks, target_fps, still_running)
};
if !still_running {
break;
}
let frame_index = self.frame_count.fetch_add(1, Ordering::SeqCst) + 1;
for task in tasks {
run_task(task, frame_index);
}
let frame_interval = frame_interval_for_fps(clamp_embedded_target_fps(target_fps));
let elapsed = frame_start.elapsed();
if elapsed < frame_interval {
let wait_duration = frame_interval - elapsed;
let state = self.lock_state();
if !state.running {
break;
}
let _ = self
.wake_signal
.wait_timeout(state, wait_duration)
.unwrap_or_else(|poisoned| poisoned.into_inner());
}
}
}
#[cfg(alloc_frugal)]
pub(crate) fn run_loop(&self) {
{
let mut state = self.lock_state();
if state.running {
return;
}
state.running = true;
}
let _running_guard = RunningGuard { shared: self };
loop {
let frame_start = Instant::now();
let (tasks, target_fps, still_running) = {
let mut state = self.lock_state();
let still_running = state.running;
let target_fps = state.target_fps;
let tasks = state.pending_tasks.drain(..).collect::<Vec<_>>();
(tasks, target_fps, still_running)
};
if !still_running {
break;
}
let frame_index = self.frame_count.fetch_add(1, Ordering::SeqCst) + 1;
for task in tasks {
run_task(task, frame_index);
}
let frame_interval = frame_interval_for_fps(clamp_embedded_target_fps(target_fps));
while frame_start.elapsed() < frame_interval {
if !self.lock_state().running {
return;
}
core::hint::spin_loop();
}
}
}
pub(crate) fn _destroy_window(&self, window_id: u64) {
let mut state = self.lock_state();
state.windows.remove(&window_id);
}
pub(crate) fn _destroy_button(&self, button_id: u64) {
let mut state = self.lock_state();
state.buttons.remove(&button_id);
}
pub(crate) fn quit(&self) {
let cancelled = {
let mut state = self.lock_state();
state.running = false;
state.windows.clear();
state.buttons.clear();
core::mem::take(&mut state.pending_tasks)
};
drop(cancelled);
#[cfg(not(alloc_frugal))]
self.wake_signal.notify_all();
}
fn alloc_widget_id(&self) -> u64 {
self.next_widget_id.fetch_add(1, Ordering::SeqCst)
}
pub(crate) fn register_window(
&self,
title: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> u64 {
let window_id = self.alloc_widget_id();
let mut state = self.lock_state();
state.windows.insert(
window_id,
EmbeddedWindowRecord { id: window_id, title: title.to_string(), x, y, width, height },
);
window_id
}
pub(crate) fn register_button(
&self,
parent: u64,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> u64 {
let button_id = self.alloc_widget_id();
let mut state = self.lock_state();
state.buttons.insert(
button_id,
EmbeddedButtonRecord {
id: button_id,
parent,
text: text.to_string(),
x,
y,
width,
height,
},
);
button_id
}
fn submit_task<F>(&self, label: String, action: F) -> u64
where
F: FnOnce(u64) + Send + 'static,
{
let task_id = self.next_task_id.fetch_add(1, Ordering::SeqCst);
let mut state = self.lock_state();
state.pending_tasks.push_back(EmbeddedTask::new(task_id, label, Box::new(action)));
drop(state);
#[cfg(not(alloc_frugal))]
self.wake_signal.notify_all();
task_id
}
fn stats(&self) -> EmbeddedEngineStats {
let state = self.lock_state();
EmbeddedEngineStats {
initialized: state.initialized,
running: state.running,
frame_count: self.frame_count.load(Ordering::SeqCst),
pending_task_count: state.pending_tasks.len(),
pending_tasks: state
.pending_tasks
.iter()
.map(|task| (task.id, task.label.clone()))
.collect(),
window_count: state.windows.len(),
button_count: state.buttons.len(),
target_fps: state.target_fps,
}
}
}
#[derive(Clone, Debug)]
pub struct EmbeddedWindowRecord {
pub id: u64,
pub title: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Debug)]
pub struct EmbeddedButtonRecord {
pub id: u64,
pub parent: u64,
pub text: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EmbeddedEngineStats {
pub initialized: bool,
pub running: bool,
pub frame_count: u64,
pub pending_task_count: usize,
pub pending_tasks: Vec<(u64, alloc::string::String)>,
pub window_count: usize,
pub button_count: usize,
pub target_fps: u32,
}
#[cfg(not(alloc_frugal))]
pub(crate) fn embedded_engine_shared() -> Arc<EmbeddedEngineShared> {
static SHARED: OnceLock<Arc<EmbeddedEngineShared>> = OnceLock::new();
SHARED.get_or_init(|| Arc::new(EmbeddedEngineShared::new())).clone()
}
#[cfg(alloc_frugal)]
pub(crate) fn embedded_engine_shared() -> Arc<EmbeddedEngineShared> {
static SHARED: OnceLock<Arc<EmbeddedEngineShared>> = OnceLock::new();
SHARED.get_or_init(|| Arc::new(EmbeddedEngineShared::new())).clone()
}
pub fn set_embedded_target_fps(fps: u32) -> u32 {
embedded_engine_shared().set_target_fps(fps)
}
pub fn embedded_target_fps() -> u32 {
embedded_engine_shared().target_fps()
}
pub fn submit_embedded_task<F>(label: impl Into<String>, action: F) -> u64
where
F: FnOnce(u64) + Send + 'static,
{
embedded_engine_shared().submit_task(label.into(), action)
}
pub fn embedded_engine_stats() -> EmbeddedEngineStats {
embedded_engine_shared().stats()
}
#[cfg(test)]
pub(crate) fn embedded_test_guard() -> crate::compat::MutexGuard<'static, ()> {
use crate::compat::{lock, Mutex, OnceLock};
static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
lock(GUARD.get_or_init(|| Mutex::new(())))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_guard() -> crate::compat::MutexGuard<'static, ()> {
embedded_test_guard()
}
#[test]
fn embedded_target_fps_clamps() {
let _guard = test_guard();
set_embedded_target_fps(DEFAULT_EMBEDDED_TARGET_FPS);
assert_eq!(set_embedded_target_fps(0), MIN_EMBEDDED_TARGET_FPS);
assert_eq!(set_embedded_target_fps(999), MAX_EMBEDDED_TARGET_FPS);
assert_eq!(set_embedded_target_fps(72), 72);
assert_eq!(embedded_target_fps(), 72);
set_embedded_target_fps(DEFAULT_EMBEDDED_TARGET_FPS);
}
#[test]
fn embedded_resource_registry_tracks_window_and_button() {
let _guard = test_guard();
let before = embedded_engine_stats();
let shared = embedded_engine_shared();
let window_id = shared.register_window("stats", 1, 2, 300, 200);
let _button_id = shared.register_button(window_id, "ok", 10, 10, 80, 24);
let after = embedded_engine_stats();
assert!(after.window_count > before.window_count);
assert!(after.button_count > before.button_count);
}
#[test]
fn a_queued_task_publishes_its_id_and_label() {
let _guard = test_guard();
let id = submit_embedded_task("publish-me", |_frame| {});
let stats = embedded_engine_stats();
assert!(
stats
.pending_tasks
.iter()
.any(|(task_id, label)| *task_id == id && label == "publish-me"),
"the queued task's id and label must be visible in the stats: {:?}",
stats.pending_tasks
);
embedded_engine_shared().quit();
}
#[cfg(not(alloc_frugal))]
#[test]
fn quit_does_not_drop_task_captures_under_the_lock() {
use core::time::Duration;
use std::sync::mpsc;
struct ReentrantProbe;
impl Drop for ReentrantProbe {
fn drop(&mut self) {
let _ = embedded_engine_stats();
}
}
let _guard = test_guard();
let probe = ReentrantProbe;
submit_embedded_task("reentrant-quit", move |_frame| {
let _ = &probe;
});
let (done_tx, done_rx) = mpsc::channel();
let worker = std::thread::spawn(move || {
embedded_engine_shared().quit();
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"quit must complete without dropping queued task captures under the state lock"
);
worker.join().expect("the quit worker must not panic");
assert_eq!(embedded_engine_stats().pending_task_count, 0, "quit cleared the queue");
}
#[cfg(not(alloc_frugal))]
#[test]
fn a_panicking_task_does_not_stop_the_frame_or_the_engine() {
use crate::render_engine::RenderEngine;
use core::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::mpsc;
let _guard = test_guard();
set_embedded_target_fps(120);
let after_ran = Arc::new(AtomicBool::new(false));
let after_ran_slot = Arc::clone(&after_ran);
submit_embedded_task("panicker", |_frame| {
panic!("deliberate panic from an embedded task");
});
let (tx, rx) = mpsc::channel();
submit_embedded_task("after", move |_frame| {
after_ran_slot.store(true, AtomicOrdering::SeqCst);
let _ = tx.send(());
});
let engine = crate::render_engine::EmbeddedRenderEngine::new();
let runner = engine.clone();
let handle = std::thread::spawn(move || runner.run());
assert!(
rx.recv_timeout(Duration::from_secs(2)).is_ok(),
"a task after a panicking task must still run in the same frame"
);
assert!(after_ran.load(AtomicOrdering::SeqCst));
engine.quit();
handle.join().expect("the embedded loop must join after a task panic");
set_embedded_target_fps(DEFAULT_EMBEDDED_TARGET_FPS);
set_embedded_target_fps(120);
let (tx2, rx2) = mpsc::channel();
submit_embedded_task("restart", move |_frame| {
let _ = tx2.send(());
});
let restart_engine = engine.clone();
let restart = std::thread::spawn(move || restart_engine.run());
assert!(
rx2.recv_timeout(Duration::from_secs(2)).is_ok(),
"the engine must be restartable after a task panic"
);
engine.quit();
restart.join().expect("the restarted loop must join");
set_embedded_target_fps(DEFAULT_EMBEDDED_TARGET_FPS);
}
}