massivly switch clock algorithm.

- before we had a Clock that tried to average multiple ticks and predict the next sleep.
   This system is massivly bugged.
   a) We know exactly how long the busy time took, so we dont need to predict anything in the first place
   b) Preduction was totally unrealistic after a single lag spike
   c) When a very slow tick happens, we dont benefit from 10 fast ticks.
 - Instead we just try to keep the tick time exact what we expect.
   If we can't manage a constant tick time because we are to slow, the systems have to "catch" this via the `dt` anyway.
This commit is contained in:
Marcel Märtens 2020-11-10 13:30:01 +01:00
parent a8ac051f33
commit e4e5c6e55b
17 changed files with 210 additions and 119 deletions

View File

@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Switched to a Whittaker map for better tree spawning patterns - Switched to a Whittaker map for better tree spawning patterns
- Switched to procedural snow cover on trees - Switched to procedural snow cover on trees
- Significantly improved terrain generation performance - Significantly improved terrain generation performance
- Significantly stabilized the game clock, to produce more "constant" TPS
### Removed ### Removed

24
Cargo.lock generated
View File

@ -4593,16 +4593,7 @@ checksum = "6f70411ba0d11c4aee042b1c73e68086322a293f8038c53ac95d36319c91ec19"
dependencies = [ dependencies = [
"tracing-core", "tracing-core",
"tracing-subscriber", "tracing-subscriber",
"tracy-client 0.9.0", "tracy-client",
]
[[package]]
name = "tracy-client"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650c280019cd1a841752d50f6a834216a2c8a810aeb18bdd48054be23cacd8a6"
dependencies = [
"tracy-client-sys 0.9.0",
] ]
[[package]] [[package]]
@ -4611,16 +4602,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb214203bffe8cefe9bc0dfc12140f85e8d59b42e1f52f696071fdf8595e066a" checksum = "eb214203bffe8cefe9bc0dfc12140f85e8d59b42e1f52f696071fdf8595e066a"
dependencies = [ dependencies = [
"tracy-client-sys 0.10.0", "tracy-client-sys",
]
[[package]]
name = "tracy-client-sys"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07e4636dad390858d247cf2633eaea90e67d7e21151cd476bbfd070d321279f"
dependencies = [
"cc",
] ]
[[package]] [[package]]
@ -4907,7 +4889,7 @@ dependencies = [
"specs-idvs", "specs-idvs",
"sum_type", "sum_type",
"tracing", "tracing",
"tracy-client 0.8.0", "tracy-client",
"vek 0.12.0", "vek 0.12.0",
] ]

View File

