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