Skip to main content

shuyi_platform/
headless.rs

1//! [`HeadlessPlatform`]: a backend with no window, no clock, no heap
2//! counter and no memory tags (unless a test hands it them) and no sound
3//! card. It plays scripted input and system events, records the frames
4//! presented to it, keeps its storage in memory, and stands in for the
5//! music player ([`Speaker`]), so a screen that keeps time with the music
6//! is tested too.
7//!
8//! The test Harness runs games on it, through the same
9//! [`App::frame`](crate::App::frame) the devices do. It is also the
10//! smallest complete backend, for anyone writing one
11//! (`docs/platform-backends.md`).
12
13use crate::alloc_prelude::*;
14use alloc::collections::{BTreeMap, VecDeque};
15use alloc::vec;
16
17#[cfg(feature = "std")]
18use shuyi_assets::bundle::Layer;
19use shuyi_ui::audio::{MusicCommand, SharedSamples, SoundId};
20use shuyi_ui::frame::Rgb;
21use shuyi_ui::frame::glyph_buffer::GlyphBuffer;
22use shuyi_ui::frame_cap::FrameCap;
23use shuyi_ui::perf::{
24    HeapCounts, HeapSnapshot, MemoryReading, MemoryScopes, PerfClock, TagClass, TagRow,
25};
26use shuyi_ui::storage::{MemoryStorage, Storage};
27use shuyi_ui::{PlatformFacts, RawInputEvent};
28
29use crate::app::Card;
30use crate::audio::{Audio, AudioFrame, SoundRequest};
31use crate::platform::{DisplayInfo, Frame, Millis, Platform, Show, SystemEvent};
32use crate::session::{FrameRecord, KeyboardFrame, SessionRecord};
33use crate::text_input::{TextAnswer, TextAsk, TextInput};
34
35/// A frame as the headless backend keeps it.
36#[derive(Debug, Clone, PartialEq)]
37pub enum Presented {
38    /// The loading card.
39    Card(Card),
40    /// Why the game couldn't start.
41    Errors(String),
42    /// The game's console.
43    Game {
44        /// Its cells and pictures.
45        buffer: GlyphBuffer,
46        /// The colour behind it.
47        background: Rgb,
48        /// Whether it asked to fill the screen.
49        fullscreen: bool,
50    },
51}
52
53impl Presented {
54    /// The game's console, if this frame shows it.
55    pub fn buffer(&self) -> Option<&GlyphBuffer> {
56        match self {
57            Self::Game { buffer, .. } => Some(buffer),
58            Self::Card(_) | Self::Errors(_) => None,
59        }
60    }
61
62    /// The frame as it was presented, kept: `old`'s buffer is reused when
63    /// both show the game.
64    fn keep(old: &mut Option<Self>, frame: &Frame<'_>) {
65        match (old.as_mut(), frame.show) {
66            (
67                Some(Self::Game {
68                    buffer,
69                    background,
70                    fullscreen,
71                }),
72                Show::Game {
73                    buffer: new,
74                    background: new_background,
75                    ..
76                },
77            ) => {
78                if buffer != new {
79                    buffer.clone_from(new);
80                }
81                *background = new_background;
82                *fullscreen = frame.fullscreen;
83            }
84            (
85                _,
86                Show::Game {
87                    buffer, background, ..
88                },
89            ) => {
90                *old = Some(Self::Game {
91                    buffer: buffer.clone(),
92                    background,
93                    fullscreen: frame.fullscreen,
94                });
95            }
96            (_, Show::Card(card)) => *old = Some(Self::Card(card.clone())),
97            (_, Show::Errors(errors)) => *old = Some(Self::Errors(errors.to_owned())),
98        }
99    }
100}
101
102/// A sound started with a handle that the [`Speaker`] holds as playing.
103/// Only the `Speaker` builds one (ADR-0039).
104#[derive(Debug, Clone, PartialEq)]
105#[non_exhaustive]
106pub struct PlayingSound {
107    /// Its handle.
108    pub id: SoundId,
109    /// Its sound cue.
110    pub cue: String,
111    /// Whether [`SoundRequest::PauseSounds`] paused it.
112    pub paused: bool,
113}
114
115/// A play of a sound the game added at run time, as the [`Speaker`] heard
116/// it: the samples it played. Only the `Speaker` builds one (ADR-0039).
117#[derive(Debug, Clone, PartialEq)]
118#[non_exhaustive]
119pub struct AddedPlay {
120    /// The sound cue.
121    pub cue: String,
122    /// The samples it played.
123    pub samples: SharedSamples,
124    /// The volume asked for, 0–1: a multiplier on the sound's own, full.
125    pub volume: f32,
126    /// The handle it was started with, if it was.
127    pub id: Option<SoundId>,
128}
129
130/// The headless backend's sound device: it records what the last frame
131/// played, works out which music "sounds" from the music commands and the
132/// clock, and keeps the sounds started with a handle until they stop (one
133/// that plays once too: it knows no lengths). Like `shuyi-app`, it mutes
134/// rather than pauses: while [`suspended`](Self::suspended) its music
135/// clock runs on. It keeps the sounds the game added at run time, and
136/// hears every play of them ([`added_plays`](Self::added_plays)). A voice
137/// clip sounds from its request until it is stopped or replaced, or until
138/// the length a test gave clips has passed
139/// ([`set_voice_length`](Self::set_voice_length)).
140#[derive(Debug, Clone, Default)]
141pub struct Speaker {
142    /// Seconds a track takes to load: it sounds that long after its
143    /// `Start`.
144    load_delay: f64,
145    /// Whether no track ever sounds (the music files are missing).
146    silent: bool,
147    /// The track started last, and when.
148    started: Option<(String, Millis)>,
149    /// How long every voice clip lasts, in seconds; `None` until it is
150    /// stopped.
151    voice_length: Option<f64>,
152    /// The voice clip asked for last and not stopped, and when.
153    voice: Option<(String, Millis)>,
154    /// The last frame's sound and voice requests.
155    requests: Vec<SoundRequest>,
156    /// The last frame's music commands.
157    music: Vec<MusicCommand>,
158    /// The last frame's volumes: music, sound and voice. Full until told.
159    volumes: Option<(f32, f32, f32)>,
160    /// Whether everything was stopped (the game quit).
161    stopped: bool,
162    /// The sounds started with a handle and not stopped, in id order.
163    playing: Vec<PlayingSound>,
164    /// Whether the system suspended the game ([`Audio::suspend`]).
165    suspended: bool,
166    /// What a session record says is sounding, while one plays: the
167    /// answer to `music_playing` in place of the speaker's own.
168    recorded: Option<RecordedMusic>,
169    /// The sounds added and not removed, by cue.
170    added: BTreeMap<String, SharedSamples>,
171    /// Every play of a sound added, in order.
172    added_plays: Vec<AddedPlay>,
173}
174
175/// What a session record says is sounding.
176#[derive(Debug, Clone, Default)]
177struct RecordedMusic {
178    /// The cue of the track, written only when it changes.
179    track: String,
180    /// Its position this frame; `None` in silence.
181    position: Option<f64>,
182    /// The file of the voice clip sounding, written only when it changes;
183    /// empty for none.
184    voice: String,
185}
186
187impl Speaker {
188    /// Makes music take `seconds` to load: a track sounds that long after
189    /// the game starts it (at once by default). Negative or NaN counts as
190    /// 0.
191    pub fn set_load_delay(&mut self, seconds: f64) {
192        self.load_delay = seconds.max(0.0);
193    }
194
195    /// Makes every voice clip last `seconds`: it stops sounding that long
196    /// after it was asked for. Until this is called a clip sounds until it
197    /// is stopped or replaced. Negative or NaN counts as 0.
198    pub fn set_voice_length(&mut self, seconds: f64) {
199        self.voice_length = Some(seconds.max(0.0));
200    }
201
202    /// Makes no music ever sound, as when the music files are missing.
203    pub fn set_silent(&mut self) {
204        self.silent = true;
205    }
206
207    /// The sound and voice requests of the last frame that played any
208    /// audio.
209    pub fn requests(&self) -> &[SoundRequest] {
210        &self.requests
211    }
212
213    /// The music commands of the last frame that played audio.
214    pub fn music(&self) -> &[MusicCommand] {
215        &self.music
216    }
217
218    /// The music, sound and voice volumes the last frame played at, 0–1;
219    /// full before any.
220    pub fn volumes(&self) -> (f32, f32, f32) {
221        self.volumes.unwrap_or((1.0, 1.0, 1.0))
222    }
223
224    /// Whether the game stopped all its audio (it quit).
225    pub fn stopped(&self) -> bool {
226        self.stopped
227    }
228
229    /// The sounds started with a handle and not yet stopped, in id order.
230    pub fn sounds_playing(&self) -> &[PlayingSound] {
231        &self.playing
232    }
233
234    /// Whether the audio is held because the system suspended the game.
235    pub fn suspended(&self) -> bool {
236        self.suspended
237    }
238
239    /// The sounds the game added at run time and hasn't removed, by cue.
240    pub fn added_sounds(&self) -> &BTreeMap<String, SharedSamples> {
241        &self.added
242    }
243
244    /// Every play and start of a sound the game added, in order, since the
245    /// speaker was made: what each played.
246    pub fn added_plays(&self) -> &[AddedPlay] {
247        &self.added_plays
248    }
249
250    /// Hears a play of `cue`, if it is a sound added.
251    fn hear_added(&mut self, cue: &str, volume: f32, id: Option<SoundId>) {
252        if let Some(samples) = self.added.get(cue) {
253            self.added_plays.push(AddedPlay {
254                cue: cue.to_owned(),
255                samples: samples.clone(),
256                volume,
257                id,
258            });
259        }
260    }
261
262    /// One frame of a session record: the track if it changed, its
263    /// position, and the voice clip if it changed.
264    fn play_recorded(&mut self, frame: &FrameRecord) {
265        let recorded = self.recorded.get_or_insert_default();
266        if let Some(track) = &frame.track {
267            track.clone_into(&mut recorded.track);
268        }
269        recorded.position = frame.music;
270        if let Some(voice) = &frame.voice {
271            voice.clone_into(&mut recorded.voice);
272        }
273    }
274
275    /// Follows one sound request: a start adds a sound playing (or
276    /// replaces the one of its id, as a new game's queue counts from 1
277    /// again), a stop takes it away.
278    fn hear(&mut self, request: &SoundRequest, now: Millis) {
279        match request {
280            SoundRequest::Voice { clip } => self.voice = Some((clip.clone(), now)),
281            SoundRequest::StopVoice => self.voice = None,
282            SoundRequest::Sound { cue, volume } => self.hear_added(cue, *volume, None),
283            SoundRequest::Start {
284                id, cue, volume, ..
285            } => {
286                self.hear_added(cue, *volume, Some(*id));
287                let sound = PlayingSound {
288                    id: *id,
289                    cue: cue.clone(),
290                    paused: false,
291                };
292                match self.playing.binary_search_by_key(id, |p| p.id) {
293                    Ok(at) => self.playing[at] = sound,
294                    Err(at) => self.playing.insert(at, sound),
295                }
296            }
297            SoundRequest::Stop { id } => self.playing.retain(|p| p.id != *id),
298            SoundRequest::PauseSounds => self.playing.iter_mut().for_each(|p| p.paused = true),
299            SoundRequest::ResumeSounds => self.playing.iter_mut().for_each(|p| p.paused = false),
300            SoundRequest::Add { cue, samples, .. } => {
301                self.added.insert(cue.clone(), samples.clone());
302            }
303            SoundRequest::Remove { cue } if self.added.remove(cue).is_some() => {
304                self.playing.retain(|p| p.cue != *cue);
305            }
306            _ => {}
307        }
308    }
309
310    /// Follows one music command: a `Start` starts the count at `now`; a
311    /// `Stop` (or a fresh `Load`) of that track ends it.
312    fn apply(&mut self, command: &MusicCommand, now: Millis) {
313        match command {
314            MusicCommand::Start { cue, .. } => self.started = Some((cue.clone(), now)),
315            MusicCommand::Stop { cue } | MusicCommand::Load { cue } => {
316                if self.started.as_ref().is_some_and(|(c, _)| c == cue) {
317                    self.started = None;
318                }
319            }
320            MusicCommand::Gain { .. } => {}
321        }
322    }
323}
324
325impl Audio for Speaker {
326    fn music_playing(&self, now: Millis) -> Option<(&str, f64)> {
327        if let Some(recorded) = &self.recorded {
328            return recorded.position.map(|at| (recorded.track.as_str(), at));
329        }
330        let (cue, at) = self.started.as_ref().filter(|_| !self.silent)?;
331        let elapsed = now.since(*at).as_secs_f64() - self.load_delay;
332        (elapsed >= 0.0).then_some((cue.as_str(), elapsed))
333    }
334
335    fn voice_playing(&self, now: Millis) -> Option<&str> {
336        if let Some(recorded) = &self.recorded {
337            return Some(recorded.voice.as_str()).filter(|voice| !voice.is_empty());
338        }
339        let (clip, at) = self.voice.as_ref()?;
340        let on = self
341            .voice_length
342            .is_none_or(|length| now.since(*at).as_secs_f64() < length);
343        on.then_some(clip.as_str())
344    }
345
346    fn play(&mut self, frame: &AudioFrame<'_>, now: Millis) {
347        self.volumes = Some((frame.music_volume, frame.sound_volume, frame.voice_volume));
348        self.requests.clear();
349        self.requests.extend_from_slice(frame.requests);
350        for request in frame.requests {
351            self.hear(request, now);
352        }
353        self.music.clear();
354        self.music.extend_from_slice(frame.music);
355        for command in frame.music {
356            self.apply(command, now);
357        }
358    }
359
360    fn stop_all(&mut self) {
361        self.started = None;
362        self.voice = None;
363        self.playing.clear();
364        self.stopped = true;
365    }
366
367    fn suspend(&mut self) {
368        self.suspended = true;
369    }
370
371    fn resume(&mut self) {
372        self.suspended = false;
373    }
374}
375
376/// A system keyboard that answers from a script: each time it opens it
377/// gives the next answer, at the next poll.
378#[derive(Debug, Clone, Default)]
379pub struct ScriptedKeyboard {
380    /// The answers still to give, in order.
381    answers: VecDeque<TextAnswer>,
382    /// Every ask it was opened with, in order.
383    asks: Vec<TextAsk>,
384    /// The answer to the ask that is open.
385    pending: Option<TextAnswer>,
386}
387
388impl ScriptedKeyboard {
389    /// A keyboard that answers `answers`, one per opening, then cancels.
390    pub fn new(answers: impl IntoIterator<Item = TextAnswer>) -> Self {
391        Self {
392            answers: answers.into_iter().collect(),
393            ..Self::default()
394        }
395    }
396
397    /// Every ask it was opened with, in order.
398    pub fn asks(&self) -> &[TextAsk] {
399        &self.asks
400    }
401}
402
403impl TextInput for ScriptedKeyboard {
404    fn open(&mut self, ask: &TextAsk) -> bool {
405        if self.pending.is_some() {
406            return false;
407        }
408        self.asks.push(ask.clone());
409        self.pending = Some(self.answers.pop_front().unwrap_or(TextAnswer::Cancelled));
410        true
411    }
412
413    fn poll(&mut self) -> Option<TextAnswer> {
414        self.pending.take()
415    }
416}
417
418/// The system keyboard a session record says was there: its answers and
419/// its opens are the frame's. Nothing asks it what to answer.
420#[derive(Debug, Clone, Default)]
421struct RecordedKeyboard {
422    present: bool,
423    answer: Option<TextAnswer>,
424    opened: Option<bool>,
425}
426
427impl RecordedKeyboard {
428    /// One frame of a session record, if the keyboard had a part in it.
429    fn play(&mut self, frame: Option<&KeyboardFrame>) {
430        self.answer = None;
431        self.opened = None;
432        if let Some(frame) = frame {
433            self.present = frame.present.unwrap_or(self.present);
434            self.answer.clone_from(&frame.answer);
435            self.opened = frame.opened;
436        }
437    }
438}
439
440impl TextInput for RecordedKeyboard {
441    fn open(&mut self, _ask: &TextAsk) -> bool {
442        self.opened.take().unwrap_or(false)
443    }
444
445    fn poll(&mut self) -> Option<TextAnswer> {
446        self.answer.take()
447    }
448}
449
450/// Where a [`HeadlessPlatform`]'s memory readings come from.
451#[derive(Debug, Clone, Copy)]
452enum Memory {
453    /// It reads none.
454    None,
455    /// Fixed amounts by the frames presented
456    /// ([`HeadlessPlatform::with_fake_memory`]).
457    Fake,
458    /// A real counter's reading.
459    Real(fn() -> HeapCounts),
460}
461
462/// The fake memory's reading after `n` frames presented: live bytes
463/// 1,048,576 + 16,384 × (*n* mod 30), the peak the most of those so far,
464/// and *n* mod 7 allocations.
465fn fake_heap(n: u32) -> HeapCounts {
466    const BASE: u64 = 1 << 20;
467    const STEP: u64 = 16 * 1024;
468    let wide = u64::from(n);
469    HeapCounts::new(BASE + STEP * (wide % 30), BASE + STEP * wide.min(29), n % 7)
470}
471
472/// The fake memory's snapshot after `n` frames presented, its rows adding
473/// up to [`fake_heap`]'s live bytes: `image` (an asset kind) 49,152 bytes
474/// in 3 blocks, `load` 524,288 in 40, `update` 16,384 × (*n* mod 30) in
475/// *n* mod 30, and `other` 475,136 in 12. Each row's peak is the most of
476/// its live bytes so far, and its bytes ever allocated its peak; a row
477/// with nothing ever allocated (`update` before the first frame) is left
478/// out, as a counter leaves it out.
479fn fake_snapshot(n: u32) -> HeapSnapshot {
480    const STEP: u64 = 16 * 1024;
481    let heap = fake_heap(n);
482    let row = |class, name: &'static str, live_bytes, live_blocks, peak_bytes| {
483        let mut row = TagRow::new(class, name, live_bytes, live_blocks);
484        row.allocated_bytes = peak_bytes;
485        row.peak_bytes = peak_bytes;
486        row
487    };
488    let fixed = |class, name, bytes, blocks| row(class, name, bytes, blocks, bytes);
489    let mut rows = vec![
490        fixed(TagClass::Asset, "image", 49_152, 3),
491        fixed(TagClass::Phase, "load", 524_288, 40),
492        fixed(TagClass::Other, "other", 475_136, 12),
493    ];
494    if n > 0 {
495        let (wide, peak) = (u64::from(n), STEP * u64::from(n.min(29)));
496        rows.push(row(
497            TagClass::Phase,
498            "update",
499            STEP * (wide % 30),
500            n % 30,
501            peak,
502        ));
503    }
504    HeapSnapshot::new(heap.live_bytes, heap.peak_bytes, rows)
505}
506
507/// A backend with no devices, for tests and tools.
508#[derive(Debug)]
509pub struct HeadlessPlatform {
510    display: DisplayInfo,
511    /// The input of the polls to come, one list per poll.
512    input: VecDeque<Vec<RawInputEvent>>,
513    /// The system's events for the next poll.
514    system: Vec<SystemEvent>,
515    /// The storage, until the game takes it.
516    storage: Option<Box<dyn Storage>>,
517    speaker: Speaker,
518    keyboard: Option<ScriptedKeyboard>,
519    /// The system keyboard of a session record, while one plays: in place
520    /// of `keyboard`.
521    recorded_keyboard: Option<RecordedKeyboard>,
522    /// Where the game is said to run.
523    facts: PlatformFacts,
524    /// The clock it gives to measure frames with, if a test set one.
525    clock: Option<PerfClock>,
526    /// Where its memory readings come from.
527    memory: Memory,
528    /// How many times `App` asked for the memory.
529    memory_calls: u32,
530    /// The memory tags it gives, if a test set them.
531    scopes: Option<MemoryScopes>,
532    /// Where its heap snapshots come from beside the fake memory: a real
533    /// counter's.
534    snapshots: Option<fn() -> HeapSnapshot>,
535    /// How many frames were presented.
536    presented: u32,
537    /// The last of them.
538    last: Option<Presented>,
539    /// The frame cap the last of them asked for, and its serial.
540    frame_cap: Option<(FrameCap, u32)>,
541    /// The reload errors the last of them asked to show.
542    reload_errors: Vec<String>,
543    /// The files that change at the next ask ([`change_file`](Self::change_file)).
544    #[cfg(feature = "std")]
545    changes: Vec<(Layer, String, Vec<u8>)>,
546    /// How many times `App` asked what changed.
547    changed_asks: u32,
548    /// What runs on each frame presented, if a tool set it.
549    present_hook: Option<Hook>,
550}
551
552/// What runs on each frame a [`HeadlessPlatform`] is presented, in its
553/// [`present`](Platform::present): the work a device's backend does there,
554/// such as a one-bit display's conversion, so a frame measured headless
555/// counts it ([`HeadlessPlatform::set_present_hook`]).
556pub type PresentHook = Box<dyn FnMut(&Frame<'_>)>;
557
558/// A [`PresentHook`], which has no `Debug` of its own.
559struct Hook(PresentHook);
560
561impl core::fmt::Debug for Hook {
562    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
563        f.write_str("PresentHook")
564    }
565}
566
567impl Default for HeadlessPlatform {
568    fn default() -> Self {
569        Self::new()
570    }
571}
572
573impl HeadlessPlatform {
574    /// The display headless frames are said to be shown on, unless set.
575    pub const DISPLAY: DisplayInfo = DisplayInfo::new(1280, 720);
576
577    /// A backend with an empty memory storage, no input waiting and no
578    /// system keyboard.
579    pub fn new() -> Self {
580        Self {
581            display: Self::DISPLAY,
582            input: VecDeque::new(),
583            system: Vec::new(),
584            storage: None,
585            speaker: Speaker::default(),
586            keyboard: None,
587            recorded_keyboard: None,
588            facts: PlatformFacts::default(),
589            clock: None,
590            memory: Memory::None,
591            memory_calls: 0,
592            scopes: None,
593            snapshots: None,
594            presented: 0,
595            last: None,
596            frame_cap: None,
597            reload_errors: Vec::new(),
598            #[cfg(feature = "std")]
599            changes: Vec::new(),
600            changed_asks: 0,
601            present_hook: None,
602        }
603    }
604
605    /// A backend ready to play `record` ([`Playback`](crate::session::Playback)):
606    /// the display and the facts it recorded, and a memory storage holding
607    /// its store (writes stay in memory). Its music and its system keyboard
608    /// are the record's from the first [`push_frame`](Self::push_frame).
609    pub fn from_session(record: &SessionRecord) -> Self {
610        let mut storage = MemoryStorage::new();
611        for (key, value) in &record.storage {
612            // A key a store refused was never recorded by one.
613            let _ = storage.write(key, value);
614        }
615        Self::new()
616            .with_display(record.display)
617            .with_facts(record.platform.clone())
618            .with_storage(Box::new(storage))
619    }
620
621    /// Queues one frame of a session record: its system events, its input,
622    /// and what the sound device and the system keyboard answer in it.
623    pub fn push_frame(&mut self, frame: &FrameRecord) {
624        self.system.extend_from_slice(&frame.system);
625        self.push_input(&frame.input);
626        self.speaker.play_recorded(frame);
627        self.recorded_keyboard
628            .get_or_insert_default()
629            .play(frame.keyboard.as_ref());
630    }
631
632    /// The session record has played: the music and the system keyboard are
633    /// this backend's own again.
634    pub fn end_session(&mut self) {
635        self.speaker.recorded = None;
636        self.recorded_keyboard = None;
637    }
638
639    /// The same backend on `display`.
640    #[must_use]
641    pub fn with_display(mut self, display: DisplayInfo) -> Self {
642        self.display = display;
643        self
644    }
645
646    /// The same backend handing the game `storage` (a test's earlier run).
647    #[must_use]
648    pub fn with_storage(mut self, storage: Box<dyn Storage>) -> Self {
649        self.storage = Some(storage);
650        self
651    }
652
653    /// The same backend saying the game runs where `facts` say (the
654    /// `desktop` target's until called): a target's, read from its
655    /// profile with [`PlatformFacts::from_profile`].
656    #[must_use]
657    pub fn with_facts(mut self, facts: PlatformFacts) -> Self {
658        self.facts = facts;
659        self
660    }
661
662    /// The same backend with a system keyboard of its own.
663    #[must_use]
664    pub fn with_keyboard(mut self, keyboard: ScriptedKeyboard) -> Self {
665        self.keyboard = Some(keyboard);
666        self
667    }
668
669    /// The same backend giving `clock` to measure frames with
670    /// ([`Platform::clock`]): a fake one ([`PerfClock::fake`]) makes a
671    /// test's records the same on every run.
672    #[must_use]
673    pub fn with_clock(mut self, clock: PerfClock) -> Self {
674        self.clock = Some(clock);
675        self
676    }
677
678    /// Gives `clock` to measure frames with from now on, or none. `App`
679    /// asks for it on its first frame only.
680    pub fn set_clock(&mut self, clock: Option<PerfClock>) {
681        self.clock = clock;
682    }
683
684    /// The same backend reading a fake heap ([`Platform::memory`]) that
685    /// moves by fixed amounts, so a test's records are the same on every
686    /// run: with *n* the frames it has presented, live bytes 1,048,576 +
687    /// 16,384 × (*n* mod 30), the peak the most of those so far, and *n*
688    /// mod 7 allocations.
689    ///
690    /// It takes fake heap snapshots too ([`Platform::heap_snapshot`]),
691    /// whose rows add up to those live bytes: `Asset`/`image` 49,152 bytes
692    /// in 3 blocks; `Phase`/`load` 524,288 in 40; `Phase`/`update` 16,384 ×
693    /// (*n* mod 30) in *n* mod 30, left out before the first frame; and
694    /// `Other`/`other` 475,136 in 12. Each row's peak is the most of its
695    /// live bytes so far, and its bytes ever allocated equal its peak.
696    #[must_use]
697    pub fn with_fake_memory(mut self) -> Self {
698        self.memory = Memory::Fake;
699        self
700    }
701
702    /// Reads the heap with `memory` from now on (a real counter's
703    /// reading), or reads none; the fake memory is dropped either way.
704    pub fn set_memory(&mut self, memory: Option<fn() -> HeapCounts>) {
705        self.memory = memory.map_or(Memory::None, Memory::Real);
706    }
707
708    /// Takes heap snapshots with `snapshot` from now on (a real counter's),
709    /// or takes none; the fake memory's snapshots come first.
710    pub fn set_heap_snapshots(&mut self, snapshot: Option<fn() -> HeapSnapshot>) {
711        self.snapshots = snapshot;
712    }
713
714    /// Runs `hook` on each frame presented from now on, after the backend
715    /// keeps it, or runs none (`None`).
716    pub fn set_present_hook(&mut self, hook: Option<PresentHook>) {
717        self.present_hook = hook.map(Hook);
718    }
719
720    /// Whether it reads the heap: a fake, or a counter set.
721    pub fn has_memory(&self) -> bool {
722        !matches!(self.memory, Memory::None)
723    }
724
725    /// How many times `App` asked for the memory ([`Platform::memory`]).
726    pub fn memory_calls(&self) -> u32 {
727        self.memory_calls
728    }
729
730    /// The same backend giving `scopes` as its memory tags
731    /// ([`Platform::memory_scopes`]): a test's fake ones, which note what
732    /// is entered.
733    #[must_use]
734    pub fn with_scopes(mut self, scopes: MemoryScopes) -> Self {
735        self.scopes = Some(scopes);
736        self
737    }
738
739    /// Gives `scopes` as its memory tags from now on, or none. `App` asks
740    /// for them on its first frame only.
741    pub fn set_scopes(&mut self, scopes: Option<MemoryScopes>) {
742        self.scopes = scopes;
743    }
744
745    /// Queues `events` as the input of one poll, after those already
746    /// queued.
747    pub fn push_input(&mut self, events: &[RawInputEvent]) {
748        self.input.push_back(events.to_vec());
749    }
750
751    /// Queues `event` for the next poll of the system's events.
752    pub fn push_system(&mut self, event: SystemEvent) {
753        self.system.push(event);
754    }
755
756    /// Queues a change of the file at `path` in `layer` (one handed in at
757    /// run time) for the next time `App` asks what changed
758    /// ([`Platform::changed_files`]), as a file changing on disk does: then
759    /// it has `bytes`, and its path is told.
760    #[cfg(feature = "std")]
761    pub fn change_file(&mut self, layer: Layer, path: &str, bytes: Vec<u8>) {
762        self.changes.push((layer, path.to_owned(), bytes));
763    }
764
765    /// How many times `App` asked what changed
766    /// ([`Platform::changed_files`]).
767    pub fn changed_asks(&self) -> u32 {
768        self.changed_asks
769    }
770
771    /// The hot reload errors the last frame presented asked to show
772    /// ([`Frame::reload_errors`]).
773    pub fn reload_errors(&self) -> &[String] {
774        &self.reload_errors
775    }
776
777    /// How many frames were presented.
778    pub fn presented(&self) -> u32 {
779        self.presented
780    }
781
782    /// The last frame presented.
783    pub fn last_frame(&self) -> Option<&Presented> {
784        self.last.as_ref()
785    }
786
787    /// The frame cap the last frame presented asked for, and its serial:
788    /// the headless backend doesn't pace, and reports nothing.
789    pub fn frame_cap(&self) -> Option<(FrameCap, u32)> {
790        self.frame_cap
791    }
792
793    /// The sound device.
794    pub fn speaker(&self) -> &Speaker {
795        &self.speaker
796    }
797
798    /// The sound device, to set how the music loads.
799    pub fn speaker_mut(&mut self) -> &mut Speaker {
800        &mut self.speaker
801    }
802
803    /// The system keyboard, if it has one.
804    pub fn keyboard(&self) -> Option<&ScriptedKeyboard> {
805        self.keyboard.as_ref()
806    }
807}
808
809impl Platform for HeadlessPlatform {
810    fn display(&self) -> DisplayInfo {
811        self.display
812    }
813
814    fn present(&mut self, frame: &Frame<'_>) {
815        self.presented += 1;
816        self.frame_cap = Some((frame.frame_cap, frame.frame_cap_serial));
817        self.reload_errors.clear();
818        self.reload_errors.extend_from_slice(frame.reload_errors);
819        Presented::keep(&mut self.last, frame);
820        if let Some(Hook(hook)) = &mut self.present_hook {
821            hook(frame);
822        }
823    }
824
825    fn poll_input(&mut self, out: &mut Vec<RawInputEvent>) {
826        if let Some(events) = self.input.pop_front() {
827            out.extend(events);
828        }
829    }
830
831    fn system_events(&mut self, out: &mut Vec<SystemEvent>) {
832        out.append(&mut self.system);
833    }
834
835    fn storage(&mut self) -> Box<dyn Storage> {
836        self.storage
837            .take()
838            .unwrap_or_else(|| Box::new(MemoryStorage::new()))
839    }
840
841    fn audio(&mut self) -> &mut dyn Audio {
842        &mut self.speaker
843    }
844
845    fn text_input(&mut self) -> Option<&mut dyn TextInput> {
846        if let Some(recorded) = &mut self.recorded_keyboard {
847            return recorded.present.then_some(recorded as &mut dyn TextInput);
848        }
849        self.keyboard.as_mut().map(|k| k as &mut dyn TextInput)
850    }
851
852    fn facts(&self) -> PlatformFacts {
853        self.facts.clone()
854    }
855
856    fn clock(&self) -> Option<PerfClock> {
857        self.clock
858    }
859
860    fn memory_scopes(&self) -> Option<MemoryScopes> {
861        self.scopes
862    }
863
864    fn memory(&mut self) -> Option<MemoryReading> {
865        self.memory_calls = self.memory_calls.saturating_add(1);
866        let heap = match self.memory {
867            Memory::None => return None,
868            Memory::Fake => fake_heap(self.presented),
869            Memory::Real(read) => read(),
870        };
871        Some(MemoryReading::new(heap))
872    }
873
874    fn heap_snapshot(&mut self) -> Option<HeapSnapshot> {
875        match self.memory {
876            Memory::Fake => Some(fake_snapshot(self.presented)),
877            Memory::None | Memory::Real(_) => self.snapshots.map(|snapshot| snapshot()),
878        }
879    }
880
881    #[cfg(feature = "std")]
882    fn changed_files(&mut self, out: &mut Vec<String>) {
883        self.changed_asks = self.changed_asks.saturating_add(1);
884        for (layer, path, bytes) in self.changes.drain(..) {
885            if layer.replace(&path, bytes) {
886                out.push(path);
887            }
888        }
889    }
890}
891
892#[cfg(test)]
893mod tests;