Skip to main content

shuyi_playdate/
driver.rs

1//! [`Driver`]: the body of the SDK's update callback, around
2//! [`App::frame`], with the backend's device behind [`Sdk`] so the tests run
3//! all of it on the fake.
4
5use alloc::boxed::Box;
6use alloc::format;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10use shuyi_assets::bundle::Watcher;
11use shuyi_platform::{App, Millis, Start};
12use shuyi_ui::log::{LogCategory, LogLevel};
13use shuyi_ui::{Game, GameData};
14
15use crate::backend::Playdate;
16use crate::display::Showing;
17use crate::report::{self, Now, Reads, RunReport, Stretch};
18use crate::sdk::{Sdk, SdkEvent};
19
20/// How long the frames a second are measured over before they are reported,
21/// in milliseconds.
22const FPS_WINDOW_MS: u64 = 1000;
23
24/// A clock of whole milliseconds from a counter that wraps at `u32::MAX`.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26struct Clock {
27    /// The wraps so far, in milliseconds.
28    wrapped: u64,
29    last: u32,
30}
31
32impl Clock {
33    /// The time at the counter's reading `now`.
34    fn at(&mut self, now: u32) -> Millis {
35        if now < self.last {
36            self.wrapped += 1 << 32;
37        }
38        self.last = now;
39        Millis(self.wrapped + u64::from(now))
40    }
41}
42
43/// The frames a second, measured over a window.
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45struct Fps {
46    /// When the window began; `None` before the first frame.
47    since: Option<Millis>,
48    frames: u32,
49    last: u16,
50}
51
52impl Fps {
53    /// A frame ran at `now`.
54    fn frame(&mut self, now: Millis) {
55        let Some(since) = self.since else {
56            self.since = Some(now);
57            return;
58        };
59        self.frames += 1;
60        let ms = now.0.saturating_sub(since.0);
61        if ms >= FPS_WINDOW_MS {
62            let rate = u64::from(self.frames) * 1000 / ms;
63            self.last = u16::try_from(rate).unwrap_or(u16::MAX);
64            self.since = Some(now);
65            self.frames = 0;
66        }
67    }
68}
69
70/// The run report's state, while it is on.
71#[derive(Debug)]
72struct Reporter {
73    report: RunReport,
74    /// When the driver was made, in the timer's microseconds: `init`.
75    init_us: u64,
76    /// When the card was shown, after `init`.
77    card_us: Option<u64>,
78    /// When the game had loaded, after `init`.
79    loaded_us: Option<u64>,
80    /// Whether the first frame of the game was shown.
81    first_shown: bool,
82    /// Whether the bundle's reads are noted.
83    reads: bool,
84    /// The start's lines, for the file, until the first frame is shown.
85    start: Vec<String>,
86    /// Every read's line, for the file, once the game has loaded.
87    each: Vec<String>,
88    /// When the stretch the running lines are of began: from the first
89    /// frame shown.
90    since: Option<Millis>,
91    stretch: Stretch,
92}
93
94/// A game on the Playdate: its [`App`] and the backend.
95pub struct Driver<G: GameData, S: Sdk> {
96    app: App<G>,
97    platform: Playdate<S>,
98    started: bool,
99    clock: Clock,
100    fps: Fps,
101    /// The run report, if the game turned it on.
102    reporter: Option<Box<Reporter>>,
103}
104
105impl<G: GameData, S: Sdk> Driver<G, S> {
106    /// The game `start` says on `sdk`.
107    pub fn new(start: Start<G>, sdk: S) -> Self {
108        Self {
109            app: App::new(start),
110            platform: Playdate::new(sdk),
111            started: false,
112            clock: Clock::default(),
113            fps: Fps::default(),
114            reporter: None,
115        }
116    }
117
118    /// The game `start` says on `sdk`, saying in the console how its start
119    /// went and how fast it runs, as `report` says ([`report`]).
120    pub fn with_report(start: Start<G>, sdk: S, report: RunReport) -> Self {
121        Self::reporting(start, sdk, report, report::watch_bundle)
122    }
123
124    /// [`with_report`](Self::with_report), setting the bundle's watcher
125    /// with `watch`.
126    pub(crate) fn reporting(
127        start: Start<G>,
128        sdk: S,
129        report: RunReport,
130        watch: fn(Watcher) -> Result<(), Watcher>,
131    ) -> Self {
132        let init_us = S::clock_us();
133        let mut driver = Self::new(start, sdk);
134        let reads = watch(report::noted).is_ok();
135        let mut start = Vec::new();
136        if reads {
137            report::with_reads(|noted| *noted = Some(Reads::new(S::clock_us)));
138        } else {
139            driver.say(report::REFUSED);
140            start.push(String::from(report::REFUSED));
141        }
142        driver.platform.measure(report.phases);
143        driver.reporter = Some(Box::new(Reporter {
144            report,
145            init_us,
146            card_us: None,
147            loaded_us: None,
148            first_shown: false,
149            reads,
150            start,
151            each: Vec::new(),
152            since: None,
153            stretch: Stretch::default(),
154        }));
155        driver
156    }
157
158    /// One update of the SDK's: true when the screen changed.
159    pub fn update(&mut self) -> bool {
160        self.app.set_pacing(self.platform.pacing(self.fps.last));
161        let now = self.clock.at(self.platform.sdk().millis());
162        let began = self.reporter.is_some().then(S::clock_us);
163        // A game that has quit keeps its last frame: the player leaves
164        // through the system's Home.
165        let _ = self.app.frame(&mut self.platform, now);
166        let timed = began.map(|began| (began, S::clock_us()));
167        self.fps.frame(now);
168        self.after_frame();
169        let changed = self.platform.take_changed();
170        if let Some((began, ended)) = timed {
171            self.report(began, ended, now, changed);
172        }
173        changed
174    }
175
176    /// The run report's part of an update whose `App::frame` ran from
177    /// `began` to `ended` (the timer's microseconds), at `now`.
178    fn report(&mut self, began: u64, ended: u64, now: Millis, changed: bool) {
179        let Some(mut r) = self.reporter.take() else {
180            return;
181        };
182        let in_start = !r.first_shown;
183        let at = ended.saturating_sub(r.init_us);
184        let showing = self.platform.showing();
185        let mut lines = Vec::new();
186        if r.card_us.is_none() && showing == Showing::Card {
187            r.card_us = Some(at);
188            lines.push(report::shown_line("Loading card", at));
189        }
190        if r.loaded_us.is_none() && self.app.game().is_some() {
191            r.loaded_us = Some(at);
192            lines.push(report::load_line(at - r.card_us.unwrap_or(0)));
193            let used = self.platform.sdk().stack_used();
194            lines.push(report::stack_line(used, r.report.stack_bytes));
195            if let Some(reads) = report::with_reads(Option::take).filter(|_| r.reads) {
196                lines.extend(reads.summary(ended, r.report.slowest));
197                r.each = reads.each(ended);
198            }
199        }
200        if !r.first_shown && r.loaded_us.is_some() && changed && showing == Showing::Game {
201            r.first_shown = true;
202            lines.push(report::shown_line("First frame of the game", at));
203            r.since = Some(now);
204            let _ = self.platform.take_measured();
205        } else if let Some(since) = r.since {
206            r.stretch.frame.add(ended.saturating_sub(began));
207            let ms = now.0.saturating_sub(since.0);
208            if ms >= u64::from(r.report.every_seconds) * 1000 {
209                lines.extend(self.running_lines(&r, ms, now));
210                r.since = Some(now);
211                r.stretch = Stretch::default();
212            }
213        }
214        for line in &lines {
215            self.say(line);
216        }
217        if in_start {
218            r.start.extend(lines);
219            if r.first_shown {
220                self.write_file(&mut r);
221            }
222        }
223        self.reporter = Some(r);
224    }
225
226    /// The running lines, for a stretch of `ms` milliseconds ending `now`.
227    fn running_lines(&mut self, r: &Reporter, ms: u64, now: Millis) -> Vec<String> {
228        let measured = self.platform.take_measured();
229        let underran = self.platform.underran();
230        let stack_used = self.platform.sdk().stack_used();
231        let at = Now {
232            screen: self.app.game().and_then(Game::top_screen),
233            ms,
234            stack_used,
235            music: self.platform.music(now),
236            underran,
237        };
238        r.stretch.lines(&at, &measured, &r.report)
239    }
240
241    /// Writes the start's lines and every read to the report's file, if it
242    /// names one; the console says if it couldn't.
243    fn write_file(&self, r: &mut Reporter) {
244        let start = core::mem::take(&mut r.start);
245        let each = core::mem::take(&mut r.each);
246        let Some(path) = &r.report.file else {
247            return;
248        };
249        let mut text = String::new();
250        for line in start.iter().chain(&each) {
251            text.push_str(line);
252            text.push('\n');
253        }
254        if let Err(error) = self.platform.sdk().write_data(path, text.as_bytes()) {
255            // check-text: not player text (for the console)
256            self.say(&format!("Run report: {path} not written: {error}"));
257        }
258    }
259
260    /// Logs `text` as the run report's line, as the log's lines are.
261    fn say(&self, text: &str) {
262        self.platform.sdk().log(&format!(
263            "f{} {} {}: {text}",
264            self.app.frame_number(),
265            LogLevel::Info.name(),
266            LogCategory::Platform.name(),
267        ));
268    }
269
270    /// What follows a frame: the backend readied once the game has loaded,
271    /// and the frame's log on the SDK's console.
272    fn after_frame(&mut self) {
273        if !self.started
274            && let Some(game) = self.app.game()
275        {
276            self.platform.start(game);
277            self.started = true;
278        }
279        for line in self.app.take_new_log() {
280            self.platform.sdk().log(&format!(
281                "f{} {} {}: {}",
282                line.frame,
283                line.level.name(),
284                line.category.name(),
285                line.text
286            ));
287        }
288    }
289
290    /// The SDK's event. What must be heard before the system stops the
291    /// updates runs a frame now: `App` lets the game save and steps nothing.
292    pub fn event(&mut self, event: SdkEvent) {
293        if self.platform.event(event) {
294            let now = self.clock.at(self.platform.sdk().millis());
295            let _ = self.app.frame(&mut self.platform, now);
296            self.after_frame();
297        }
298    }
299
300    /// The player chose the system menu's item in `slot`.
301    pub fn menu_chosen(&mut self, slot: u8) {
302        self.platform.menu_chosen(slot);
303    }
304
305    /// The game's app, for the tests.
306    pub fn app(&self) -> &App<G> {
307        &self.app
308    }
309
310    /// The backend, for the tests.
311    pub fn platform(&self) -> &Playdate<S> {
312        &self.platform
313    }
314}
315
316#[cfg(test)]
317mod tests;