Skip to main content

veloren_common/
clock.rs

1use common_base::span;
2use std::time::{Duration, Instant};
3use vek::Lerp;
4
5/// A type for maintaining consistent tick/frame pacing.
6pub struct Clock {
7    // Inputs
8    /// This is the dt that the Clock tries to archive with each call of tick.
9    target_dt: Duration,
10
11    // Working state
12    /// The amount of real time that has passed on the clock
13    real_time: Duration,
14    /// The amount of game time that has passed on the clock
15    game_time: Duration,
16    /// The last time the clock was ticked
17    last_tick: Instant,
18    /// The last time we started performing work
19    last_work: Instant,
20    /// The number of ticks that have elapsed so far
21    tick: u64,
22
23    /// The average time between ticks, seconds
24    average_dt: f64,
25    /// The average amount of time within each tick in which we're busy (i.e:
26    /// not sleeping)
27    average_busy: f64,
28    /// The average amount of variance between ticks
29    average_variance: f64,
30    /// The time that passed between the last tick, and the tick before it
31    last_real_dt: f64,
32    /// The dt to be used for the next game tick, in game time.
33    last_game_dt: f64,
34}
35
36pub struct ClockStats {
37    /// A weighted average of the recent 'busy period' (i.e: time spent doing
38    /// work rather than sleeping) per tick.
39    pub average_busy_dt: Duration,
40    /// A weighted average of the recent number of ticks per second.
41    pub average_tps: f64,
42    /// A weighted average of the variance of the clock relative to the average
43    /// TPS.
44    pub average_variance: Duration,
45}
46
47/// The weighting used to calculate averages. Must be > 0.0. 1.0 = no averaging.
48const SMOOTH_WEIGHT: f64 = 0.05;
49/// The proportion of the difference between real and game time that gets
50/// applied each tick to keep the two aligned.
51const NUDGE_RATE: f64 = 0.05;
52/// The maximum dt that the game should ever run at.
53const MAX_GAME_DT: f64 = 1.0 / 5.0;
54
55impl Clock {
56    pub fn new(target_dt: Duration) -> Self {
57        Self {
58            target_dt,
59
60            real_time: Duration::ZERO,
61            game_time: Duration::ZERO,
62            last_tick: Instant::now(),
63            last_work: Instant::now(),
64            tick: 0,
65
66            average_dt: target_dt.as_secs_f64(),
67            average_busy: target_dt.as_secs_f64(),
68            average_variance: 0.0,
69            last_real_dt: target_dt.as_secs_f64(),
70            last_game_dt: target_dt.as_secs_f64(),
71        }
72    }
73
74    pub fn set_target_dt(&mut self, target_dt: Duration) {
75        if target_dt != self.target_dt {
76            self.target_dt = target_dt;
77
78            // The target dt has changed, throw out the existing stats to avoid problems
79            self.average_dt = target_dt.as_secs_f64();
80            self.average_busy = target_dt.as_secs_f64();
81            self.average_variance = 0.0;
82        }
83    }
84
85    pub fn stats(&self) -> ClockStats {
86        ClockStats {
87            average_busy_dt: Duration::from_secs_f64(self.average_busy),
88            average_tps: 1.0 / self.average_dt.max(0.000001),
89            average_variance: Duration::from_secs_f64(self.average_variance),
90        }
91    }
92
93    pub fn real_dt(&self) -> Duration { Duration::from_secs_f64(self.last_real_dt) }
94
95    pub fn game_dt(&self) -> Duration { Duration::from_secs_f64(self.last_game_dt) }
96
97    pub fn tick(&mut self) {
98        span!(_guard, "tick", "Clock::tick");
99        span!(guard, "clock work");
100
101        // Give the tick thread realtime priority to minimise stuttering. Don't do this
102        // all the time to avoid upsetting the scheduler.
103        if self.tick == 0
104        /* .is_multiple_of(30) */
105        {
106            use thread_priority::*;
107            // // We choose scheduler parameters based on averages from previous frames
108            // // Try to target a tick period that's consistent with our current FPS (a low
109            // but // consistent framerate is a better outcome than one that's
110            // faster on paper but // is bouncing around all over the place).
111            // let stable_dt = self.average_busy
112            //     // Don't try to schedule for a tick rate that's higher than our target,
113            // even if we     // could achieve it.
114            //     .max(self.target_dt.as_secs_f64());
115            // let priority = ThreadPriority::Deadline {
116            //     runtime: Duration::from_secs_f64(self.average_busy * 0.5),
117            //     deadline: Duration::from_millis(10),
118            //     period: Duration::from_secs_f64(stable_dt),
119            //     flags: Default::default(),
120            // };
121            let priority =
122                ThreadPriority::Crossplatform(ThreadPriorityValue::try_from(90).unwrap());
123            _ = cfg_select! {
124                target_os = "linux" => std::thread::current().set_priority_and_policy(
125                    // ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Deadline),
126                    ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo),
127                    priority,
128                ),
129                _ => std::thread::current().set_priority(priority),
130            };
131        }
132
133        let this_tick = Instant::now();
134
135        // Calculate average metrics
136
137        let busy_time = self.last_work.elapsed();
138        self.average_busy = Lerp::lerp(self.average_busy, busy_time.as_secs_f64(), SMOOTH_WEIGHT);
139
140        let tick_time = (this_tick - self.last_tick).as_secs_f64();
141        self.average_dt = Lerp::lerp(self.average_dt, tick_time, SMOOTH_WEIGHT);
142
143        let variance = (tick_time - self.average_dt).abs();
144        self.average_variance = Lerp::lerp(self.average_variance, variance, SMOOTH_WEIGHT);
145
146        drop(guard);
147
148        // Sleep for any remaining time before the next tick
149        if let Some(sleep_dur) = self.target_dt.checked_sub(busy_time) {
150            spin_sleep::sleep(sleep_dur);
151        }
152
153        // Update clock state
154
155        self.last_tick = this_tick;
156        self.last_work = Instant::now();
157
158        // Progress real and game time
159        self.real_time += Duration::from_secs_f64(self.last_real_dt);
160        self.game_time += Duration::from_secs_f64(self.last_game_dt);
161
162        // Calculate the deltas for both real and game clocks. The real clock is
163        // absolute: we can't alter the progression of time. However, we can
164        // alter the game clock and nudge it toward real time. The reason we
165        // don't want to keep the two *exactly* in time is that a lag spike on a
166        // single tick would cause a corresponding jump in dt on the next tick, which
167        // might produce strange results for any dt-dependent gameplay systems.
168        // Instead, we gradually nudge the game time back toward real time over
169        // several ticks.
170        self.last_real_dt = tick_time;
171        self.last_game_dt = (self.average_dt
172            + (self.real_time.as_secs_f64() - self.game_time.as_secs_f64()) * NUDGE_RATE)
173            .min(MAX_GAME_DT);
174
175        self.tick += 1;
176    }
177}