Skip to main content

firefly_runtime/
runtime.rs

1use crate::color::FromRGB;
2use crate::config::{FullID, RuntimeConfig};
3use crate::error::Error;
4use crate::frame_buffer::RenderFB;
5use crate::linking::link;
6use crate::state::{NetHandler, State};
7use crate::stats::StatsTracker;
8use crate::utils::read_all;
9use alloc::boxed::Box;
10use embedded_graphics::draw_target::DrawTarget;
11use embedded_graphics::geometry::OriginDimensions;
12use embedded_graphics::pixelcolor::RgbColor;
13use embedded_io::Read;
14use firefly_hal::*;
15use firefly_types::*;
16
17/// Default frames per second.
18const FPS: u8 = 60;
19const KB: u32 = 1024;
20const FUEL_PER_CALL: u64 = 10_000_000;
21
22pub struct Runtime<'a, D, C>
23where
24    D: DrawTarget<Color = C> + RenderFB + OriginDimensions,
25    C: RgbColor + FromRGB,
26{
27    display: D,
28    instance: wasmi::Instance,
29    store: wasmi::Store<Box<State<'a>>>,
30
31    update: Option<wasmi::TypedFunc<(), ()>>,
32    render: Option<wasmi::TypedFunc<(), ()>>,
33    before_exit: Option<wasmi::TypedFunc<(), ()>>,
34    cheat: Option<wasmi::TypedFunc<(i32, i32), (i32,)>>,
35    handle_menu: Option<wasmi::TypedFunc<(u32,), ()>>,
36
37    /// Time to render a single frame to match the expected FPS.
38    per_frame: Duration,
39    /// The last time when the frame was updated.
40    prev_time: Instant,
41    /// The time that the previous frame took over the `per_frame` limit.
42    prev_lag: Duration,
43    n_frames: u8,
44    lagging_frames: u8,
45    fast_frames: u8,
46    render_every: u8,
47
48    stats: Option<StatsTracker>,
49    serial: SerialImpl,
50}
51
52impl<'a, D, C> Runtime<'a, D, C>
53where
54    D: DrawTarget<Color = C> + RenderFB + OriginDimensions,
55    C: RgbColor + FromRGB,
56{
57    /// Create a new runtime with the wasm module loaded and instantiated.
58    pub fn new(mut config: RuntimeConfig<'a, D, C>) -> Result<Self, Error> {
59        let id = match config.id {
60            Some(id) => id,
61            None => match detect_launcher(&mut config.device) {
62                Some(id) => id,
63                None => return Err(Error::NoLauncher),
64            },
65        };
66        id.validate()?;
67
68        let rom_path = &["roms", id.author(), id.app()];
69        let mut rom_dir = match config.device.open_dir(rom_path) {
70            Ok(dir) => dir,
71            Err(err) => return Err(Error::OpenDir(rom_path.join("/"), err)),
72        };
73        let file = match rom_dir.open_file("_meta") {
74            Ok(file) => file,
75            Err(err) => return Err(Error::OpenFile("_meta", err)),
76        };
77        let bytes = match read_all(file) {
78            Ok(bytes) => bytes,
79            Err(err) => return Err(Error::ReadFile("_meta", err.into())),
80        };
81        let meta = match Meta::decode(&bytes[..]) {
82            Ok(meta) => meta,
83            Err(err) => return Err(Error::DecodeMeta(err)),
84        };
85        if meta.author_id != id.author() {
86            return Err(Error::AuthorIDMismatch);
87        }
88        if meta.app_id != id.app() {
89            return Err(Error::AppIDMismatch);
90        }
91        let sudo = meta.sudo;
92        let launcher = meta.launcher;
93
94        let mut serial = config.device.serial();
95        let res = serial.start();
96        if let Err(err) = res {
97            return Err(Error::SerialStart(err));
98        }
99        let now = config.device.now();
100
101        let bin_size = match rom_dir.get_file_size("_bin") {
102            Ok(0) => Err(Error::FileEmpty("_bin")),
103            Ok(bin_size) => Ok(bin_size),
104            Err(err) => Err(Error::OpenFile("_bin", err)),
105        }?;
106
107        let engine = {
108            let mut wasmi_config = wasmi::Config::default();
109            wasmi_config.ignore_custom_sections(true);
110            wasmi_config.consume_fuel(true);
111            if bin_size > 40 * KB {
112                wasmi_config.compilation_mode(wasmi::CompilationMode::Lazy);
113            }
114            wasmi::Engine::new(&wasmi_config)
115        };
116
117        let mut state = State::new(
118            id.clone(),
119            config.device,
120            rom_dir,
121            config.net_handler,
122            launcher,
123        );
124        state.load_app_stats()?;
125        state.load_stash()?;
126
127        // Load the binary wasm file into PSRAM.
128        let wasm_bin = {
129            let mut stream = match state.rom_dir.open_file("_bin") {
130                Ok(stream) => Ok(stream),
131                Err(err) => Err(Error::OpenFile("_bin", err)),
132            }?;
133            let bin_size = bin_size as usize;
134            let mut wasm_bin = state.device.alloc_psram(bin_size);
135            wasm_bin.resize(bin_size, 0);
136            match stream.read_exact(&mut wasm_bin) {
137                Ok(_) => {}
138                Err(embedded_io::ReadExactError::UnexpectedEof) => {
139                    let err = FSError::AllocationError;
140                    return Err(Error::OpenFile("_bin", err));
141                }
142                Err(embedded_io::ReadExactError::Other(err)) => {
143                    let err = FSError::from(err);
144                    return Err(Error::OpenFile("_bin", err));
145                }
146            }
147            wasm_bin
148        };
149
150        let mut store = wasmi::Store::new(&engine, state);
151        _ = store.set_fuel(FUEL_PER_CALL);
152        let instance = {
153            let module = wasmi::Module::new(&engine, wasm_bin)?;
154            let mut linker = wasmi::Linker::new(&engine);
155            link(&mut linker, sudo)?;
156            linker.instantiate_and_start(&mut store, &module)?
157        };
158
159        let runtime = Self {
160            display: config.display,
161            instance,
162            store,
163            update: None,
164            render: None,
165            before_exit: None,
166            cheat: None,
167            handle_menu: None,
168            stats: None,
169            per_frame: Duration::from_fps(u32::from(FPS)),
170            n_frames: 0,
171            lagging_frames: 0,
172            fast_frames: 0,
173            render_every: 2,
174            prev_time: now,
175            prev_lag: Duration::from_ms(0),
176            serial,
177        };
178        Ok(runtime)
179    }
180
181    /// Set how often `render` should be called relative to `update`.
182    ///
183    /// Exposed excludively for firefly-test to combat FPS auto-adjustment.
184    pub fn set_render_every(&mut self, render_every: u8) {
185        self.render_every = render_every;
186    }
187
188    pub fn display_mut(&mut self) -> &mut D {
189        &mut self.display
190    }
191
192    /// Run the app until exited or an error occurs.
193    pub fn run(mut self) -> Result<(), Error> {
194        self.start()?;
195        loop {
196            self.update()?;
197        }
198    }
199
200    /// Call init functions in the module.
201    pub fn start(&mut self) -> Result<(), Error> {
202        self.set_memory();
203
204        let ins = self.instance;
205        // The `_initialize` and `_start` functions are defined by wasip1.
206        let f = ins.get_typed_func::<(), ()>(&self.store, "_initialize");
207        self.call_callback("_initialize", f.ok())?;
208        let f = ins.get_typed_func::<(), ()>(&self.store, "_start");
209        self.call_callback("_start", f.ok())?;
210        // The `boot` function is defined by our spec.
211        let f = ins.get_typed_func::<(), ()>(&self.store, "boot");
212        self.call_callback("boot", f.ok())?;
213
214        // Other functions defined by our spec.
215        self.update = ins.get_typed_func(&self.store, "update").ok();
216        self.render = ins.get_typed_func(&self.store, "render").ok();
217        self.before_exit = ins.get_typed_func(&self.store, "before_exit").ok();
218        self.cheat = ins.get_typed_func(&self.store, "cheat").ok();
219        self.handle_menu = ins.get_typed_func(&self.store, "handle_menu").ok();
220        Ok(())
221    }
222
223    /// Update the app state and flush the frame on the display.
224    ///
225    /// If there is not enough time passed since the last update,
226    /// the update will be delayed to keep the expected frame rate.
227    pub fn update(&mut self) -> Result<bool, Error> {
228        self.handle_serial()?;
229        let state = self.store.data_mut();
230        let menu_was_active = state.menu.active();
231        let menu_index = state.update();
232
233        if let Some(scene) = &mut state.error {
234            let res = scene.render(&mut self.display);
235            if res.is_err() {
236                return Err(Error::CannotDisplay);
237            }
238            return Ok(false);
239        }
240
241        // TODO: pause audio when opening menu
242        let menu_is_active = state.menu.active();
243        if menu_is_active {
244            if self.n_frames.is_multiple_of(60) {
245                if let Some(battery) = &mut state.battery {
246                    let res = battery.update(&mut state.device);
247                    if let Err(err) = res {
248                        state.device.log_error("battery", err);
249                    }
250                }
251            }
252            // We render the system menu directly on the screen,
253            // bypassing the frame buffer. That way, we preserve
254            // the frame buffer rendered by the app.
255            // Performance isn't an issue for a simple text menu.
256            let res = state.menu.render(&mut self.display, &state.battery);
257            if res.is_err() {
258                return Err(Error::CannotDisplay);
259            }
260            self.delay();
261            return Ok(false);
262        } else if menu_was_active {
263            state.frame.dirty = true;
264            if self.render.is_none() {
265                // When menu was open but now closed, if the app doesn't have the `render`
266                // callback defined, the screen flushing will never be called.
267                // As a result, the menu image will stuck on the display.
268                // To avoid that, we fill the screen with a color.
269                //
270                // The color is the same as the menu background color
271                // to avoid flashing that may cause an epilepsy episode.
272                _ = self.display.clear(C::BG);
273            }
274        }
275
276        // If a custom menu item is selected, trigger the handle_menu callback.
277        if let Some(custom_menu) = menu_index {
278            if let Some(handle_menu) = self.handle_menu {
279                if let Err(err) = handle_menu.call(&mut self.store, (custom_menu as u32,)) {
280                    let stats = self.store.data().runtime_stats();
281                    return Err(Error::FuncCall("handle_menu", err, stats));
282                };
283            }
284        }
285
286        // TODO: continue execution even if an update fails.
287        let fuel_update = self.call_callback("update", self.update)?;
288        if let Some(stats) = &mut self.stats {
289            stats.update_fuel.add(fuel_update);
290        }
291        {
292            let state = self.store.data_mut();
293            let audio_buf = state.device.get_audio_buffer();
294            if !audio_buf.is_empty() {
295                state.audio.write(audio_buf);
296            }
297        }
298
299        // Check if the app is lagging.
300        // Adjust, if needed, how often "render" is called.
301        // If we have time to spare, delay rendering to keep steady frame rate.
302        if self.fast_frames >= FPS {
303            self.render_every = (self.render_every - 1).max(1);
304            self.fast_frames = 0;
305        } else if self.lagging_frames >= FPS {
306            self.render_every = (self.render_every + 1).min(8);
307            self.lagging_frames = 0;
308        }
309        self.delay();
310
311        let state = self.store.data();
312        let should_render = state.exit || self.n_frames.is_multiple_of(self.render_every);
313        // The frame number must be updated after calculating "should_render"
314        // so that "render" is always called on the first "update" run
315        // (when the app is just launched).
316        self.n_frames = (self.n_frames + 1) % (FPS * 4);
317        if should_render {
318            let fuel_render = self.call_callback("render", self.render)?;
319            if let Some(stats) = &mut self.stats {
320                stats.render_fuel.add(fuel_render);
321            }
322            let state = self.store.data();
323            if state.frame.dirty {
324                self.flush_frame()?;
325            }
326        }
327        let state = self.store.data();
328        Ok(state.exit)
329    }
330
331    // Delay the screen flushing to adjust the frame rate.
332    fn delay(&mut self) {
333        let state = self.store.data();
334        let now = state.device.now();
335        let elapsed = now - self.prev_time;
336        if elapsed < self.per_frame {
337            let delay = self.per_frame - elapsed;
338            if delay > self.prev_lag {
339                let delay = delay - self.prev_lag;
340                if let Some(stats) = &mut self.stats {
341                    stats.delays += delay;
342                    // we shaved off the previous lag, yay!
343                    stats.lags -= self.prev_lag;
344                }
345                state.device.delay(delay);
346            }
347            self.fast_frames = (self.fast_frames + 1) % (FPS * 4);
348            self.prev_lag = Duration::from_ms(0);
349            self.lagging_frames = 0;
350        } else {
351            if let Some(stats) = &mut self.stats {
352                stats.lags += elapsed - self.per_frame;
353            }
354            self.prev_lag = elapsed - self.per_frame;
355            self.lagging_frames = (self.lagging_frames + 1) % (FPS * 4);
356            self.fast_frames = 0;
357        }
358        self.prev_time = state.device.now();
359    }
360
361    /// Gracefully stop the runtime.
362    ///
363    /// 1. Calls `before_exit` callback.
364    /// 2. Persists stash and update stats.
365    /// 3. Releases [`Device`] ownership.
366    /// 3. Tells which app to run next.
367    pub fn finalize(mut self) -> Result<RuntimeConfig<'a, D, C>, Error> {
368        self.call_callback("before_exit", self.before_exit)?;
369        let mut state = self.store.into_data();
370        state.save_stash();
371        state.update_app_stats();
372        state.save_app_stats();
373        let net_handler = state.net_handler.replace(NetHandler::None);
374        let config = RuntimeConfig {
375            id: state.next,
376            device: state.device,
377            display: self.display,
378            net_handler,
379        };
380        Ok(config)
381    }
382
383    pub fn device_mut(&mut self) -> &mut DeviceImpl<'a> {
384        let state = self.store.data_mut();
385        &mut state.device
386    }
387
388    /// Draw the frame buffer on the actual screen.
389    fn flush_frame(&mut self) -> Result<(), Error> {
390        let state = self.store.data_mut();
391        let res = self.display.render_fb(&mut state.frame);
392        if res.is_err() {
393            return Err(Error::CannotDisplay);
394        }
395        Ok(())
396    }
397
398    /// Find exported memory in the instance and add it into the state.
399    fn set_memory(&mut self) {
400        let memory = self.instance.get_memory(&self.store, "memory");
401        let state = self.store.data_mut();
402        state.memory = memory;
403    }
404
405    /// Handle requests and responses on the USB serial port.
406    fn handle_serial(&mut self) -> Result<(), Error> {
407        let maybe_msg = match self.serial.recv() {
408            Ok(msg) => msg,
409            Err(err) => return Err(Error::SerialRecv(err)),
410        };
411        if let Some(raw_msg) = maybe_msg {
412            match serial::Request::decode(&raw_msg) {
413                Ok(req) => self.handle_serial_request(req)?,
414                Err(err) => return Err(Error::SerialDecode(err)),
415            }
416        }
417        self.send_stats()?;
418        Ok(())
419    }
420
421    /// Send runtime stats to the serial port.
422    fn send_stats(&mut self) -> Result<(), Error> {
423        let Some(stats) = &mut self.stats else {
424            return Ok(());
425        };
426        let state = self.store.data();
427        let now = state.device.now();
428        if let Some(memory) = state.memory {
429            let data = memory.data(&self.store);
430            stats.analyze_memory(data);
431        }
432        let Some(resp) = stats.as_message(now) else {
433            return Ok(());
434        };
435        let encoded = match resp.encode_vec() {
436            Ok(encoded) => encoded,
437            Err(err) => return Err(Error::SerialEncode(err)),
438        };
439        let res = self.serial.send(&encoded);
440        if let Err(err) = res {
441            return Err(Error::SerialSend(err));
442        }
443        Ok(())
444    }
445
446    fn handle_serial_request(&mut self, req: serial::Request) -> Result<(), Error> {
447        match req {
448            serial::Request::Cheat(a, b) => {
449                let Some(cheat) = self.cheat else {
450                    return Err(Error::CheatUndefined);
451                };
452                let state = self.store.data_mut();
453                if !matches!(state.net_handler.get_mut(), NetHandler::None) {
454                    return Err(Error::CheatInNet);
455                }
456                match cheat.call(&mut self.store, (a, b)) {
457                    Ok((result,)) => {
458                        let resp = serial::Response::Cheat(result);
459                        self.serial_send(resp)?;
460                    }
461                    Err(err) => {
462                        let stats = self.store.data().runtime_stats();
463                        return Err(Error::FuncCall("cheat", err, stats));
464                    }
465                }
466            }
467            serial::Request::Stats(stats) => {
468                let state = self.store.data_mut();
469                let now = state.device.now();
470                if stats && self.stats.is_none() {
471                    self.stats = Some(StatsTracker::new(now));
472                };
473                if !stats && self.stats.is_some() {
474                    self.stats = None;
475                };
476            }
477            serial::Request::AppId => {
478                let state = self.store.data();
479                let author = state.id.author().into();
480                let app = state.id.app().into();
481                let resp = serial::Response::AppID((author, app));
482                self.serial_send(resp)?;
483            }
484            serial::Request::Screenshot => {
485                let state = self.store.data_mut();
486                state.take_screenshot();
487                let resp = serial::Response::Ok;
488                self.serial_send(resp)?;
489            }
490            serial::Request::Launch((author, app)) => {
491                let state = self.store.data_mut();
492                let resp = if let Some(id) = FullID::from_str(&author, &app) {
493                    state.next = Some(id);
494                    state.exit = true;
495                    serial::Response::Ok
496                } else {
497                    serial::Response::Log("ERROR(runtime): app ID is too long".into())
498                };
499                self.serial_send(resp)?;
500            }
501            serial::Request::Exit => {
502                let state = self.store.data_mut();
503                state.exit = true;
504                let resp = serial::Response::Ok;
505                self.serial_send(resp)?;
506            }
507            serial::Request::Buttons(_) => todo!(),
508            serial::Request::Data(_) => todo!(),
509        }
510        Ok(())
511    }
512
513    fn serial_send(&mut self, resp: serial::Response) -> Result<(), Error> {
514        let encoded = match resp.encode_vec() {
515            Ok(encoded) => encoded,
516            Err(err) => return Err(Error::SerialEncode(err)),
517        };
518        let res = self.serial.send(&encoded);
519        if let Err(err) = res {
520            return Err(Error::SerialSend(err));
521        }
522        Ok(())
523    }
524
525    /// Call a guest function. Returns the amount of fuel consumed.
526    fn call_callback(
527        &mut self,
528        name: &'static str,
529        f: Option<wasmi::TypedFunc<(), ()>>,
530    ) -> Result<u32, Error> {
531        _ = self.store.set_fuel(FUEL_PER_CALL);
532        if let Some(f) = f {
533            if let Err(err) = f.call(&mut self.store, ()) {
534                let stats = self.store.data().runtime_stats();
535                return Err(Error::FuncCall(name, err, stats));
536            }
537        }
538        let Ok(left) = self.store.get_fuel() else {
539            return Ok(0);
540        };
541        let consumed = FUEL_PER_CALL - left;
542        let consumed = u32::try_from(consumed).unwrap_or_default();
543        Ok(consumed)
544    }
545}
546
547fn detect_launcher(device: &mut DeviceImpl) -> Option<FullID> {
548    let mut dir = device.open_dir(&["sys"]).ok()?;
549    if let Some(id) = get_short_meta(&mut dir, "launcher") {
550        return Some(id);
551    }
552    get_short_meta(&mut dir, "new-app")
553}
554
555fn get_short_meta(dir: &mut DirImpl, fname: &str) -> Option<FullID> {
556    let file = dir.open_file(fname).ok()?;
557    let bytes = read_all(file).ok()?;
558    let meta = ShortMeta::decode(&bytes[..]).ok()?;
559    let author = meta.author_id.try_into().ok()?;
560    let app = meta.app_id.try_into().ok()?;
561    let id = FullID::new(author, app);
562    Some(id)
563}