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
18const 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 per_frame: Duration,
40 prev_time: Instant,
42 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 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 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 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 pub fn run(mut self) -> Result<(), Error> {
192 self.start()?;
193 loop {
194 self.update()?;
195 }
196 }
197
198 pub fn start(&mut self) -> Result<(), Error> {
200 self.set_memory();
201
202 let ins = self.instance;
203 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 let f = ins.get_typed_func::<(), ()>(&self.store, "boot");
210 self.call_callback("boot", f.ok())?;
211
212 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 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 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 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 _ = self.display.clear(C::BG);
271 }
272 }
273
274 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 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 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 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 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 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 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 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 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 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 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 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}