Skip to main content

shuyi_ui/
frame_cap.rs

1//! The frame cap: the most frames a second the platform shows
2//! ([`FrameCap`]), 60 unless the game or the player says otherwise, so a
3//! simple game doesn't run as fast as it can and keep a CPU core busy
4//! (fans, heat, the battery).
5//!
6//! The cap is the player's setting
7//! ([`Settings::frame_cap`](crate::settings::Settings::frame_cap)), whose
8//! default and choices are the game's
9//! ([`GameSettings::DEFAULT_FRAME_CAP`](crate::settings::GameSettings::DEFAULT_FRAME_CAP),
10//! [`FRAME_CAP_CHOICES`](crate::settings::GameSettings::FRAME_CAP_CHOICES)).
11//! Only the platform can apply it, so a change is a request it confirms:
12//! every change of the cap takes the next serial number, the platform is
13//! handed the cap with its serial each frame, and before each frame it
14//! reports what it applied ([`Pacing`], through
15//! [`Game::set_pacing`](crate::Game::set_pacing)). A game that asks for a
16//! cap ([`Ctx::request_frame_cap`](crate::Ctx::request_frame_cap)) gets a
17//! [`CapRequest`] and checks it like a promise
18//! ([`Ctx::frame_cap_status`](crate::Ctx::frame_cap_status)): pending until
19//! the platform reports that serial, then applied, with the report.
20//!
21//! **With vsync and variable refresh (VRR).** The platform never waits
22//! twice: when vsync, the display or a slow machine already keeps the
23//! frames as far apart as the cap asks, it doesn't wait at all, so a cap
24//! of 60 on a 60 Hz display with vsync on never misses a vblank. On a VRR
25//! display (G-Sync, `FreeSync`, adaptive sync), a cap a few frames under the
26//! display's refresh (141 on a 144 Hz display) keeps it in its variable
27//! range, so the display follows the frames.
28//!
29//! **With fixed steps.** A tick stage steps 60 times a second. At a cap
30//! above 60 each step shows for whole frames: evenly at 120 and 240, but 2
31//! and 3 frames in turn at 144 and 165, which moves unevenly. A cap that
32//! is a multiple of 60 avoids it.
33
34use crate::prelude::*;
35
36/// The lowest cap [`FrameCap::fps`] takes, in frames a second. *Tunable.*
37pub const MIN_FPS: u16 = 15;
38
39/// The highest cap [`FrameCap::fps`] takes, in frames a second. *Tunable.*
40pub const MAX_FPS: u16 = 1000;
41
42/// The choices an options screen offers when the game doesn't give its own
43/// ([`GameSettings::FRAME_CAP_CHOICES`](crate::settings::GameSettings::FRAME_CAP_CHOICES)):
44/// the common refresh rates, then no cap.
45pub const CHOICES: [FrameCap; 7] = [
46    FrameCap::Fps(30),
47    FrameCap::Fps(60),
48    FrameCap::Fps(120),
49    FrameCap::Fps(144),
50    FrameCap::Fps(165),
51    FrameCap::Fps(240),
52    FrameCap::Off,
53];
54
55/// Text key ([`Ctx::text`](crate::Ctx::text)) of a cap's name, with the
56/// number of frames a second as `{n}` (`"{n} FPS"`).
57pub const FPS_KEY: &str = "options.value.fps";
58
59/// Text key of no cap's name (`"Unlimited"`).
60pub const UNLIMITED_KEY: &str = "options.value.unlimited";
61
62/// The most frames a second the platform shows.
63///
64/// 60 by default ([`FrameCap::DEFAULT`]): enough for a smooth picture on
65/// most displays, and a game that does little uses little of the CPU.
66/// Build one with [`FrameCap::fps`], which checks the range; a cap out of
67/// the range ([`MIN_FPS`] to [`MAX_FPS`]), set as `Fps(n)` in code or read
68/// from a file, is held to it when the platform applies it
69/// ([`limit`](Self::limit)).
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
71#[non_exhaustive]
72pub enum FrameCap {
73    /// No cap: the platform shows frames as fast as vsync, the display or
74    /// the machine let it, on purpose.
75    Off,
76    /// At most this many frames a second.
77    Fps(u16),
78}
79
80impl Default for FrameCap {
81    fn default() -> Self {
82        Self::DEFAULT
83    }
84}
85
86impl FrameCap {
87    /// The engine's default: 60 frames a second.
88    pub const DEFAULT: Self = Self::Fps(60);
89
90    /// A cap of `n` frames a second; out of [`MIN_FPS`] to [`MAX_FPS`] is
91    /// [`CapError::OutOfRange`].
92    pub const fn fps(n: u16) -> Result<Self, CapError> {
93        if n < MIN_FPS || n > MAX_FPS {
94            return Err(CapError::OutOfRange);
95        }
96        Ok(Self::Fps(n))
97    }
98
99    /// The most frames a second it allows, held to [`MIN_FPS`] to
100    /// [`MAX_FPS`]; `None` for no cap.
101    pub fn limit(self) -> Option<u16> {
102        match self {
103            Self::Off => None,
104            Self::Fps(n) => Some(n.clamp(MIN_FPS, MAX_FPS)),
105        }
106    }
107
108    /// The text key of its name: [`FPS_KEY`] or [`UNLIMITED_KEY`].
109    pub const fn key(self) -> &'static str {
110        match self {
111            Self::Off => UNLIMITED_KEY,
112            Self::Fps(_) => FPS_KEY,
113        }
114    }
115
116    /// Its name on an options screen, in `ctx`'s language: `"60 FPS"`
117    /// ([`FPS_KEY`], with the number as `{n}`) or `"Unlimited"`
118    /// ([`UNLIMITED_KEY`]).
119    pub fn name<G: crate::GameData>(self, ctx: &crate::Ctx<G>) -> String {
120        match self {
121            Self::Off => ctx.text(UNLIMITED_KEY).to_owned(),
122            Self::Fps(n) => ctx.text_with(FPS_KEY, &[("n", &n)]),
123        }
124    }
125}
126
127/// Why [`FrameCap::fps`] refused a cap.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
129#[non_exhaustive]
130pub enum CapError {
131    /// Fewer than [`MIN_FPS`] or more than [`MAX_FPS`] frames a second.
132    #[error("a frame cap is {MIN_FPS} to {MAX_FPS} frames a second, or off")]
133    OutOfRange,
134}
135
136/// A request for a frame cap
137/// ([`Ctx::request_frame_cap`](crate::Ctx::request_frame_cap)): a handle to
138/// check it with ([`Ctx::frame_cap_status`](crate::Ctx::frame_cap_status)),
139/// as a promise is, without waiting on it.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub struct CapRequest {
142    serial: u32,
143}
144
145impl CapRequest {
146    /// The request with `serial`, the serial its change took.
147    pub(crate) const fn new(serial: u32) -> Self {
148        Self { serial }
149    }
150
151    /// The serial number its change took: the platform reports it once it
152    /// has applied the cap ([`Pacing::serial`]).
153    pub const fn serial(self) -> u32 {
154        self.serial
155    }
156}
157
158/// Where a [`CapRequest`] is.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum CapStatus {
162    /// The platform hasn't reported applying it yet. A platform that never
163    /// reports (one whose rate is fixed and that doesn't say so) leaves
164    /// every request here.
165    Pending,
166    /// The platform applied it: its report.
167    Applied(Pacing),
168    /// A later change of the cap (another request, the options screen, a
169    /// profile switched to) took the next serial: the cap asked for is no
170    /// longer the one in force, whether or not it had applied.
171    Superseded,
172}
173
174/// What the platform reports of its pacing, before each frame
175/// ([`Game::set_pacing`](crate::Game::set_pacing)): the cap it applied and
176/// its serial, whether it asked for vsync, and the frames a second it
177/// measured. Built with [`Pacing::new`]; the rest is assigned.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
179#[serde(deny_unknown_fields)]
180#[non_exhaustive]
181pub struct Pacing {
182    /// The cap applied.
183    pub cap: FrameCap,
184    /// The serial the cap came with: the latest the platform was handed.
185    pub serial: u32,
186    /// Whether the platform asked the display's driver for vsync (the
187    /// driver may not give it).
188    pub vsync: bool,
189    /// The frames a second it measured: a rolling average, 0 before it has
190    /// measured.
191    pub fps: u16,
192}
193
194impl Pacing {
195    /// A report of `cap` applied, which came with `serial`: no vsync and
196    /// nothing measured until assigned.
197    pub const fn new(cap: FrameCap, serial: u32) -> Self {
198        Self {
199            cap,
200            serial,
201            vsync: false,
202            fps: 0,
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn a_cap_is_15_to_1000_frames_a_second() {
213        assert_eq!(FrameCap::fps(14), Err(CapError::OutOfRange));
214        assert_eq!(FrameCap::fps(1001), Err(CapError::OutOfRange));
215        assert_eq!(FrameCap::fps(15), Ok(FrameCap::Fps(15)));
216        assert_eq!(FrameCap::fps(1000), Ok(FrameCap::Fps(1000)));
217        assert_eq!(FrameCap::default(), FrameCap::Fps(60));
218        assert!(CapError::OutOfRange.to_string().contains("15 to 1000"));
219    }
220
221    #[test]
222    fn a_cap_out_of_range_is_held_to_it() {
223        assert_eq!(FrameCap::Off.limit(), None);
224        assert_eq!(FrameCap::Fps(60).limit(), Some(60));
225        assert_eq!(FrameCap::Fps(0).limit(), Some(MIN_FPS));
226        assert_eq!(FrameCap::Fps(5000).limit(), Some(MAX_FPS));
227    }
228
229    #[test]
230    fn a_cap_is_named_by_its_text_key() {
231        assert_eq!(FrameCap::Off.key(), "options.value.unlimited");
232        assert_eq!(FrameCap::Fps(60).key(), "options.value.fps");
233        assert_eq!(
234            (UNLIMITED_KEY, FPS_KEY),
235            (FrameCap::Off.key(), FrameCap::Fps(1).key())
236        );
237    }
238
239    #[test]
240    fn the_choices_are_in_range_and_end_with_no_cap() {
241        for cap in CHOICES {
242            let checked = cap.limit().map(FrameCap::fps);
243            assert_eq!(checked, cap.limit().map(|_| Ok(cap)));
244        }
245        assert_eq!(CHOICES.last(), Some(&FrameCap::Off));
246        assert!(CHOICES.contains(&FrameCap::DEFAULT));
247    }
248
249    #[test]
250    fn a_cap_is_saved_by_name() {
251        assert_eq!(ron::to_string(&FrameCap::Fps(60)).unwrap(), "Fps(60)");
252        assert_eq!(ron::from_str::<FrameCap>("Off"), Ok(FrameCap::Off));
253    }
254
255    #[test]
256    fn a_report_starts_unmeasured() {
257        let pacing = Pacing::new(FrameCap::Off, 3);
258        assert_eq!((pacing.cap, pacing.serial), (FrameCap::Off, 3));
259        assert!(!pacing.vsync);
260        assert_eq!(pacing.fps, 0);
261        assert_eq!(CapRequest::new(4).serial(), 4);
262    }
263}