1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
//! Frame rate limiting. //! //! An amethyst [`Application`] runs in a loop, executing game update logic each frame. In //! order to reduce CPU usage and keep frame timing predictable, amethyst uses a configurable //! frame limiting strategy to introduce a delay before starting each frame if the previous //! frame completed sufficiently quickly. //! //! The frame rate limiting strategy has two parts: A maximum frame rate, given as a number of //! frames per second, and a strategy for returning any remaining time in the frame to the //! operating system. Based on the specified maximum frame rate, each frame has a budget for //! how long it can take. For example, at 60 fps each frame has 16.6 milliseconds to perform //! any work it needs to. If a frame takes less time than is budgeted, amethyst will attempt to //! yield the remaining time back to the operating system, using the chosen strategy. //! //! By default, amethyst will set the maximum frame rate to 144 fps, and will use a yield-only //! limiting strategy. //! //! # Examples //! //! ``` //! use std::time::Duration; //! //! use amethyst::prelude::*; //! use amethyst::core::frame_limiter::FrameRateLimitStrategy; //! //! # struct GameState; //! # impl SimpleState for GameState {} //! # fn main() -> amethyst::Result<()> { //! let assets_dir = "./"; //! let mut game = Application::build(assets_dir, GameState)? //! .with_frame_limit( //! FrameRateLimitStrategy::SleepAndYield(Duration::from_millis(2)), //! 144, //! ) //! .build(GameDataBuilder::new())?; //! # Ok(()) //! # } //! ``` //! //! # Frame Rate Limiting Strategies //! //! The four possible strategies described by [`FrameRateLimitStrategy`] are as follows: //! //! * `Unlimited` will not try to limit the frame rate to the specified maximum. Amethyst //! will call [`thread::yield_now`] once and then continue to the next frame. //! * `Yield` will call [`thread::yield_now`] repeatedly until the frame duration has //! passed. This will result in the most accurate frame timings, but effectively guarantees //! that one CPU core will be fully utilized during the frame's idle time. //! * `Sleep` will call [`thread::sleep`] with a duration of 0 milliseconds until the //! frame duration has passed. This will result in lower CPU usage while the game is idle, but //! risks fluctuations in frame timing if the operating system doesn't wake the game until //! after the frame should have started. //! * `SleepAndYield` will sleep until there's only a small amount of time left in the frame, //! and then will yield until the next frame starts. This approach attempts to get the //! consistent frame timings of yielding, while reducing CPU usage compared to the yield-only //! approach. //! //! By default amethyst will use the `Yield` strategy, which is fine for desktop and console //! games that aren't as affected by extra CPU usage. For mobile devices, the `Sleep` strategy //! will help conserve battery life. //! //! `SleepAndYield` can potentially be as accurate as `Yield` while using less CPU time, but you //! will have to test different grace period timings to determine how much time needs to be left //! to ensure that the main thread doesn't sleep too long and miss the start of the next frame. //! //! [`Application`]: ../../amethyst/struct.Application.html //! [`FrameRateLimitStrategy`]: ./enum.FrameRateLimitStrategy.html //! [`thread::yield_now`]: https://doc.rust-lang.org/std/thread/fn.yield_now.html //! [`thread::sleep`]: https://doc.rust-lang.org/stable/std/thread/fn.sleep.html use std::{ thread::{sleep, yield_now}, time::{Duration, Instant}, }; use derive_new::new; use serde::{Deserialize, Serialize}; const ZERO: Duration = Duration::from_millis(0); /// Frame rate limiting strategy. /// /// See the [module documentation] on the difference between sleeping and yielding, and when /// these different strategies should be used. /// /// [module documentation]: ./index.html#frame-rate-limiting-strategies #[derive(Debug, Clone, Deserialize, Serialize)] pub enum FrameRateLimitStrategy { /// No limit, will do a single yield and then continue with the next frame. Unlimited, /// Yield repeatedly until the frame duration has passed. Yield, /// Sleep repeatedly until the frame duration has passed. Sleep, /// Use sleep and yield combined. /// /// Will sleep repeatedly until the given duration remains, and then will yield repeatedly /// for the remaining frame time. SleepAndYield(Duration), } impl Default for FrameRateLimitStrategy { fn default() -> Self { FrameRateLimitStrategy::Yield } } /// Frame limiting configuration loaded from a configuration file. /// /// Provides the configuration for a [`FrameLimiter`] using a configuration file. The config /// file can be loaded using the methods of the [`Config`] trait. /// /// # Examples /// /// ```no_run /// use amethyst::prelude::*; /// use amethyst::core::frame_limiter::FrameRateLimitConfig; /// /// let config = FrameRateLimitConfig::load("./config/frame_limiter.ron"); /// ``` /// /// [`FrameLimiter`]: ./struct.FrameLimiter.html /// [`Config`]: ../../amethyst_config/trait.Config.html #[derive(Debug, Clone, Deserialize, Serialize, new)] pub struct FrameRateLimitConfig { /// Frame rate limiting strategy. pub strategy: FrameRateLimitStrategy, /// The FPS to limit the game loop execution. pub fps: u32, } impl Default for FrameRateLimitConfig { fn default() -> Self { FrameRateLimitConfig { fps: 144, strategy: Default::default(), } } } /// Frame limiter resource. /// /// `FrameLimiter` is used internally by amethyst to limit the frame rate to the /// rate specified by the user. It is added as a resource to the world so that user code may /// change the frame rate limit at runtime if necessary. #[derive(Debug)] pub struct FrameLimiter { frame_duration: Duration, strategy: FrameRateLimitStrategy, last_call: Instant, } impl Default for FrameLimiter { fn default() -> Self { FrameLimiter::from_config(Default::default()) } } impl FrameLimiter { /// Creates a new frame limiter. pub fn new(strategy: FrameRateLimitStrategy, fps: u32) -> Self { let mut s = Self { frame_duration: Duration::from_secs(0), strategy: Default::default(), last_call: Instant::now(), }; s.set_rate(strategy, fps); s } /// Sets the maximum fps and frame rate limiting strategy. pub fn set_rate(&mut self, mut strategy: FrameRateLimitStrategy, mut fps: u32) { if fps == 0 { strategy = FrameRateLimitStrategy::Unlimited; fps = 144; } self.strategy = strategy; self.frame_duration = Duration::from_secs(1) / fps; } /// Creates a new frame limiter with the given config. pub fn from_config(config: FrameRateLimitConfig) -> Self { Self::new(config.strategy, config.fps) } /// Resets the frame start time to the current instant. /// /// This resets the frame limiter's internal tracking of when the last frame started to the /// current instant. Be careful when calling `start`, as doing so will cause the current /// frame to be longer than normal if not called at the very beginning of the frame. pub fn start(&mut self) { self.last_call = Instant::now(); } /// Blocks the current thread until the allotted frame time has passed. /// /// `wait` is used internally by [`Application`] to limit the frame rate of the game /// to the configured rate. This should likely never be called directly by game logic. /// /// [`Application`]: ../../amethyst/struct.Application.html pub fn wait(&mut self) { use self::FrameRateLimitStrategy::*; match self.strategy { Unlimited => yield_now(), Yield => self.do_yield(), Sleep => self.do_sleep(ZERO), SleepAndYield(dur) => { self.do_sleep(dur); self.do_yield(); } } self.last_call = Instant::now(); } fn do_yield(&self) { while Instant::now() - self.last_call < self.frame_duration { yield_now(); } } fn do_sleep(&self, stop_on_remaining: Duration) { let frame_duration = self.frame_duration - stop_on_remaining; loop { let elapsed = Instant::now() - self.last_call; if elapsed >= frame_duration { break; } else { sleep(frame_duration - elapsed); } } } }