Skip to main content

lumen_core/
tick.rs

1//! Main-world tick stages and the [`Tick`] resource.
2//!
3//! - Each stage is a `bevy_ecs` [`SystemSet`].
4//! - Ordering is enforced by `.chain()` in [`crate::app::App::new`].
5//! - The render schedule runs after the main schedule and the extract step; see [`crate::render_world`].
6
7use crate::plugin_events::plugin_events_pending;
8use crate::property_store::external_properties_pending;
9use crate::render_world::{AnimationsActive, FrameDirty};
10use crate::time::{Duration, Instant};
11use bevy_ecs::prelude::*;
12
13/// The five ordered main-world stages of a Lumen tick.
14#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
15pub enum TickStage {
16    /// Ingests OS events (keyboard, mouse, IME, window). Window backend writes here.
17    Input,
18    /// Drains the bounded [`crate::command::CommandQueue`] and applies deferred mutations.
19    CommandDrain,
20    /// Runs application systems: state mutation, animations, scripts.
21    Systems,
22    /// Runs the layout engine: dirty flush, taffy round-trip, absolute-coord write-back.
23    LayoutSync,
24    /// Computes the accessibility-tree diff and pushes it to the OS.
25    A11ySync,
26}
27
28/// Per-tick frame clock resource.
29///
30/// - [`Self::now`] is captured at the start of each [`crate::app::App::tick`] before the [`TickStage::Input`] systems run.
31/// - [`Self::dt`] is `now - previous_now` (zero on the first tick).
32/// - [`Self::frame`] is a monotonic counter incremented once per tick (starts at 0; reaches 1 on the first tick).
33///
34/// Wave 1 migrates the animation primitives off [`Instant::now`] to read this resource so headless tests can
35/// drive deterministic frame clocks; foundation only installs and updates the resource.
36#[derive(Resource, Clone, Copy, Debug)]
37pub struct Tick {
38    /// Wall-clock instant captured at the start of the current tick.
39    pub now: Instant,
40    /// Elapsed time since the previous tick's [`Self::now`]. Zero on the first tick.
41    pub dt: Duration,
42    /// Monotonic tick counter; 0 before the first tick, 1 after, and so on.
43    pub frame: u64,
44}
45
46impl Default for Tick {
47    fn default() -> Self {
48        Self {
49            now: Instant::now(),
50            dt: Duration::ZERO,
51            frame: 0,
52        }
53    }
54}
55
56impl Tick {
57    /// Advances the clock by capturing a fresh `Instant::now()` and bumping [`Self::frame`].
58    /// Called by [`crate::app::App::tick`] at the top of each tick, before the main schedule runs.
59    pub fn advance(&mut self) {
60        let now = Instant::now();
61        self.dt = now.saturating_duration_since(self.now);
62        self.now = now;
63        self.frame = self.frame.wrapping_add(1);
64    }
65}
66
67/// Whether the tick that just ran left work behind, so a driver that only
68/// wakes on events has to schedule another frame.
69///
70/// Five sources, each of which reaches `false` on its own once the system
71/// settles, so a caller that loops on this can never spin forever:
72///
73/// 1. The external typed-property bus still holds undrained writes, from a
74///    cross-thread producer or a main-thread script write that landed after
75///    this tick's drain. It empties once drained.
76/// 2. The plugin-event bus still holds events a portable plugin pushed (see
77///    [`crate::plugin_events`]). It likewise empties once drained.
78/// 3. A driver reported unfinished work this tick through
79///    [`AnimationsActive`]: a hover or press tween, an opacity transition,
80///    scroll inertia, an element still waiting on content that has not
81///    arrived. The flag is cleared at the top of every tick and re-raised
82///    only while the thing it is keyed on is still outstanding, and every
83///    driver keys its claim on something that settles or leaves the tree.
84/// 4. [`FrameDirty`] is still set, which a system dirtying state after the
85///    encode leaves behind. The next present clears it.
86/// 5. A [`WakeDeadline`] requested this tick has already passed. A driver
87///    that reaches a deadline hands its work out on the tick that follows,
88///    so the claim settles with it.
89///
90/// This is a frame predicate, not a state predicate: an app with a permanent
91/// animation raises the third source forever. A caller that needs to know
92/// when an app's *state* stopped moving compares the state itself, as the
93/// prerenderer does.
94pub fn work_pending(world: &World) -> bool {
95    external_properties_pending()
96        || plugin_events_pending()
97        || world
98            .get_resource::<AnimationsActive>()
99            .is_some_and(|a| a.get())
100        || world.get_resource::<FrameDirty>().is_some_and(|f| f.dirty)
101        || wake_deadline(world).is_some_and(|at| at <= Instant::now())
102}
103
104/// The earliest instant a driver asked for another tick at, if any.
105///
106/// The companion of [`work_pending`] for work that is due later rather than
107/// now: a driver that parks on events alone sleeps until this instant (winit's
108/// `ControlFlow::WaitUntil`, a timed park) instead of polling, and ticks when
109/// it passes. `None` means nothing is scheduled and the loop may wait for an
110/// event indefinitely.
111pub fn wake_deadline(world: &World) -> Option<Instant> {
112    world
113        .get_resource::<WakeDeadline>()
114        .and_then(WakeDeadline::get)
115}
116
117/// Per-tick "tick again no later than this" request, the timed counterpart of
118/// [`AnimationsActive`].
119///
120/// A system whose work comes due at a known instant with nothing else to wake
121/// the loop (a script timer, a delayed transition) calls [`Self::request`]
122/// with that instant. Several requests keep the earliest. Like
123/// [`AnimationsActive`], [`reset_wake_deadline`] clears it at the top of every
124/// tick and each driver re-requests only while its deadline is still
125/// outstanding, so a loop that settles has nothing left to wake for.
126///
127/// Interior-mutable so drivers can request through a shared `Res`.
128#[derive(Resource, Debug, Default)]
129pub struct WakeDeadline(std::sync::Mutex<Option<Instant>>);
130
131impl WakeDeadline {
132    /// Ask for a tick no later than `at`. Keeps the earliest of all requests
133    /// made since the last [`Self::clear`].
134    pub fn request(&self, at: Instant) {
135        let mut slot = self.0.lock().unwrap_or_else(|p| p.into_inner());
136        *slot = Some(slot.map_or(at, |cur| cur.min(at)));
137    }
138
139    /// The earliest instant requested since the last [`Self::clear`].
140    pub fn get(&self) -> Option<Instant> {
141        *self.0.lock().unwrap_or_else(|p| p.into_inner())
142    }
143
144    /// Drop every request. Called by [`reset_wake_deadline`] at tick start.
145    pub fn clear(&self) {
146        *self.0.lock().unwrap_or_else(|p| p.into_inner()) = None;
147    }
148}
149
150/// Clears [`WakeDeadline`] at the top of every tick (registered in
151/// [`TickStage::Input`], chained before the `Systems` stage where drivers
152/// request), so a deadline nobody renews stops waking the loop.
153pub fn reset_wake_deadline(deadline: Res<WakeDeadline>) {
154    deadline.clear();
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn the_earliest_request_wins_until_cleared() {
163        let deadline = WakeDeadline::default();
164        assert_eq!(deadline.get(), None);
165        let now = Instant::now();
166        deadline.request(now + Duration::from_secs(5));
167        deadline.request(now + Duration::from_secs(2));
168        deadline.request(now + Duration::from_secs(9));
169        assert_eq!(deadline.get(), Some(now + Duration::from_secs(2)));
170        deadline.clear();
171        assert_eq!(deadline.get(), None);
172    }
173
174    #[test]
175    fn only_a_passed_deadline_is_pending_work() {
176        let mut world = World::new();
177        world.init_resource::<WakeDeadline>();
178        assert!(!work_pending(&world));
179        let later = Instant::now() + Duration::from_secs(60);
180        world.resource::<WakeDeadline>().request(later);
181        assert!(!work_pending(&world), "a future deadline is not due yet");
182        assert_eq!(wake_deadline(&world), Some(later));
183        world.resource::<WakeDeadline>().request(Instant::now());
184        assert!(work_pending(&world), "a passed deadline is due");
185    }
186}