@ -26,7 +26,7 @@ fn main() {
info!("Starting chat-cli..."); info!("Starting chat-cli...");
// Set up an fps clock. // Set up an fps clock.
let mut clock = Clock::start(); let mut clock = Clock::new(Duration::from_millis(1000 / TPS));
println!("Enter your username"); println!("Enter your username");
let username = read_input(); let username = read_input();
@ -71,11 +71,7 @@ fn main() {
client.send_chat(msg) client.send_chat(msg)
} }
let events = match client.tick( let events = match client.tick(comp::ControllerInputs::default(), clock.dt(), |_| {}) {
comp::ControllerInputs::default(),
clock.get_last_delta(),
|_| {},
) {
Ok(events) => events, Ok(events) => events,
Err(err) => { Err(err) => {
error!("Error: {:?}", err); error!("Error: {:?}", err);
@ -104,6 +100,6 @@ fn main() {
client.cleanup(); client.cleanup();
// Wait for the next tick. // Wait for the next tick.
clock.tick(Duration::from_millis(1000 / TPS)); clock.tick();
} }
} }

View File

@ -42,7 +42,7 @@ num-traits = "0.2"
num-derive = "0.3" num-derive = "0.3"
# Tracy # Tracy
tracy-client = { version = "0.8.0", optional = true } tracy-client = { version = "0.9.0", optional = true }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"

View File

@ -1,63 +1,170 @@
use crate::span; use crate::span;
use std::{ use std::{
collections::VecDeque,
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
const CLOCK_SMOOTHING: f64 = 0.9; /// This Clock tries to make this tick a constant time by sleeping the rest of
/// the tick
/// - if we actually took less time than we planned: sleep and return planned
/// time
/// - if we ran behind: don't sleep and return actual time
/// We DON'T do any fancy averaging of the deltas for tick for 2 reasons:
/// - all Systems have to work based on `dt` and we cannot assume that this is
/// const through all ticks
/// - when we have a slow tick, a lag, it doesn't help that we have 10 fast
/// ticks directly afterwards
/// We return a smoothed version for display only!
pub struct Clock { pub struct Clock {
/// This is the dt that the Clock tries to archive with each call of tick.
target_dt: Duration,
/// last time `tick` was called
last_sys_time: Instant, last_sys_time: Instant,
last_delta: Option<Duration>, /// will be calculated in `tick` returns the dt used by the next iteration
running_average_delta: f64, /// of the main loop
compensation: f64, last_dt: Duration,
/// summed up `last_dt`
total_tick_time: Duration,
// Stats only
// stored as millis in u16 to save space. if it's more than u16::MAX (16s) we have other
// problems
last_dts_millis: VecDeque<u16>,
last_dts_millis_sorted: Vec<u16>,
stats: ClockStats,
} }
pub struct ClockStats {
/// busy_dt is the part of the last tick that we didn't sleep.
/// e.g. the total tick is 33ms, including 25ms sleeping. then this returns
/// 8ms
pub last_busy_dt: Duration,
/// avg over the last NUMBER_OF_OLD_DELTAS_KEPT ticks
pub average_tps: f64,
/// = 50% percentile
pub median_tps: f64,
/// lowest 10% of the frames
pub percentile_90_tps: f64,
/// lowest 5% of the frames
pub percentile_95_tps: f64,
/// lowest 1% of the frames
pub percentile_99_tps: f64,
}
const NUMBER_OF_OLD_DELTAS_KEPT: usize = 100;
impl Clock { impl Clock {
pub fn start() -> Self { pub fn new(target_dt: Duration) -> Self {
Self { Self {
target_dt,
last_sys_time: Instant::now(), last_sys_time: Instant::now(),
last_delta: None, last_dt: target_dt,
running_average_delta: 0.0, total_tick_time: Duration::default(),
compensation: 1.0, last_dts_millis: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
last_dts_millis_sorted: Vec::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
stats: ClockStats::new(&Vec::new(), target_dt),
} }
} }
pub fn get_tps(&self) -> f64 { 1.0 / self.running_average_delta } pub fn set_target_dt(&mut self, target_dt: Duration) { self.target_dt = target_dt; }
pub fn get_last_delta(&self) -> Duration { pub fn stats(&self) -> &ClockStats { &self.stats }
self.last_delta.unwrap_or_else(|| Duration::new(0, 0))
}
pub fn get_avg_delta(&self) -> Duration { Duration::from_secs_f64(self.running_average_delta) } pub fn dt(&self) -> Duration { self.last_dt }
pub fn tick(&mut self, tgt: Duration) { /// Do not modify without asking @xMAC94x first!
pub fn tick(&mut self) {
span!(_guard, "tick", "Clock::tick"); span!(_guard, "tick", "Clock::tick");
let delta = Instant::now().duration_since(self.last_sys_time); let current_sys_time = Instant::now();
let busy_delta = current_sys_time.duration_since(self.last_sys_time);
// Maintain TPS
self.last_dts_millis_sorted = self.last_dts_millis.iter().copied().collect();
self.last_dts_millis_sorted.sort_unstable();
self.stats = ClockStats::new(&self.last_dts_millis_sorted, busy_delta);
// Attempt to sleep to fill the gap. // Attempt to sleep to fill the gap.
if let Some(sleep_dur) = tgt.checked_sub(delta) { if let Some(sleep_dur) = self.target_dt.checked_sub(busy_delta) {
if self.running_average_delta != 0.0 { Clock::sleep(current_sys_time, sleep_dur)
self.compensation =
(self.compensation + (tgt.as_secs_f64() / self.running_average_delta) - 1.0)
.max(0.0)
} }
let sleep_secs = sleep_dur.as_secs_f64() * self.compensation; let after_sleep_sys_time = Instant::now();
if sleep_secs > 0.0 { self.last_dt = after_sleep_sys_time.duration_since(self.last_sys_time);
thread::sleep(Duration::from_secs_f64(sleep_secs)); if self.last_dts_millis.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
self.last_dts_millis.pop_front();
} }
self.last_dts_millis
.push_back((self.last_dt.as_millis() as u16).min(std::u16::MAX));
self.total_tick_time += self.last_dt;
self.last_sys_time = after_sleep_sys_time;
} }
let delta = Instant::now().duration_since(self.last_sys_time); /// try to do a high precision sleep.
/// When this `fn` returns it SHOULD be before+dur as accurate is possible.
/// Returns the after sleep Instant.
fn sleep(before: Instant, dur: Duration) {
const BUSY_WAITING_TRIGGER_DUR: Duration = Duration::from_millis(2);
#[cfg(not(windows))]
const BUSY_WAITING_TIME: Duration = Duration::from_nanos(125_000);
#[cfg(windows)]
const BUSY_WAITING_TIME: Duration = Duration::from_nanos(500_000);
self.last_sys_time = Instant::now(); // If we have more than BUSY_TRIGGER_DUR on our sleep bucket, do sleep for
self.last_delta = Some(delta); // (1-BUSY_WAITING_TIME) time and do some busy waiting. If we are on high load,
self.running_average_delta = if self.running_average_delta == 0.0 { // don't try such fancy precision increasing
delta.as_secs_f64() if dur > BUSY_WAITING_TRIGGER_DUR {
let first_sleep_dur = dur - BUSY_WAITING_TIME;
thread::sleep(first_sleep_dur);
let target_time = before + dur;
span!(_guard, "tick", "Clock::busy_wait");
while target_time > Instant::now() {
//busy waiting
std::sync::atomic::spin_loop_hint();
}
} else { } else {
CLOCK_SMOOTHING * self.running_average_delta thread::sleep(dur);
+ (1.0 - CLOCK_SMOOTHING) * delta.as_secs_f64() }
}; }
}
impl ClockStats {
fn new(sorted: &[u16], last_busy_dt: Duration) -> Self {
const NANOS_PER_SEC: f64 = Duration::from_secs(1).as_nanos() as f64;
const NANOS_PER_MILLI: f64 = Duration::from_millis(1).as_nanos() as f64;
let len = sorted.len();
let average_millis = sorted.iter().fold(0, |a, x| a + *x as u32) / len.max(1) as u32;
let average_tps = NANOS_PER_SEC / (average_millis as f64 * NANOS_PER_MILLI);
let (median_tps, percentile_90_tps, percentile_95_tps, percentile_99_tps) = if len
>= NUMBER_OF_OLD_DELTAS_KEPT
{
let median_millis = sorted[len / 2];
let percentile_90_millis = sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.1) as usize];
let percentile_95_millis = sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.05) as usize];
let percentile_99_millis = sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.01) as usize];
let median_tps = NANOS_PER_SEC / (median_millis as f64 * NANOS_PER_MILLI);
let percentile_90_tps = NANOS_PER_SEC / (percentile_90_millis as f64 * NANOS_PER_MILLI);
let percentile_95_tps = NANOS_PER_SEC / (percentile_95_millis as f64 * NANOS_PER_MILLI);
let percentile_99_tps = NANOS_PER_SEC / (percentile_99_millis as f64 * NANOS_PER_MILLI);
(
median_tps,
percentile_90_tps,
percentile_95_tps,
percentile_99_tps,
)
} else {
let avg_tps = NANOS_PER_SEC / last_busy_dt.as_nanos() as f64;
(avg_tps, avg_tps, avg_tps, avg_tps)
};
Self {
last_busy_dt,
average_tps,
median_tps,
percentile_90_tps,
percentile_95_tps,
percentile_99_tps,
}
} }
} }

