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