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}