View File

@ -140,19 +140,22 @@ fn main() -> io::Result<()> {
let mut shutdown_coordinator = ShutdownCoordinator::new(Arc::clone(&sigusr1_signal)); let mut shutdown_coordinator = ShutdownCoordinator::new(Arc::clone(&sigusr1_signal));
// Set up an fps clock // Set up an fps clock
let mut clock = Clock::start(); let mut clock = Clock::new(Duration::from_millis(1000 / TPS));
// Wait for a tick so we don't start with a zero dt // Wait for a tick so we don't start with a zero dt
// TODO: consider integrating this into Clock::start?
clock.tick(Duration::from_millis(1000 / TPS));
loop { loop {
#[cfg(feature = "tracy")]
common::util::tracy_client::finish_continuous_frame!();
#[cfg(feature = "tracy")]
let frame = common::util::tracy_client::start_noncontinuous_frame!("work");
// Terminate the server if instructed to do so by the shutdown coordinator // Terminate the server if instructed to do so by the shutdown coordinator
if shutdown_coordinator.check(&mut server, &settings) { if shutdown_coordinator.check(&mut server, &settings) {
break; break;
} }
let events = server let events = server
.tick(Input::default(), clock.get_last_delta()) .tick(Input::default(), clock.dt())
.expect("Failed to tick server"); .expect("Failed to tick server");
for event in events { for event in events {
@ -165,8 +168,6 @@ fn main() -> io::Result<()> {
// Clean up the server after a tick. // Clean up the server after a tick.
server.cleanup(); server.cleanup();
#[cfg(feature = "tracy")]
common::util::tracy_client::finish_continuous_frame!();
if let Some(tui) = tui.as_ref() { if let Some(tui) = tui.as_ref() {
match tui.msg_r.try_recv() { match tui.msg_r.try_recv() {
@ -194,8 +195,10 @@ fn main() -> io::Result<()> {
} }
} }
#[cfg(feature = "tracy")]
drop(frame);
// Wait for the next tick. // Wait for the next tick.
clock.tick(Duration::from_millis(1000 / TPS)); clock.tick();
} }
Ok(()) Ok(())

View File

@ -114,7 +114,7 @@ impl MusicChannel {
/// Maintain the fader attached to this channel. If the channel is not /// Maintain the fader attached to this channel. If the channel is not
/// fading, no action is taken. /// fading, no action is taken.
pub fn maintain(&mut self, dt: f32) { pub fn maintain(&mut self, dt: std::time::Duration) {
if self.state == ChannelState::Fading { if self.state == ChannelState::Fading {
self.fader.update(dt); self.fader.update(dt);
self.sink.set_volume(self.fader.get_volume()); self.sink.set_volume(self.fader.get_volume());

View File

@ -1,11 +1,13 @@
//! Controls volume transitions for Audio Channels //! Controls volume transitions for Audio Channels
use std::time::Duration;
/// Faders are attached to channels with initial and target volumes as well as a /// Faders are attached to channels with initial and target volumes as well as a
/// transition time. /// transition time.
#[derive(PartialEq, Clone, Copy)] #[derive(PartialEq, Clone, Copy)]
pub struct Fader { pub struct Fader {
length: f32, length: Duration,
running_time: f32, running_time: Duration,
volume_from: f32, volume_from: f32,
volume_to: f32, volume_to: f32,
is_running: bool, is_running: bool,
@ -21,19 +23,19 @@ pub enum FadeDirection {
fn lerp(t: f32, a: f32, b: f32) -> f32 { (1.0 - t) * a + t * b } fn lerp(t: f32, a: f32, b: f32) -> f32 { (1.0 - t) * a + t * b }
impl Fader { impl Fader {
pub fn fade(length: f32, volume_from: f32, volume_to: f32) -> Self { pub fn fade(length: Duration, volume_from: f32, volume_to: f32) -> Self {
Self { Self {
length, length,
running_time: 0.0, running_time: Duration::default(),
volume_from, volume_from,
volume_to, volume_to,
is_running: true, is_running: true,
} }
} }
pub fn fade_in(time: f32, volume_to: f32) -> Self { Self::fade(time, 0.0, volume_to) } pub fn fade_in(time: Duration, volume_to: f32) -> Self { Self::fade(time, 0.0, volume_to) }
pub fn fade_out(time: f32, volume_from: f32) -> Self { Self::fade(time, volume_from, 0.0) } pub fn fade_out(time: Duration, volume_from: f32) -> Self { Self::fade(time, volume_from, 0.0) }
/// Used to update the `target` volume of the fader when the max or min /// Used to update the `target` volume of the fader when the max or min
/// volume changes. This occurs when the player changes their in-game /// volume changes. This occurs when the player changes their in-game
@ -62,7 +64,7 @@ impl Fader {
} }
/// Called each tick to update the volume and state /// Called each tick to update the volume and state
pub fn update(&mut self, dt: f32) { pub fn update(&mut self, dt: std::time::Duration) {
if self.is_running { if self.is_running {
self.running_time += dt; self.running_time += dt;
if self.running_time >= self.length { if self.running_time >= self.length {
@ -74,7 +76,7 @@ impl Fader {
pub fn get_volume(&self) -> f32 { pub fn get_volume(&self) -> f32 {
lerp( lerp(
self.running_time / self.length, self.running_time.as_nanos() as f32 / self.length.as_nanos() as f32,
self.volume_from, self.volume_from,
self.volume_to, self.volume_to,
) )
@ -87,8 +89,8 @@ impl Fader {
impl Default for Fader { impl Default for Fader {
fn default() -> Self { fn default() -> Self {
Self { Self {
length: 0.0, length: Duration::default(),
running_time: 0.0, running_time: Duration::default(),
volume_from: 0.0, volume_from: 0.0,
volume_to: 1.0, volume_to: 1.0,
is_running: false, is_running: false,
@ -102,14 +104,14 @@ mod tests {
#[test] #[test]
fn fade_direction_in() { fn fade_direction_in() {
let fader = Fader::fade_in(10.0, 0.0); let fader = Fader::fade_in(Duration::from_secs(10), 0.0);
assert_eq!(fader.direction(), FadeDirection::In); assert_eq!(fader.direction(), FadeDirection::In);
} }
#[test] #[test]
fn fade_direction_out() { fn fade_direction_out() {
let fader = Fader::fade_out(10.0, 1.0); let fader = Fader::fade_out(Duration::from_secs(10), 1.0);
assert_eq!(fader.direction(), FadeDirection::Out); assert_eq!(fader.direction(), FadeDirection::Out);
} }
@ -117,10 +119,10 @@ mod tests {
#[test] #[test]
#[allow(clippy::float_cmp)] // TODO: Pending review in #587 #[allow(clippy::float_cmp)] // TODO: Pending review in #587
fn fade_out_completes() { fn fade_out_completes() {
let mut fader = Fader::fade_out(10.0, 1.0); let mut fader = Fader::fade_out(Duration::from_secs(10), 1.0);
// Run for the full duration // Run for the full duration
fader.update(10.0); fader.update(Duration::from_secs(10));
assert_eq!(fader.get_volume(), 0.0); assert_eq!(fader.get_volume(), 0.0);
assert!(fader.is_finished()); assert!(fader.is_finished());
@ -128,32 +130,32 @@ mod tests {
#[test] #[test]
fn update_target_volume_fading_out_when_currently_above() { fn update_target_volume_fading_out_when_currently_above() {
let mut fader = Fader::fade_out(20.0, 1.0); let mut fader = Fader::fade_out(Duration::from_secs(20), 1.0);
// After 0.1s, the fader should still be close to 1.0 // After 0.1s, the fader should still be close to 1.0
fader.update(0.1); fader.update(Duration::from_millis(100));
// Reduce volume to 0.4. We are currently above that. // Reduce volume to 0.4. We are currently above that.
fader.update_target_volume(0.4); fader.update_target_volume(0.4);
// The volume should immediately reduce to < 0.4 on the next update // The volume should immediately reduce to < 0.4 on the next update
fader.update(0.1); fader.update(Duration::from_millis(100));
assert!(fader.get_volume() < 0.4) assert!(fader.get_volume() < 0.4)
} }
#[test] #[test]
fn update_target_volume_fading_out_when_currently_below() { fn update_target_volume_fading_out_when_currently_below() {
let mut fader = Fader::fade_out(10.0, 0.8); let mut fader = Fader::fade_out(Duration::from_secs(10), 0.8);
// After 9s, the fader should be close to 0 // After 9s, the fader should be close to 0
fader.update(9.0); fader.update(Duration::from_secs(9));
// Notify of a volume increase to 1.0. We are already far below that. // Notify of a volume increase to 1.0. We are already far below that.
fader.update_target_volume(1.0); fader.update_target_volume(1.0);
// The fader should be unaffected by the new value, and continue dropping // The fader should be unaffected by the new value, and continue dropping
fader.update(0.1); fader.update(Duration::from_millis(100));
assert!(fader.get_volume() < 0.2); assert!(fader.get_volume() < 0.2);
} }
@ -161,16 +163,16 @@ mod tests {
#[test] #[test]
#[allow(clippy::float_cmp)] // TODO: Pending review in #587 #[allow(clippy::float_cmp)] // TODO: Pending review in #587
fn update_target_volume_fading_in_when_currently_above() { fn update_target_volume_fading_in_when_currently_above() {
let mut fader = Fader::fade_in(10.0, 1.0); let mut fader = Fader::fade_in(Duration::from_secs(10), 1.0);
// After 9s, the fader should be close to 1.0 // After 9s, the fader should be close to 1.0
fader.update(9.0); fader.update(Duration::from_secs(9));
// Reduce volume to 0.4. We are currently above that. // Reduce volume to 0.4. We are currently above that.
fader.update_target_volume(0.4); fader.update_target_volume(0.4);
// Run out the fader. It's volume should be 0.4 // Run out the fader. It's volume should be 0.4
fader.update(1.0); fader.update(Duration::from_secs(1));
assert_eq!(fader.get_volume(), 0.4); assert_eq!(fader.get_volume(), 0.4);
} }
@ -178,10 +180,10 @@ mod tests {
#[test] #[test]
#[allow(clippy::float_cmp)] // TODO: Pending review in #587 #[allow(clippy::float_cmp)] // TODO: Pending review in #587
fn update_target_volume_fading_in_when_currently_below() { fn update_target_volume_fading_in_when_currently_below() {
let mut fader = Fader::fade_in(20.0, 1.0); let mut fader = Fader::fade_in(Duration::from_secs(20), 1.0);
// After 0.1s, the fader should still be close to 0.0 // After 0.1s, the fader should still be close to 0.0
fader.update(0.1); fader.update(Duration::from_millis(100));
// Reduce volume to 0.4. The volume_to should be reduced accordingly. // Reduce volume to 0.4. The volume_to should be reduced accordingly.
fader.update_target_volume(0.4); fader.update_target_volume(0.4);

View File

@ -9,6 +9,7 @@ pub mod soundcache;
use channel::{MusicChannel, MusicChannelTag, SfxChannel}; use channel::{MusicChannel, MusicChannelTag, SfxChannel};
use fader::Fader; use fader::Fader;
use soundcache::SoundCache; use soundcache::SoundCache;
use std::time::Duration;
use tracing::warn; use tracing::warn;
use common::assets; use common::assets;
@ -84,7 +85,7 @@ impl AudioFrontend {
} }
/// Drop any unused music channels, and update their faders /// Drop any unused music channels, and update their faders
pub fn maintain(&mut self, dt: f32) { pub fn maintain(&mut self, dt: Duration) {
self.music_channels.retain(|c| !c.is_done()); self.music_channels.retain(|c| !c.is_done());
for channel in self.music_channels.iter_mut() { for channel in self.music_channels.iter_mut() {
@ -124,11 +125,13 @@ impl AudioFrontend {
if existing_channel.get_tag() != next_channel_tag { if existing_channel.get_tag() != next_channel_tag {
// Fade the existing channel out. It will be removed when the fade completes. // Fade the existing channel out. It will be removed when the fade completes.
existing_channel.set_fader(Fader::fade_out(2.0, self.music_volume)); existing_channel
.set_fader(Fader::fade_out(Duration::from_secs(2), self.music_volume));
let mut next_music_channel = MusicChannel::new(&audio_device); let mut next_music_channel = MusicChannel::new(&audio_device);
next_music_channel.set_fader(Fader::fade_in(12.0, self.music_volume)); next_music_channel
.set_fader(Fader::fade_in(Duration::from_secs(12), self.music_volume));
self.music_channels.push(next_music_channel); self.music_channels.push(next_music_channel);
} }

View File

@ -746,7 +746,7 @@ impl Hud {
// pulse time for pulsating elements // pulse time for pulsating elements
self.pulse = self.pulse + dt.as_secs_f32(); self.pulse = self.pulse + dt.as_secs_f32();
// FPS // FPS
let fps = global_state.clock.get_tps(); let fps = global_state.clock.stats().average_tps;
let version = common::util::DISPLAY_VERSION_LONG.clone(); let version = common::util::DISPLAY_VERSION_LONG.clone();
if self.show.ingame { if self.show.ingame {

View File

@ -63,7 +63,7 @@ impl GlobalState {
self.window.needs_refresh_resize(); self.window.needs_refresh_resize();
} }
pub fn maintain(&mut self, dt: f32) { self.audio.maintain(dt); } pub fn maintain(&mut self, dt: std::time::Duration) { self.audio.maintain(dt); }
#[cfg(feature = "singleplayer")] #[cfg(feature = "singleplayer")]
pub fn paused(&self) -> bool { pub fn paused(&self) -> bool {

View File

@ -185,7 +185,7 @@ fn main() {
profile, profile,
window, window,
settings, settings,
clock: Clock::start(), clock: Clock::new(std::time::Duration::from_millis(30)),
info_message: None, info_message: None,
#[cfg(feature = "singleplayer")] #[cfg(feature = "singleplayer")]
singleplayer: None, singleplayer: None,

View File

@ -155,7 +155,7 @@ impl PlayState for CharSelectionState {
match self.client.borrow_mut().tick( match self.client.borrow_mut().tick(
comp::ControllerInputs::default(), comp::ControllerInputs::default(),
global_state.clock.get_last_delta(), global_state.clock.dt(),
|_| {}, |_| {},
) { ) {
Ok(events) => { Ok(events) => {

View File

@ -211,7 +211,7 @@ impl PlayState for MainMenuState {
// Maintain the UI. // Maintain the UI.
for event in self for event in self
.main_menu_ui .main_menu_ui
.maintain(global_state, global_state.clock.get_last_delta()) .maintain(global_state, global_state.clock.dt())
{ {
match event { match event {
MainMenuEvent::LoginAttempt { MainMenuEvent::LoginAttempt {

View File

@ -168,12 +168,13 @@ fn handle_main_events_cleared(
if !exit { if !exit {
// Wait for the next tick. // Wait for the next tick.
span!(_guard, "Main thread sleep"); span!(_guard, "Main thread sleep");
global_state.clock.tick(Duration::from_millis( global_state.clock.set_target_dt(Duration::from_millis(
1000 / global_state.settings.graphics.max_fps as u64, 1000 / global_state.settings.graphics.max_fps as u64,
)); ));
global_state.clock.tick();
span!(_guard, "Maintain global state"); span!(_guard, "Maintain global state");
// Maintain global state. // Maintain global state.
global_state.maintain(global_state.clock.get_last_delta().as_secs_f32()); global_state.maintain(global_state.clock.dt());
} }
} }

View File

@ -631,7 +631,7 @@ impl PlayState for SessionState {
let right = self.scene.camera().right(); let right = self.scene.camera().right();
let dir = right * axis_right + forward * axis_up; let dir = right * axis_right + forward * axis_up;
let dt = global_state.clock.get_last_delta().as_secs_f32(); let dt = global_state.clock.dt().as_secs_f32();
if self.freefly_vel.magnitude_squared() > 0.01 { if self.freefly_vel.magnitude_squared() > 0.01 {
let new_vel = self.freefly_vel let new_vel = self.freefly_vel
- self.freefly_vel.normalized() * (FREEFLY_DAMPING * dt); - self.freefly_vel.normalized() * (FREEFLY_DAMPING * dt);
@ -668,11 +668,7 @@ impl PlayState for SessionState {
// Runs if either in a multiplayer server or the singleplayer server is unpaused // Runs if either in a multiplayer server or the singleplayer server is unpaused
if !global_state.paused() { if !global_state.paused() {
// Perform an in-game tick. // Perform an in-game tick.
match self.tick( match self.tick(global_state.clock.dt(), global_state, &mut outcomes) {
global_state.clock.get_avg_delta(),
global_state,
&mut outcomes,
) {
Ok(TickAction::Continue) => {}, // Do nothing Ok(TickAction::Continue) => {}, // Do nothing
Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu
Err(err) => { Err(err) => {
@ -697,8 +693,8 @@ impl PlayState for SessionState {
.gameplay .gameplay
.toggle_debug .toggle_debug
.then(|| DebugInfo { .then(|| DebugInfo {
tps: global_state.clock.get_tps(), tps: global_state.clock.stats().average_tps,
frame_time: global_state.clock.get_avg_delta(), frame_time: global_state.clock.stats().last_busy_dt,
ping_ms: self.client.borrow().get_ping_ms_rolling_avg(), ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
coordinates: self coordinates: self
.client .client
@ -741,7 +737,7 @@ impl PlayState for SessionState {
global_state, global_state,
&debug_info, &debug_info,
&self.scene.camera(), &self.scene.camera(),
global_state.clock.get_last_delta(), global_state.clock.dt(),
HudInfo { HudInfo {
is_aiming, is_aiming,
is_first_person: matches!( is_first_person: matches!(

View File

@ -152,7 +152,7 @@ fn run_server(mut server: Server, rec: Receiver<Msg>, paused: Arc<AtomicBool>) {
info!("Starting server-cli..."); info!("Starting server-cli...");
// Set up an fps clock // Set up an fps clock
let mut clock = Clock::start(); let mut clock = Clock::new(Duration::from_millis(1000 / TPS));
loop { loop {
// Check any event such as stopping and pausing // Check any event such as stopping and pausing
@ -167,7 +167,7 @@ fn run_server(mut server: Server, rec: Receiver<Msg>, paused: Arc<AtomicBool>) {
} }
// Wait for the next tick. // Wait for the next tick.
clock.tick(Duration::from_millis(1000 / TPS)); clock.tick();
// Skip updating the server if it's paused // Skip updating the server if it's paused
if paused.load(Ordering::SeqCst) && server.number_of_players() < 2 { if paused.load(Ordering::SeqCst) && server.number_of_players() < 2 {
@ -177,7 +177,7 @@ fn run_server(mut server: Server, rec: Receiver<Msg>, paused: Arc<AtomicBool>) {
} }
let events = server let events = server
.tick(Input::default(), clock.get_last_delta()) .tick(Input::default(), clock.dt())
.expect("Failed to tick server!"); .expect("Failed to tick server!");
for event in events { for event in events {