Skip to main content

shuyi_platform/
app.rs

1//! [`App`]: a whole game behind one call per frame, for the backend's own
2//! loop (ADR-0008 §2, ADR-0024). The backend calls [`App::frame`] with its
3//! clock; `App` reads the system's events and the input from the
4//! [`Platform`], steps the game ([`Game`]: its screens and input state)
5//! and its fixed simulation steps ([`TickClock`]), hands the frame's audio
6//! to the sound device and the frame to the display.
7//!
8//! A game starts in a loading phase ([`Start`]): the first frame shows a
9//! [`Card`] the game gives, the next loads the game behind it, and the
10//! frames after run it. On [`SystemEvent::Suspend`] the game's hook saves,
11//! `App` stops stepping and the device holds the audio
12//! ([`Audio::suspend`](crate::Audio::suspend)); on [`SystemEvent::Resume`]
13//! the audio comes back and `App` forgets the time it was away, so a sleep
14//! never becomes a burst of catch-up steps, and tells the game
15//! ([`Game::set_resumed`]), so a tick stage forgets the presses it kept
16//! from before and every key and button held before the suspend is let go
17//! (a key may come up while suspended, unheard); one still down counts
18//! once it is pressed again. On [`SystemEvent::FocusLost`] the game lets go
19//! of what is held at once ([`Game::forget_held_input`]), for the same
20//! reason, and until [`SystemEvent::FocusGained`] `App` applies the game's
21//! [`Background`] policy (ADR-0077): paused, the game isn't updated and its
22//! last frame is shown again, and the time away is forgotten as after a
23//! resume; muted, its audio goes to the device at volume 0.
24//!
25//! `App` keeps the log (ADR-0030): at the end of every frame it takes the
26//! lines the game logged ([`Ctx::log`]), stamps them with the frame's
27//! number and keeps the last [`LOG_LINES`] for tests, tools and the
28//! backend's sinks ([`App::take_new_log`]).
29//!
30//! In debug builds `App` also measures each frame (ADR-0046) with the
31//! platform's clock ([`Platform::clock`]), and keeps the last
32//! [`PERF_FRAMES`] records beside the log ([`App::perf`],
33//! [`App::take_new_perf`]). With the platform's memory tags
34//! ([`Platform::memory_scopes`]) it tags what each part of a frame
35//! allocates: `load` (the game's load), `paint` (presenting the game's
36//! frame), `perf` (its record), and `app` for the rest; the game's own
37//! frame tags `update`, `draw` and its spans.
38//!
39//! `App` also records a run, when told ([`App::record`], ADR-0072): it notes
40//! each value where it reads it from the [`Platform`], since only it sees
41//! the order of a frame's calls, the keyboard's handshake and the load.
42
43use crate::alloc_prelude::*;
44use alloc::format;
45use core::time::Duration;
46
47use serde::Serialize;
48use shuyi_assets::dialogue::LineId;
49use shuyi_assets::font::CellWidths;
50use shuyi_assets::voice::clip_file;
51use shuyi_core::made::Made;
52use shuyi_core::{StateHash, state_hash};
53use shuyi_ui::audio::{AudioRequest, MusicCommand};
54use shuyi_ui::frame::Rgb;
55use shuyi_ui::frame_cap::Pacing;
56use shuyi_ui::input::frame_seconds;
57use shuyi_ui::log::{LOG_LINES, LogCategory, LogFilter, LogLevel, LogLine, LogRing};
58use shuyi_ui::perf::{
59    FrameRecord, HeapSnapshot, MemoryScopes, PERF_FRAMES, PerfClock, PerfRing, TagClass,
60};
61use shuyi_ui::settings::{Background, BackgroundAudio, GameSettings};
62use shuyi_ui::storage::Storage;
63use shuyi_ui::system_keyboard::TextAnswer;
64use shuyi_ui::{Ctx, Game, GameData, IdleMotion, KeyPrompt, PlatformFacts, RawInputEvent};
65
66use crate::audio::{AudioFrame, SoundRequest};
67use crate::crash::{self, LastCrash};
68use crate::platform::{DisplayInfo, Frame, Look, Millis, Platform, Show, SystemEvent};
69use crate::session::{Recorder, SessionRecord};
70use crate::text_input::TextInput;
71use crate::tick::TickClock;
72
73/// What a frame tells the backend's loop.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[must_use]
76pub enum FrameOutcome {
77    /// Call [`App::frame`] again next frame.
78    Continue,
79    /// The game has quit (it asked to, or the player closed it). A backend
80    /// that can close does; one that can't (a web page) may go on calling
81    /// [`App::frame`], which shows the last frame in silence.
82    Quit,
83}
84
85/// What the screen shows while the game's data loads, from the first frame
86/// (a slow device takes seconds to load a game, and shows nothing until a
87/// frame is drawn). Drawn by the backend with what it has of its own, since
88/// the game's font isn't loaded yet.
89#[derive(Debug, Clone, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum Card {
92    /// A line of text (the game's name, say), centred, in the backend's
93    /// own font.
94    Text {
95        /// The line.
96        line: String,
97        /// Its colour.
98        ink: Rgb,
99        /// The colour of the rest of the screen.
100        background: Rgb,
101    },
102    /// A picture from the game's files, by path, centred at the largest
103    /// whole scale that fits.
104    Picture {
105        /// Its path among the game's files.
106        path: String,
107        /// The colour of the rest of the screen.
108        background: Rgb,
109    },
110}
111
112/// White, a card's ink until said.
113const WHITE: Rgb = Rgb::new(255, 255, 255);
114/// Black, a card's background until said.
115const BLACK: Rgb = Rgb::new(0, 0, 0);
116
117impl Card {
118    /// `line` in white on black.
119    pub fn text(line: impl Into<String>) -> Self {
120        Self::Text {
121            line: line.into(),
122            ink: WHITE,
123            background: BLACK,
124        }
125    }
126
127    /// The picture at `path` among the game's files, on black.
128    pub fn picture(path: impl Into<String>) -> Self {
129        Self::Picture {
130            path: path.into(),
131            background: BLACK,
132        }
133    }
134
135    /// The same card in `ink` (a picture has none) on `background`.
136    #[must_use]
137    pub fn colored(self, ink: Rgb, background: Rgb) -> Self {
138        match self {
139            Self::Text { line, .. } => Self::Text {
140                line,
141                ink,
142                background,
143            },
144            Self::Picture { path, .. } => Self::Picture { path, background },
145        }
146    }
147
148    /// The colour around what it shows.
149    pub fn background(&self) -> Rgb {
150        match self {
151            Self::Text { background, .. } | Self::Picture { background, .. } => *background,
152        }
153    }
154}
155
156/// What the platform gives a game as it loads: what goes into its context
157/// ([`ctx`](Self::ctx)). A backend with more to give (the voice manifest it
158/// read) adds it with [`Start::before_load`].
159#[derive(Debug)]
160#[non_exhaustive]
161pub struct Setup {
162    /// The display, for a game that picks its console to fit it.
163    pub display: DisplayInfo,
164    /// Where the game's saves and settings persist ([`Platform::storage`]).
165    pub storage: Box<dyn Storage>,
166    /// A seed from the clock, different each launch: the music's random
167    /// picks, and a game's own luck where it wants that.
168    pub seed: u64,
169    /// The text of the voice manifest the backend read, if it found one.
170    pub voice_manifest: Option<String>,
171    /// Where the game runs: the target's id, whether the game may quit,
172    /// whether its window goes full screen, and the keyboard, pointer and
173    /// touch screen the device has ([`Platform::facts`]).
174    pub platform: PlatformFacts,
175    /// Whether the game waits for a first key press (browsers block sound
176    /// until one): [`KeyPrompt::Waiting`] unless the backend says not.
177    pub key_prompt: KeyPrompt,
178    /// The last crash, if the game hasn't heard of it: the newest report in
179    /// [`storage`](Self::storage) newer than the last one offered
180    /// ([`crash::offer_last_crash`]), so each is offered once. What the
181    /// player sees of it, if anything, is the game's to word.
182    pub last_crash: Option<LastCrash>,
183}
184
185impl Setup {
186    /// `ctx` on this platform: its saves and settings in the platform's
187    /// storage, the voices the voice manifest allows, idle motion on, the
188    /// platform's facts and the key prompt the backend says, and the music
189    /// seeded from the clock so each launch picks differently.
190    pub fn ctx<G: GameData>(self, mut ctx: Ctx<G>) -> Ctx<G> {
191        ctx.idle_motion = IdleMotion::On;
192        ctx.music_seed = self.seed;
193        ctx.key_prompt = self.key_prompt;
194        if let Some(device) = self.platform.device {
195            ctx.device = device;
196        }
197        ctx.platform = self.platform;
198        let mut ctx = ctx.with_storage(self.storage);
199        if let Some(manifest) = &self.voice_manifest {
200            ctx.set_voice_manifest(manifest);
201        }
202        ctx
203    }
204}
205
206/// A game's load: its data read and its game made from the [`Setup`], or
207/// why it can't start (shown instead of the game).
208pub type Load<G> = Box<dyn FnOnce(Setup) -> Result<Game<G>, String>>;
209
210/// Where a game's frame finds what it is painted with, in its data.
211pub type LookOf<G> = fn(&G) -> Look<'_>;
212
213/// The game's hook for the system's events: it hears each one after `App`
214/// has done its part. On [`SystemEvent::Suspend`],
215/// [`QuitRequested`](SystemEvent::QuitRequested) and
216/// [`LowBattery`](SystemEvent::LowBattery) it is where the game saves what
217/// it hasn't; on [`FocusLost`](SystemEvent::FocusLost), whose held input
218/// `App` has let go of and whose [`GameSettings::BACKGROUND`] it applies,
219/// it may do more (open a pause menu).
220pub type SystemHook<G> = fn(&mut Game<G>, SystemEvent);
221
222/// The game's own state hash, for the frame's hash ([`Start::state_hash`]).
223pub type GameHash<G> = fn(&Game<G>) -> Option<StateHash>;
224
225/// The game's record of its play as text, in the format its tools' `replay`
226/// reads, for a crash report ([`Start::crash_record`]); `None` when it has
227/// none (no stage is open).
228pub type CrashRecord<G> = fn(&Game<G>) -> Option<String>;
229
230/// How a game starts: the card shown while it loads, the load, where its
231/// look is and its hook for the system's events. Built with [`Start::new`],
232/// its optional parts set by the methods named for them (ADR-0039).
233#[non_exhaustive]
234pub struct Start<G: GameData> {
235    /// What the first frame shows.
236    pub card: Card,
237    /// Runs on the second frame, the card on the screen.
238    pub load: Load<G>,
239    /// What the game's frames are painted with.
240    pub look: LookOf<G>,
241    /// The game's hook for the system's events, if it has one.
242    pub on_system: Option<SystemHook<G>>,
243    /// The build's stamp, copied into a session record.
244    pub made: Option<Made>,
245    /// The game's own state hash, in each frame's hash.
246    pub state_hash: Option<GameHash<G>>,
247    /// The game's record of its play, for a crash report.
248    pub crash_record: Option<CrashRecord<G>>,
249}
250
251impl<G: GameData> Start<G> {
252    /// A game that shows `card`, then loads with `load`, painted with what
253    /// `look` finds in its data.
254    pub fn new(
255        card: Card,
256        look: LookOf<G>,
257        load: impl FnOnce(Setup) -> Result<Game<G>, String> + 'static,
258    ) -> Self {
259        Self {
260            card,
261            load: Box::new(load),
262            look,
263            on_system: None,
264            made: None,
265            state_hash: None,
266            crash_record: None,
267        }
268    }
269
270    /// The same start with the game's hook for the system's events.
271    #[must_use]
272    pub fn on_system(mut self, hook: SystemHook<G>) -> Self {
273        self.on_system = Some(hook);
274        self
275    }
276
277    /// The same start with the build's stamp, copied into a session record
278    /// and checked when one is played ([`session::prepare`]).
279    ///
280    /// [`session::prepare`]: crate::session::prepare
281    #[must_use]
282    pub fn made(mut self, made: Made) -> Self {
283        self.made = Some(made);
284        self
285    }
286
287    /// The same start with the game's own state hash, in each frame's hash
288    /// ([`App::frame_hash`]): so a playback that plays differently is found
289    /// at the frame the game's state moved, not only when a screen or a
290    /// sound shows it.
291    #[must_use]
292    pub fn state_hash(mut self, hash: GameHash<G>) -> Self {
293        self.state_hash = Some(hash);
294        self
295    }
296
297    /// The same start with the game's record of its play, which a backend
298    /// that catches a panic adds to its crash report
299    /// ([`App::crash_record`]). A record should not hold what the player
300    /// typed (a name).
301    #[must_use]
302    pub fn crash_record(mut self, hook: CrashRecord<G>) -> Self {
303        self.crash_record = Some(hook);
304        self
305    }
306
307    /// The same start with `before` run on the setup ahead of the game's
308    /// load: a backend's own part (the voice manifest it read).
309    #[must_use]
310    pub fn before_load(self, before: impl FnOnce(&mut Setup) + 'static) -> Self {
311        let load = self.load;
312        Self {
313            load: Box::new(move |mut setup| {
314                before(&mut setup);
315                load(setup)
316            }),
317            ..self
318        }
319    }
320}
321
322/// Where the game is.
323enum Phase<G: GameData> {
324    /// Loading: the card, and the load until it has run.
325    Loading {
326        card: Card,
327        load: Option<Load<G>>,
328        /// Whether a frame has shown the card.
329        shown: bool,
330    },
331    /// The game runs.
332    Running(Box<Game<G>>),
333    /// The load failed: why.
334    Failed(String),
335}
336
337/// The time one frame covers.
338#[derive(Debug, Clone, Copy)]
339struct Elapsed {
340    /// The time on the backend's clock, for the sound device.
341    now: Millis,
342    /// Seconds since the last frame, for the screens.
343    dt: f32,
344    /// Fixed simulation steps due.
345    ticks: u32,
346}
347
348/// `App`'s clock: the backend's time at the last frame, or the driver's
349/// own, and the fixed steps owed.
350#[derive(Debug, Default)]
351struct Clock {
352    /// The backend's time at the last frame; `None` before the first and
353    /// after a restart, when a frame counts no time.
354    last: Option<Millis>,
355    /// The time on a driver's own clock ([`App::frame_after`]).
356    driven: Duration,
357    ticks: TickClock,
358}
359
360impl Clock {
361    /// A frame at `now` on the backend's clock.
362    fn at(&mut self, now: Millis) -> Elapsed {
363        let elapsed = self.last.map_or(Duration::ZERO, |last| now.since(last));
364        self.last = Some(now);
365        Elapsed {
366            now,
367            dt: elapsed.as_secs_f32(),
368            ticks: self.ticks.advance(elapsed),
369        }
370    }
371
372    /// A frame `seconds` after the last on the driver's own clock.
373    fn after(&mut self, seconds: f32) -> Elapsed {
374        let dt = frame_seconds(seconds);
375        let elapsed = Duration::try_from_secs_f32(dt).unwrap_or(Duration::ZERO);
376        self.driven = self.driven.saturating_add(elapsed);
377        Elapsed {
378            now: Millis(0).after(self.driven),
379            dt,
380            ticks: self.ticks.advance(elapsed),
381        }
382    }
383
384    /// Forgets the time since the last frame and the steps owed: the next
385    /// frame starts counting again.
386    fn restart(&mut self) {
387        self.last = None;
388        self.ticks.reset();
389    }
390}
391
392/// What `App` measures in debug builds (ADR-0046): the platform's clock
393/// until the game's context takes it, the load's time, this frame's
394/// times, and the last frames' records.
395struct Perf {
396    /// Whether the platform's clock was asked for (on the first frame).
397    asked: bool,
398    /// The platform's clock, until the game runs and its context takes it.
399    clock: Option<PerfClock>,
400    /// The platform's memory tags, until the game runs and its context
401    /// takes them.
402    scopes: Option<MemoryScopes>,
403    /// The `app` tag this frame entered, and the scopes it entered with.
404    app_tag: Entered,
405    /// How long the game's load took.
406    load_us: Option<u32>,
407    /// This frame's start, while it is measured.
408    start: Option<u64>,
409    /// This frame's `present` call, once it is measured.
410    paint_us: Option<u32>,
411    /// The end of the last frame measured; `None` after a frame that
412    /// wasn't, so the time away isn't counted as a wait.
413    last_end: Option<u64>,
414    records: PerfRing,
415    /// The sequence number of the first record [`App::take_new_perf`]
416    /// hasn't given.
417    read: u64,
418}
419
420impl Perf {
421    fn new() -> Self {
422        Self {
423            asked: false,
424            clock: None,
425            scopes: None,
426            app_tag: None,
427            load_us: None,
428            start: None,
429            paint_us: None,
430            last_end: None,
431            records: PerfRing::new(PERF_FRAMES),
432            read: 0,
433        }
434    }
435
436    /// The time on `game`'s clock, in a measured frame only.
437    fn now<G: GameData>(&self, game: &mut Game<G>) -> Option<u64> {
438        self.start?;
439        game.ctx_mut().perf_now()
440    }
441}
442
443/// A phase tag entered, and the scopes that entered it, to leave it with.
444type Entered = Option<(MemoryScopes, u16)>;
445
446/// Enters the phase tag `name`, with `scopes`.
447fn enter(scopes: Option<MemoryScopes>, name: &'static str) -> Entered {
448    scopes.map(|scopes| (scopes, scopes.enter(TagClass::Phase, name)))
449}
450
451/// Leaves the tag [`enter`] entered.
452fn leave(entered: Entered) {
453    if let Some((scopes, before)) = entered {
454        scopes.leave(before);
455    }
456}
457
458/// The microseconds from `from` to `to`: none if `to` is earlier, and at
459/// most `u32::MAX`.
460fn micros(from: u64, to: u64) -> u32 {
461    u32::try_from(to.saturating_sub(from)).unwrap_or(u32::MAX)
462}
463
464/// What one frame's hash covers: what happened, never the picture, so a
465/// skin or palette change breaks no recorded session.
466#[derive(Serialize)]
467struct FrameFacts<'a> {
468    phase: PhaseFact<'a>,
469    /// The open screens' names, bottom first.
470    screens: Vec<&'static str>,
471    quit: bool,
472    sounds: &'a [SoundRequest],
473    music: &'a [MusicCommand],
474    /// The game's own state hash, if its [`Start`] gives one.
475    game: Option<u64>,
476}
477
478/// Where the game is, in a frame's hash.
479#[derive(Serialize)]
480enum PhaseFact<'a> {
481    Loading,
482    Failed(&'a str),
483    Running,
484}
485
486/// The lists one frame fills, kept to save allocating them each frame.
487struct Lists<V> {
488    input: Vec<RawInputEvent>,
489    system: Vec<SystemEvent>,
490    sounds: Vec<SoundRequest>,
491    audio: Vec<AudioRequest<V>>,
492    music: Vec<MusicCommand>,
493    changed: Vec<String>,
494    /// The voice clip asked for last: its line, and its file as the sound
495    /// device names it.
496    voice: Option<(LineId, String)>,
497}
498
499/// The frames' hashes.
500#[derive(Debug, Clone, Copy, Default)]
501struct FrameHashes {
502    /// Whether frames are hashed when not recording
503    /// ([`hash_frames`](App::hash_frames)).
504    on: bool,
505    /// The last frame's ([`frame_hash`](App::frame_hash)).
506    last: Option<StateHash>,
507}
508
509/// A game run one frame at a time by a backend's loop.
510pub struct App<G: GameData> {
511    phase: Phase<G>,
512    look: Option<LookOf<G>>,
513    on_system: Option<SystemHook<G>>,
514    clock: Clock,
515    suspended: bool,
516    focused: bool,
517    /// What the game does without focus: its settings'
518    /// [`GameSettings::BACKGROUND`].
519    background: Background,
520    quit: bool,
521    lists: Lists<G::Voice>,
522    /// Frames run: 0 before the first.
523    frames: u32,
524    /// The last lines logged, stamped with their frame.
525    log: LogRing,
526    /// What enters `log`.
527    log_filter: LogFilter,
528    /// The sequence number of the first line [`take_new_log`] hasn't
529    /// given.
530    ///
531    /// [`take_new_log`]: Self::take_new_log
532    log_read: u64,
533    perf: Perf,
534    /// The backend's report of its pacing, until the next frame that runs
535    /// the game hands it over ([`set_pacing`](Self::set_pacing)).
536    pacing: Option<Pacing>,
537    /// The build's stamp ([`Start::made`]).
538    made: Option<Made>,
539    /// The game's own state hash ([`Start::state_hash`]).
540    game_hash: Option<GameHash<G>>,
541    /// The game's record of its play ([`Start::crash_record`]).
542    crash_record: Option<CrashRecord<G>>,
543    /// The run being recorded, once [`record`](Self::record) says.
544    recorder: Option<Box<Recorder>>,
545    frame_hashes: FrameHashes,
546    /// The fixed steps the last frame handed the game ([`ticks`](Self::ticks)).
547    ticks: u32,
548}
549
550impl<G: GameData> App<G> {
551    /// A game that starts as `start` says: the card, then the load.
552    pub fn new(start: Start<G>) -> Self {
553        let Start {
554            card,
555            load,
556            look,
557            on_system,
558            made,
559            state_hash,
560            crash_record,
561        } = start;
562        let mut app = Self::in_phase(
563            Phase::Loading {
564                card,
565                load: Some(load),
566                shown: false,
567            },
568            Some(look),
569            on_system,
570        );
571        app.made = made;
572        app.game_hash = state_hash;
573        app.crash_record = crash_record;
574        app
575    }
576
577    /// `game`, already loaded and running: for a test, which has no look to
578    /// paint with ([`Show::Game`]'s `look` is `None`).
579    /// What its context logged before is taken, at frame 0.
580    pub fn running(game: Game<G>) -> Self {
581        let mut app = Self::in_phase(Phase::Running(Box::new(game)), None, None);
582        app.take_game_log();
583        app
584    }
585
586    fn in_phase(
587        phase: Phase<G>,
588        look: Option<LookOf<G>>,
589        on_system: Option<SystemHook<G>>,
590    ) -> Self {
591        Self {
592            phase,
593            look,
594            on_system,
595            clock: Clock::default(),
596            suspended: false,
597            focused: true,
598            background: <G::Settings as GameSettings>::BACKGROUND,
599            quit: false,
600            lists: Lists {
601                input: Vec::new(),
602                system: Vec::new(),
603                sounds: Vec::new(),
604                audio: Vec::new(),
605                music: Vec::new(),
606                changed: Vec::new(),
607                voice: None,
608            },
609            frames: 0,
610            log: LogRing::new(LOG_LINES),
611            log_filter: LogFilter::default(),
612            log_read: 0,
613            perf: Perf::new(),
614            pacing: None,
615            made: None,
616            game_hash: None,
617            crash_record: None,
618            recorder: None,
619            frame_hashes: FrameHashes::default(),
620            ticks: 0,
621        }
622    }
623
624    /// Gives the game `hook` for the system's events, in place of any it
625    /// had.
626    pub fn set_system_hook(&mut self, hook: SystemHook<G>) {
627        self.on_system = Some(hook);
628    }
629
630    /// Runs one frame at `now` on the backend's clock: the system's events,
631    /// then the input, the game's update and its fixed steps, the audio,
632    /// and the frame to present. The time since the last frame is what the
633    /// screens see and the steps are made of; the first frame, and the
634    /// first after a load or a [`SystemEvent::Resume`], count none.
635    pub fn frame(&mut self, platform: &mut impl Platform, now: Millis) -> FrameOutcome {
636        if let Some(recorder) = &mut self.recorder {
637            recorder.begin(now.0);
638        }
639        self.begin(platform);
640        let elapsed = self.clock.at(now);
641        self.end(platform, elapsed)
642    }
643
644    /// Takes the backend's report of how it paces the frames: the frame
645    /// cap it applied, with the serial the cap came with on the last frame
646    /// it was given ([`Frame::frame_cap_serial`]), and what it measured. A
647    /// backend that paces calls this between frames, and the next frame
648    /// that runs the game hands it over before the update
649    /// ([`Game::set_pacing`]), so a request for a cap is confirmed. A
650    /// later report before that frame replaces it.
651    pub fn set_pacing(&mut self, pacing: Pacing) {
652        self.pacing = Some(pacing);
653    }
654
655    /// Presents the last frame again without running one: no input is
656    /// read, nothing steps and no time passes for the game. For a backend
657    /// whose display wants a picture on a frame the game isn't due (the
658    /// web under a frame cap, where the browser sets the rate and clears
659    /// the picture each frame). Before anything is shown, shows nothing.
660    pub fn present_again(&mut self, platform: &mut impl Platform) {
661        match &self.phase {
662            Phase::Loading {
663                card, shown: true, ..
664            } => platform.present(&Frame::new(Show::Card(card))),
665            Phase::Loading { .. } => {}
666            Phase::Running(game) => present_tagged(platform, game, self.look),
667            Phase::Failed(errors) => platform.present(&Frame::new(Show::Errors(errors))),
668        }
669    }
670
671    /// Runs one frame `seconds` after the last on a clock of the driver's
672    /// own, as [`frame`](Self::frame) does otherwise: for a driver whose
673    /// frames a clock in whole milliseconds can't give (the test Harness's,
674    /// exactly 1/60 s), or a backend whose system says how long the last
675    /// frame took. A driver uses this or `frame`, never both. A recording
676    /// stops here, since a record's time is whole milliseconds, and says so
677    /// ([`SessionRecord::truncated`]).
678    pub fn frame_after(&mut self, platform: &mut impl Platform, seconds: f32) -> FrameOutcome {
679        if let Some(recorder) = &mut self.recorder {
680            recorder.stop();
681        }
682        self.begin(platform);
683        let elapsed = self.clock.after(seconds);
684        self.end(platform, elapsed)
685    }
686
687    /// A frame's start: its number, its time if it is measured, and the
688    /// system's events.
689    fn begin(&mut self, platform: &mut impl Platform) {
690        self.frames = self.frames.saturating_add(1);
691        self.take_clock(platform);
692        self.perf.app_tag = enter(self.memory_scopes(), "app");
693        self.begin_perf();
694        self.system_events(platform);
695    }
696
697    /// The rest of a frame, then the lines the game logged in it and its
698    /// record.
699    fn end(&mut self, platform: &mut impl Platform, elapsed: Elapsed) -> FrameOutcome {
700        let outcome = self.run(platform, elapsed);
701        self.take_game_log();
702        let tagged = enter(self.memory_scopes(), "perf");
703        self.end_perf(platform);
704        leave(tagged);
705        leave(self.perf.app_tag.take());
706        self.end_session_frame();
707        outcome
708    }
709
710    /// Hashes the frame just run, while recording or asked to
711    /// ([`hash_frames`](Self::hash_frames)), and notes it and its input.
712    fn end_session_frame(&mut self) {
713        let recording = self.recorder.as_ref().is_some_and(|r| r.is_on());
714        if !(recording || self.frame_hashes.on) {
715            self.frame_hashes.last = None;
716            return;
717        }
718        let (phase, screens, game) = match &self.phase {
719            Phase::Loading { .. } => (PhaseFact::Loading, Vec::new(), None),
720            Phase::Failed(errors) => (PhaseFact::Failed(errors), Vec::new(), None),
721            Phase::Running(game) => (
722                PhaseFact::Running,
723                game.screens(),
724                self.game_hash
725                    .and_then(|hash| hash(game))
726                    .map(|hash| hash.0),
727            ),
728        };
729        let facts = FrameFacts {
730            phase,
731            screens,
732            quit: self.quit,
733            sounds: &self.lists.sounds,
734            music: &self.lists.music,
735            game,
736        };
737        // Strings, numbers and lists: nothing here refuses to serialize.
738        let hash = state_hash(&facts).unwrap_or_default();
739        self.frame_hashes.last = Some(hash);
740        if let Some(recorder) = &mut self.recorder {
741            recorder.end(&self.lists.input, hash);
742        }
743    }
744
745    /// Takes the platform's clock and memory tags on the first frame, in a
746    /// build with debug tools, and hands them to a game already running.
747    fn take_clock(&mut self, platform: &impl Platform) {
748        if core::mem::replace(&mut self.perf.asked, true) {
749            return;
750        }
751        if shuyi_ui::DEBUG_TOOLS {
752            self.perf.clock = platform.clock();
753            self.perf.scopes = platform.memory_scopes();
754        }
755        self.give_clock();
756    }
757
758    /// Moves the clock and memory tags `App` holds into the running game's
759    /// context, each unless that has its own already (a test's).
760    fn give_clock(&mut self) {
761        let Phase::Running(game) = &mut self.phase else {
762            return;
763        };
764        let ctx = game.ctx_mut();
765        if let Some(clock) = self.perf.clock.take()
766            && ctx.perf_clock().is_none()
767        {
768            ctx.set_perf_clock(Some(clock));
769        }
770        if let Some(scopes) = self.perf.scopes.take()
771            && ctx.memory_scopes().is_none()
772        {
773            ctx.set_memory_scopes(Some(scopes));
774        }
775    }
776
777    /// The memory tags: the running game's context's, or `App`'s own
778    /// before it runs.
779    fn memory_scopes(&self) -> Option<MemoryScopes> {
780        match &self.phase {
781            Phase::Running(game) => game.ctx().memory_scopes(),
782            Phase::Loading { .. } | Phase::Failed(_) => self.perf.scopes,
783        }
784    }
785
786    /// Starts measuring this frame: one of the running game's, not
787    /// suspended or quit, with debug tools on and a clock in its context.
788    fn begin_perf(&mut self) {
789        self.perf.start = None;
790        self.perf.paint_us = None;
791        if self.suspended || self.quit || self.is_paused() {
792            return;
793        }
794        if let Phase::Running(game) = &mut self.phase
795            && game.ctx().debug_tools
796        {
797            self.perf.start = game.ctx_mut().begin_perf_frame();
798        }
799    }
800
801    /// Ends this frame's measuring: its record, if it stepped the game and
802    /// presented it, with the platform's memory read at its end.
803    fn end_perf(&mut self, platform: &mut impl Platform) {
804        let Some(start) = self.perf.start.take() else {
805            self.perf.last_end = None;
806            return;
807        };
808        let Phase::Running(game) = &mut self.phase else {
809            return;
810        };
811        let ctx = game.ctx_mut();
812        let end = ctx.perf_now();
813        let measured = ctx.end_perf_frame();
814        let (Some(end), Some(mut record), Some(paint_us)) =
815            (end, measured, self.perf.paint_us.take())
816        else {
817            // Suspended or quit in this frame: no record, and the time
818            // away is no wait.
819            self.perf.last_end = None;
820            return;
821        };
822        let present_us = self.perf.last_end.map_or(0, |last| micros(last, start));
823        record.frame = self.frames;
824        record.times.paint_us = paint_us;
825        record.times.present_us = present_us;
826        record.times.frame_us = present_us.saturating_add(micros(start, end));
827        record.heap = platform.memory().map(|memory| memory.heap);
828        self.perf.last_end = Some(end);
829        self.perf.records.push(record);
830    }
831
832    /// Takes the lines the game has logged, stamped with this frame, and
833    /// counts those its context dropped.
834    fn take_game_log(&mut self) {
835        let Phase::Running(game) = &mut self.phase else {
836            return;
837        };
838        let (lines, dropped) = game.ctx_mut().take_log();
839        self.log.add_dropped(dropped);
840        for line in lines {
841            self.log_line(line.level, line.category, line.text);
842        }
843    }
844
845    /// Keeps a line of this frame's, if the filter lets it in.
846    fn log_line(&mut self, level: LogLevel, category: LogCategory, text: String) {
847        if self.log_filter.allows(category, level) {
848            self.log
849                .push(LogLine::new(self.frames, level, category, text));
850        }
851    }
852
853    /// Reads the system's events and acts on them; a resume restarts the
854    /// clock before this frame reads it.
855    fn system_events(&mut self, platform: &mut impl Platform) {
856        let mut events = core::mem::take(&mut self.lists.system);
857        events.clear();
858        platform.system_events(&mut events);
859        if let Some(recorder) = &mut self.recorder {
860            recorder.system(&events);
861        }
862        for &event in &events {
863            self.on_system_event(platform, event);
864        }
865        self.lists.system = events;
866    }
867
868    fn on_system_event(&mut self, platform: &mut impl Platform, event: SystemEvent) {
869        if self.quit {
870            return;
871        }
872        match event {
873            SystemEvent::Suspend => {
874                self.suspended = true;
875                platform.audio().suspend();
876            }
877            SystemEvent::Resume => {
878                self.suspended = false;
879                self.clock.restart();
880                platform.audio().resume();
881                // A tick stage forgets what it kept from before, and the
882                // input lets go of what was held.
883                if let Some(game) = self.game_mut() {
884                    game.set_resumed();
885                }
886            }
887            SystemEvent::FocusLost => {
888                self.focused = false;
889                // A window without focus hears no release: let go now, so
890                // nothing held repeats while the player is elsewhere.
891                if let Some(game) = self.game_mut() {
892                    game.forget_held_input();
893                }
894            }
895            SystemEvent::FocusGained => {
896                if self.is_paused() {
897                    // As after a resume: the time away makes no steps.
898                    self.clock.restart();
899                }
900                self.focused = true;
901            }
902            SystemEvent::QuitRequested => self.quit = true,
903            SystemEvent::ToggleFullscreen => {
904                if let Some(game) = self.game_mut() {
905                    game.toggle_fullscreen();
906                }
907            }
908            SystemEvent::LowBattery => {}
909            SystemEvent::CrankDocked | SystemEvent::CrankUndocked => {
910                if let Some(game) = self.game_mut() {
911                    game.ctx_mut().crank_docked = event == SystemEvent::CrankDocked;
912                }
913            }
914        }
915        if let (Some(hook), Phase::Running(game)) = (self.on_system, &mut self.phase) {
916            hook(game, event);
917        }
918    }
919
920    fn run(&mut self, platform: &mut impl Platform, elapsed: Elapsed) -> FrameOutcome {
921        self.lists.audio.clear();
922        self.lists.music.clear();
923        self.lists.sounds.clear();
924        self.ticks = 0;
925        if self.suspended && !self.quit {
926            // Suspended: nothing steps and nothing is shown.
927            drain(platform, &mut self.lists.input);
928            return FrameOutcome::Continue;
929        }
930        let mut loading = None;
931        let paused = self.is_paused();
932        let muted = self.is_muted();
933        match &mut self.phase {
934            Phase::Loading { card, load, shown } => {
935                if *shown && !self.quit {
936                    loading = load.take();
937                } else {
938                    *shown = true;
939                    platform.present(&Frame::new(Show::Card(card)));
940                    drain(platform, &mut self.lists.input);
941                }
942            }
943            Phase::Running(game) if paused => {
944                // Paused without focus: no update, its last frame again,
945                // and in silence if it is muted too.
946                drain(platform, &mut self.lists.input);
947                if muted {
948                    platform.audio().play(&silent(&[], &[]), elapsed.now);
949                }
950                present_tagged(platform, game, self.look);
951            }
952            Phase::Running(game) if !self.quit => {
953                let recording = self.recorder.as_ref().is_some_and(|r| r.is_on());
954                game.ctx_mut()
955                    .set_session_recording(recording || self.frame_hashes.on);
956                let mut recorder = self.recorder.as_deref_mut();
957                if let Some(pacing) = self.pacing.take() {
958                    game.set_pacing(pacing);
959                    if let Some(recorder) = recorder.as_deref_mut() {
960                        recorder.handed_pacing(pacing);
961                    }
962                }
963                let told = tell_changed(platform, game, &mut self.lists.changed);
964                if told {
965                    // A reloaded font atlas may have new wide glyphs.
966                    use_look_widths(game, self.look);
967                }
968                if let Some(recorder) = recorder.as_deref_mut().filter(|_| told) {
969                    recorder.reloaded(self.frames);
970                }
971                step(platform, game, &mut self.lists, elapsed, muted, recorder);
972                self.ticks = elapsed.ticks;
973                if game.quit_requested() {
974                    platform.audio().stop_all();
975                    self.quit = true;
976                }
977                let painting = self.perf.now(game);
978                present_tagged(platform, game, self.look);
979                let painted = self.perf.now(game);
980                self.perf.paint_us = painting.zip(painted).map(|(from, to)| micros(from, to));
981            }
982            Phase::Running(game) => {
983                // The game has quit: its last frame, in silence.
984                drain(platform, &mut self.lists.input);
985                present_tagged(platform, game, self.look);
986            }
987            Phase::Failed(errors) => {
988                platform.present(&Frame::new(Show::Errors(errors)));
989                drain(platform, &mut self.lists.input);
990            }
991        }
992        if let Some(load) = loading {
993            self.load(platform, load, elapsed.now);
994        }
995        if self.quit {
996            FrameOutcome::Quit
997        } else {
998            FrameOutcome::Continue
999        }
1000    }
1001
1002    /// Runs the game's load, the card on the screen, and shows the game it
1003    /// made, or why it couldn't. The game prints wide what its look's font
1004    /// atlas has wide ([`Game::set_widths`]).
1005    fn load(&mut self, platform: &mut impl Platform, load: Load<G>, now: Millis) {
1006        let mut setup = Setup {
1007            display: platform.display(),
1008            storage: platform.storage(),
1009            seed: mix(now.0),
1010            voice_manifest: None,
1011            platform: platform.facts(),
1012            key_prompt: KeyPrompt::Waiting,
1013            last_crash: None,
1014        };
1015        if let Some(recorder) = &mut self.recorder {
1016            recorder.handed_over(setup.display, setup.storage.as_ref(), &setup.platform);
1017        }
1018        // After the record took the storage: a playback offers it again.
1019        setup.last_crash = crash::offer_last_crash(setup.storage.as_mut());
1020        drain(platform, &mut self.lists.input);
1021        let tagged = enter(self.perf.scopes, "load");
1022        let loading = self.perf.clock.as_mut().map(PerfClock::read);
1023        let loaded = load(setup);
1024        let done = self.perf.clock.as_mut().map(PerfClock::read);
1025        leave(tagged);
1026        self.perf.load_us = loading.zip(done).map(|(from, to)| micros(from, to));
1027        match loaded {
1028            Ok(mut game) => {
1029                use_look_widths(&mut game, self.look);
1030                present_game(platform, &game, self.look);
1031                self.phase = Phase::Running(Box::new(game));
1032                self.give_clock();
1033            }
1034            Err(errors) => {
1035                let (level, category) = (LogLevel::Error, LogCategory::Platform);
1036                self.log_line(level, category, format!("the game didn't load: {errors}"));
1037                platform.present(&Frame::new(Show::Errors(&errors)));
1038                self.phase = Phase::Failed(errors);
1039            }
1040        }
1041        // The game's time starts now, not before its load.
1042        self.clock.restart();
1043    }
1044
1045    /// Turns recording on (ADR-0072): `false` once a frame has run, since a
1046    /// record must start at the first frame to play back. While recording,
1047    /// a backend hands the game nothing around the [`Platform`]
1048    /// ([`game_mut`](Self::game_mut)), except through
1049    /// [`session_mut`](Self::session_mut) for what it puts into the [`Setup`].
1050    pub fn record(&mut self) -> bool {
1051        if self.frames > 0 {
1052            return false;
1053        }
1054        let made = self.made.clone();
1055        self.recorder
1056            .get_or_insert_with(|| Box::new(Recorder::new(made)));
1057        true
1058    }
1059
1060    /// The record so far, if this run is recorded.
1061    pub fn session(&self) -> Option<&SessionRecord> {
1062        self.recorder.as_ref().map(|recorder| &recorder.record)
1063    }
1064
1065    /// The record so far, for what a backend puts into the game's [`Setup`]
1066    /// and `App` can't see ([`SessionRecord::voice_manifest`]).
1067    pub fn session_mut(&mut self) -> Option<&mut SessionRecord> {
1068        self.recorder.as_mut().map(|recorder| &mut recorder.record)
1069    }
1070
1071    /// Ends the recording, handing back the record.
1072    pub fn take_session(&mut self) -> Option<SessionRecord> {
1073        self.recorder.take().map(|recorder| recorder.record)
1074    }
1075
1076    /// Whether a debug tool asked in the last frames to save the session
1077    /// ([`Ctx::ask_session_save`]): true once for each ask. The backend
1078    /// writes [`session`](Self::session), which goes on recording.
1079    pub fn take_session_save(&mut self) -> bool {
1080        self.game_mut()
1081            .is_some_and(|game| game.ctx_mut().take_session_save())
1082    }
1083
1084    /// The last frame's hash: of what happened in it, never its picture.
1085    /// `None` unless frames are hashed: while recording, or with
1086    /// [`hash_frames`](Self::hash_frames) on.
1087    pub fn frame_hash(&self) -> Option<StateHash> {
1088        self.frame_hashes.last
1089    }
1090
1091    /// Hashes each frame from now on ([`frame_hash`](Self::frame_hash)), or
1092    /// stops: what a playback compares.
1093    pub fn hash_frames(&mut self, on: bool) {
1094        self.frame_hashes.on = on;
1095    }
1096
1097    /// The game, once it has loaded.
1098    pub fn game(&self) -> Option<&Game<G>> {
1099        match &self.phase {
1100            Phase::Running(game) => Some(game),
1101            Phase::Loading { .. } | Phase::Failed(_) => None,
1102        }
1103    }
1104
1105    /// The game, once it has loaded, to change from outside a frame (a
1106    /// test, a platform's own menu: [`Game::act`]).
1107    pub fn game_mut(&mut self) -> Option<&mut Game<G>> {
1108        match &mut self.phase {
1109            Phase::Running(game) => Some(game),
1110            Phase::Loading { .. } | Phase::Failed(_) => None,
1111        }
1112    }
1113
1114    /// Ends the run, handing back the game if it loaded.
1115    pub fn into_game(self) -> Option<Game<G>> {
1116        match self.phase {
1117            Phase::Running(game) => Some(*game),
1118            Phase::Loading { .. } | Phase::Failed(_) => None,
1119        }
1120    }
1121
1122    /// The game's record of its play ([`Start::crash_record`]), for a crash
1123    /// report: `None` while it loads, after a failed load, or if it has no
1124    /// record.
1125    pub fn crash_record(&self) -> Option<String> {
1126        (self.crash_record?)(self.game()?)
1127    }
1128
1129    /// Whether the game hasn't loaded yet.
1130    pub fn is_loading(&self) -> bool {
1131        matches!(self.phase, Phase::Loading { .. })
1132    }
1133
1134    /// Why the game couldn't start, if its load failed.
1135    pub fn errors(&self) -> Option<&str> {
1136        match &self.phase {
1137            Phase::Failed(errors) => Some(errors),
1138            Phase::Loading { .. } | Phase::Running(_) => None,
1139        }
1140    }
1141
1142    /// Whether the system has suspended the game.
1143    pub fn is_suspended(&self) -> bool {
1144        self.suspended
1145    }
1146
1147    /// The fixed simulation steps the last frame handed the game
1148    /// ([`Game::set_ticks`]): 0 in a frame that didn't run it (loading,
1149    /// suspended, quit).
1150    pub fn ticks(&self) -> u32 {
1151        self.ticks
1152    }
1153
1154    /// Whether the game's window has the player's attention (until the
1155    /// system says otherwise, it has).
1156    pub fn is_focused(&self) -> bool {
1157        self.focused
1158    }
1159
1160    /// Whether the game is paused because its window has no focus and its
1161    /// [`GameSettings::BACKGROUND`] pauses it: it runs, hasn't quit, and
1162    /// isn't updated.
1163    pub fn is_paused(&self) -> bool {
1164        !self.focused
1165            && self.background.pause
1166            && !self.quit
1167            && matches!(self.phase, Phase::Running(_))
1168    }
1169
1170    /// Whether the game's audio goes to the device at volume 0: its window
1171    /// has no focus and its [`GameSettings::BACKGROUND`] mutes it.
1172    fn is_muted(&self) -> bool {
1173        !self.focused && self.background.audio == BackgroundAudio::Mute
1174    }
1175
1176    /// Whether the game has quit.
1177    pub fn has_quit(&self) -> bool {
1178        self.quit
1179    }
1180
1181    /// The frames run: 0 before the first, then one more for each
1182    /// [`frame`](Self::frame) or [`frame_after`](Self::frame_after). The
1183    /// frame the log's lines carry.
1184    pub fn frame_number(&self) -> u32 {
1185        self.frames
1186    }
1187
1188    /// Which lines the log keeps from now on: [`LogLevel::Info`] and more
1189    /// serious until set.
1190    pub fn set_log_filter(&mut self, filter: LogFilter) {
1191        self.log_filter = filter;
1192    }
1193
1194    /// The log: the last [`LOG_LINES`] lines, oldest first, and how many
1195    /// were dropped.
1196    pub fn log(&self) -> &LogRing {
1197        &self.log
1198    }
1199
1200    /// The log's lines since the last call, oldest first, for the
1201    /// backend's sinks: each line is given once.
1202    pub fn take_new_log(&mut self) -> Vec<LogLine> {
1203        let lines = self.log.since(self.log_read).cloned().collect();
1204        self.log_read = self.log.next_seq();
1205        lines
1206    }
1207
1208    /// The last [`PERF_FRAMES`] frames' records, oldest first, and how many
1209    /// were dropped (ADR-0046). A frame is measured in a build with debug
1210    /// tools, on a platform with a clock ([`Platform::clock`]), while the
1211    /// game's [`Ctx::debug_tools`] is on; a loading, suspended, failed or
1212    /// quit frame makes no record.
1213    pub fn perf(&self) -> &PerfRing {
1214        &self.perf.records
1215    }
1216
1217    /// The records since the last call, oldest first, for the backend's
1218    /// sinks: each record is given once.
1219    pub fn take_new_perf(&mut self) -> Vec<FrameRecord> {
1220        let records = self.perf.records.since(self.perf.read).cloned().collect();
1221        self.perf.read = self.perf.records.next_seq();
1222        records
1223    }
1224
1225    /// How long the game's load took, in microseconds, if it was timed: in
1226    /// a build with debug tools, on a platform with a clock.
1227    pub fn load_us(&self) -> Option<u32> {
1228        self.perf.load_us
1229    }
1230
1231    /// What holds the heap now, by memory tag (ADR-0046 §6): `platform`'s
1232    /// snapshot ([`Platform::heap_snapshot`]) stamped with
1233    /// [`frame_number`](Self::frame_number). Only while debug tools are on:
1234    /// in a build with them, and the running game's [`Ctx::debug_tools`]
1235    /// on; `None` otherwise, or where the platform takes none.
1236    pub fn heap_snapshot(&self, platform: &mut impl Platform) -> Option<HeapSnapshot> {
1237        let on = match &self.phase {
1238            Phase::Running(game) => game.ctx().debug_tools,
1239            Phase::Loading { .. } | Phase::Failed(_) => true,
1240        };
1241        if !(shuyi_ui::DEBUG_TOOLS && on) {
1242            return None;
1243        }
1244        let mut snapshot = platform.heap_snapshot()?;
1245        snapshot.frame = self.frames;
1246        Some(snapshot)
1247    }
1248
1249    /// The audio the screens asked for in the last frame, in order, as they
1250    /// asked (the device got it as [`SoundRequest`]s).
1251    pub fn audio(&self) -> &[AudioRequest<G::Voice>] {
1252        &self.lists.audio
1253    }
1254
1255    /// The music commands of the last frame, in order.
1256    pub fn music(&self) -> &[MusicCommand] {
1257        &self.lists.music
1258    }
1259}
1260
1261/// Reads the input into `input`, where nothing takes it: it stays there
1262/// until the next frame, for a session record.
1263fn drain(platform: &mut impl Platform, input: &mut Vec<RawInputEvent>) {
1264    input.clear();
1265    platform.poll_input(input);
1266}
1267
1268/// Has `game` print wide what the font atlas of its `look` has wide
1269/// ([`Game::set_widths`]), which draws it again if that changed: once it
1270/// has loaded, and after its files change, as the atlas may have.
1271fn use_look_widths<G: GameData>(game: &mut Game<G>, look: Option<LookOf<G>>) {
1272    if let Some(look) = look {
1273        let widths = CellWidths::of(look(&game.ctx().game).font);
1274        game.set_widths(widths);
1275    }
1276}
1277
1278/// Hands the files the platform says changed to `game`'s modules: while
1279/// debug tools are on, in a build with them. A build that ships never asks.
1280/// Whether it named any.
1281fn tell_changed<G: GameData>(
1282    platform: &mut impl Platform,
1283    game: &mut Game<G>,
1284    changed: &mut Vec<String>,
1285) -> bool {
1286    if !(shuyi_ui::DEBUG_TOOLS && game.ctx().debug_tools) {
1287        return false;
1288    }
1289    changed.clear();
1290    platform.changed_files(changed);
1291    let told = !changed.is_empty();
1292    if told {
1293        game.reloaded(changed);
1294    }
1295    told
1296}
1297
1298/// `requests` and `music` at volume 0: a muted frame's audio.
1299fn silent<'a>(requests: &'a [SoundRequest], music: &'a [MusicCommand]) -> AudioFrame<'a> {
1300    AudioFrame {
1301        requests,
1302        music,
1303        music_volume: 0.0,
1304        sound_volume: 0.0,
1305        voice_volume: 0.0,
1306    }
1307}
1308
1309/// One frame of the running game: the input, the system keyboard's
1310/// answer, the music sounding and the steps due go in; the update runs;
1311/// its audio goes to the device, at volume 0 if `muted`, and a keyboard
1312/// it asked for opens.
1313fn step<G: GameData>(
1314    platform: &mut impl Platform,
1315    game: &mut Game<G>,
1316    lists: &mut Lists<G::Voice>,
1317    elapsed: Elapsed,
1318    muted: bool,
1319    mut recorder: Option<&mut Recorder>,
1320) {
1321    lists.input.clear();
1322    platform.poll_input(&mut lists.input);
1323    let keyboard = &mut game.ctx_mut().system_keyboard;
1324    let text_input = platform.text_input();
1325    let present = text_input.is_some();
1326    keyboard.set_present(present);
1327    if let Some(recorder) = recorder.as_deref_mut() {
1328        recorder.keyboard_present(present);
1329    }
1330    if keyboard.is_waiting()
1331        && let Some(answer) = text_input.and_then(TextInput::poll)
1332    {
1333        if let Some(recorder) = recorder.as_deref_mut() {
1334            recorder.keyboard_answer(&answer);
1335        }
1336        keyboard.answer(answer);
1337    }
1338    let playing = platform.audio().music_playing(elapsed.now);
1339    if let Some(recorder) = recorder.as_deref_mut() {
1340        recorder.music(playing);
1341    }
1342    game.set_music_playing(playing);
1343    let voice = platform.audio().voice_playing(elapsed.now);
1344    if let Some(recorder) = recorder.as_deref_mut() {
1345        recorder.voice(voice);
1346    }
1347    let asked = lists.voice.as_ref();
1348    let line = asked.filter(|(_, file)| voice == Some(file.as_str()));
1349    game.set_voice_playing(line.map(|(line, _)| line));
1350    game.set_ticks(elapsed.ticks);
1351    let out = game.frame(&lists.input, elapsed.dt);
1352    lists.audio.extend_from_slice(out.audio);
1353    for request in out.audio {
1354        if let AudioRequest::PlayVoice { line, variant } = request {
1355            lists.voice = Some((line.clone(), clip_file(line, *variant)));
1356        }
1357    }
1358    lists.music.extend_from_slice(out.music);
1359    let (music_volume, sound_volume, voice_volume) =
1360        (out.music_volume, out.sound_volume, out.voice_volume);
1361    lists.sounds.clear();
1362    let sounds = lists.audio.iter().filter_map(SoundRequest::from_request);
1363    lists.sounds.extend(sounds);
1364    let audio = if muted {
1365        silent(&lists.sounds, &lists.music)
1366    } else {
1367        AudioFrame {
1368            requests: &lists.sounds,
1369            music: &lists.music,
1370            music_volume,
1371            sound_volume,
1372            voice_volume,
1373        }
1374    };
1375    platform.audio().play(&audio, elapsed.now);
1376    let keyboard = &mut game.ctx_mut().system_keyboard;
1377    if let Some(ask) = keyboard.take_ask() {
1378        let opened = platform.text_input().is_some_and(|input| input.open(&ask));
1379        if let Some(recorder) = recorder {
1380            recorder.keyboard_opened(opened);
1381        }
1382        if !opened {
1383            keyboard.answer(TextAnswer::Cancelled);
1384        }
1385    }
1386}
1387
1388/// Presents `game`'s last frame, on its background, in the window mode
1389/// and at the frame cap its settings ask for.
1390fn present_game<G: GameData>(
1391    platform: &mut impl Platform,
1392    game: &Game<G>,
1393    look: Option<LookOf<G>>,
1394) {
1395    let ctx = game.ctx();
1396    let show = Show::Game {
1397        buffer: game.buffer(),
1398        background: ctx.game.blank_cell(&ctx.palette).bg,
1399        look: look.map(|look| look(&ctx.game)),
1400    };
1401    let mut frame = Frame::new(show);
1402    frame.fullscreen = ctx.settings().fullscreen;
1403    frame.frame_cap = ctx.settings().frame_cap;
1404    frame.frame_cap_serial = ctx.frame_cap_serial();
1405    frame.reload_errors = ctx.reload_errors();
1406    platform.present(&frame);
1407}
1408
1409/// [`present_game`], under the `paint` tag with the game's memory tags.
1410fn present_tagged<G: GameData>(
1411    platform: &mut impl Platform,
1412    game: &Game<G>,
1413    look: Option<LookOf<G>>,
1414) {
1415    let tagged = enter(game.ctx().memory_scopes(), "paint");
1416    present_game(platform, game, look);
1417    leave(tagged);
1418}
1419
1420/// `x` mixed (`SplitMix64`'s finalizer), so two launches a millisecond
1421/// apart get seeds far apart.
1422fn mix(x: u64) -> u64 {
1423    let mut z = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
1424    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1425    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1426    z ^ (z >> 31)
1427}
1428
1429#[cfg(test)]
1430mod tests;