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
17const 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 per_frame: Duration,
39 prev_time: Instant,
41 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 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 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 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 pub fn run(mut self) -> Result<(), Error> {
194 self.start()?;
195 loop {
196 self.update()?;
197 }
198 }
199
200 pub fn start(&mut self) -> Result<(), Error> {
202 self.set_memory();
203
204 let ins = self.instance;
205 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 let f = ins.get_typed_func::<(), ()>(&self.store, "boot");
212 self.call_callback("boot", f.ok())?;
213
214 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 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 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 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 _ = self.display.clear(C::BG);
273 }
274 }
275
276 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 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 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 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 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 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 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 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 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 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 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 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}