1use 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
20const FPS_WINDOW_MS: u64 = 1000;
23
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26struct Clock {
27 wrapped: u64,
29 last: u32,
30}
31
32impl Clock {
33 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45struct Fps {
46 since: Option<Millis>,
48 frames: u32,
49 last: u16,
50}
51
52impl Fps {
53 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#[derive(Debug)]
72struct Reporter {
73 report: RunReport,
74 init_us: u64,
76 card_us: Option<u64>,
78 loaded_us: Option<u64>,
80 first_shown: bool,
82 reads: bool,
84 start: Vec<String>,
86 each: Vec<String>,
88 since: Option<Millis>,
91 stretch: Stretch,
92}
93
94pub struct Driver<G: GameData, S: Sdk> {
96 app: App<G>,
97 platform: Playdate<S>,
98 started: bool,
99 clock: Clock,
100 fps: Fps,
101 reporter: Option<Box<Reporter>>,
103}
104
105impl<G: GameData, S: Sdk> Driver<G, S> {
106 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 pub fn with_report(start: Start<G>, sdk: S, report: RunReport) -> Self {
121 Self::reporting(start, sdk, report, report::watch_bundle)
122 }
123
124 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 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 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 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 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 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 self.say(&format!("Run report: {path} not written: {error}"));
257 }
258 }
259
260 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 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 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 pub fn menu_chosen(&mut self, slot: u8) {
302 self.platform.menu_chosen(slot);
303 }
304
305 pub fn app(&self) -> &App<G> {
307 &self.app
308 }
309
310 pub fn platform(&self) -> &Playdate<S> {
312 &self.platform
313 }
314}
315
316#[cfg(test)]
317mod tests;