353 lines
12 KiB
Rust
353 lines
12 KiB
Rust
use crate::firmware::boot_target::BootTarget;
|
|
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
|
|
use crate::firmware::prometeu_context::PrometeuContext;
|
|
use prometeu_hal::cartridge::Cartridge;
|
|
use prometeu_hal::telemetry::CertificationConfig;
|
|
use prometeu_hal::{HardwareBridge, InputSignals};
|
|
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
|
use prometeu_vm::VirtualMachine;
|
|
|
|
/// PROMETEU Firmware.
|
|
///
|
|
/// The central orchestrator of the console. The firmware acts as the
|
|
/// "Control Unit", managing the high-level state machine of the system.
|
|
///
|
|
/// It is responsible for transitioning between different modes of operation,
|
|
/// such as showing the splash screen, running the Hub (launcher), or
|
|
/// executing a game/app.
|
|
///
|
|
/// ### Execution Loop:
|
|
/// The firmware is designed to be ticked once per frame (60Hz). During each
|
|
/// tick, it:
|
|
/// 1. Updates peripherals with the latest input signals.
|
|
/// 2. Delegates the logic update to the current active state.
|
|
/// 3. Handles state transitions (e.g., from Loading to Playing).
|
|
pub struct Firmware {
|
|
/// The execution engine (PVM) for user applications.
|
|
pub vm: VirtualMachine,
|
|
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
|
|
pub os: VirtualMachineRuntime,
|
|
/// The internal state of the system launcher (Hub).
|
|
pub hub: PrometeuHub,
|
|
/// The current operational state (e.g., Reset, SplashScreen, GameRunning).
|
|
pub state: FirmwareState,
|
|
/// The desired application to run after boot (Hub or specific Cartridge).
|
|
pub boot_target: BootTarget,
|
|
/// State-machine lifecycle tracker.
|
|
state_initialized: bool,
|
|
}
|
|
|
|
impl Firmware {
|
|
/// Initializes the firmware in the `Reset` state.
|
|
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
|
Self {
|
|
vm: VirtualMachine::default(),
|
|
os: VirtualMachineRuntime::new(cap_config),
|
|
hub: PrometeuHub::new(),
|
|
state: FirmwareState::Reset(ResetStep),
|
|
boot_target: BootTarget::Hub,
|
|
state_initialized: false,
|
|
}
|
|
}
|
|
|
|
/// The main entry point for the Host to advance the system logic.
|
|
///
|
|
/// This method is called exactly once per Host frame (60Hz).
|
|
/// It updates peripheral signals and delegates the logic to the current state.
|
|
pub fn tick(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
|
// 0. Process asset commits at the beginning of the frame boundary.
|
|
hw.assets_mut().apply_commits();
|
|
|
|
// 1. Update the peripheral state using the latest signals from the Host.
|
|
// This ensures input is consistent throughout the entire update.
|
|
hw.pad_mut().begin_frame(signals);
|
|
hw.touch_mut().begin_frame(signals);
|
|
|
|
// 2. State machine lifecycle management.
|
|
if !self.state_initialized {
|
|
self.on_enter(signals, hw);
|
|
self.state_initialized = true;
|
|
}
|
|
|
|
// 3. Update the current state and check for transitions.
|
|
if let Some(next_state) = self.on_update(signals, hw) {
|
|
self.change_state(next_state, signals, hw);
|
|
}
|
|
}
|
|
|
|
/// Transitions the system to a new state, handling lifecycle hooks.
|
|
pub fn change_state(
|
|
&mut self,
|
|
new_state: FirmwareState,
|
|
signals: &InputSignals,
|
|
hw: &mut dyn HardwareBridge,
|
|
) {
|
|
self.on_exit(signals, hw);
|
|
self.state = new_state;
|
|
self.state_initialized = false;
|
|
|
|
// Enter the new state immediately to avoid "empty" frames during transitions.
|
|
self.on_enter(signals, hw);
|
|
self.state_initialized = true;
|
|
}
|
|
|
|
/// Dispatches the `on_enter` event to the current state implementation.
|
|
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
|
let mut req = PrometeuContext {
|
|
vm: &mut self.vm,
|
|
os: &mut self.os,
|
|
hub: &mut self.hub,
|
|
boot_target: &self.boot_target,
|
|
signals,
|
|
hw,
|
|
};
|
|
match &mut self.state {
|
|
FirmwareState::Reset(s) => s.on_enter(&mut req),
|
|
FirmwareState::SplashScreen(s) => s.on_enter(&mut req),
|
|
FirmwareState::LaunchHub(s) => s.on_enter(&mut req),
|
|
FirmwareState::HubHome(s) => s.on_enter(&mut req),
|
|
FirmwareState::LoadCartridge(s) => s.on_enter(&mut req),
|
|
FirmwareState::GameRunning(s) => s.on_enter(&mut req),
|
|
FirmwareState::AppCrashes(s) => s.on_enter(&mut req),
|
|
}
|
|
}
|
|
|
|
/// Dispatches the `on_update` event to the current state implementation.
|
|
/// Returns an optional `FirmwareState` if a transition is requested.
|
|
fn on_update(
|
|
&mut self,
|
|
signals: &InputSignals,
|
|
hw: &mut dyn HardwareBridge,
|
|
) -> Option<FirmwareState> {
|
|
let mut req = PrometeuContext {
|
|
vm: &mut self.vm,
|
|
os: &mut self.os,
|
|
hub: &mut self.hub,
|
|
boot_target: &self.boot_target,
|
|
signals,
|
|
hw,
|
|
};
|
|
match &mut self.state {
|
|
FirmwareState::Reset(s) => s.on_update(&mut req),
|
|
FirmwareState::SplashScreen(s) => s.on_update(&mut req),
|
|
FirmwareState::LaunchHub(s) => s.on_update(&mut req),
|
|
FirmwareState::HubHome(s) => s.on_update(&mut req),
|
|
FirmwareState::LoadCartridge(s) => s.on_update(&mut req),
|
|
FirmwareState::GameRunning(s) => s.on_update(&mut req),
|
|
FirmwareState::AppCrashes(s) => s.on_update(&mut req),
|
|
}
|
|
}
|
|
|
|
/// Dispatches the `on_exit` event to the current state implementation.
|
|
fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
|
let mut req = PrometeuContext {
|
|
vm: &mut self.vm,
|
|
os: &mut self.os,
|
|
hub: &mut self.hub,
|
|
boot_target: &self.boot_target,
|
|
signals,
|
|
hw,
|
|
};
|
|
match &mut self.state {
|
|
FirmwareState::Reset(s) => s.on_exit(&mut req),
|
|
FirmwareState::SplashScreen(s) => s.on_exit(&mut req),
|
|
FirmwareState::LaunchHub(s) => s.on_exit(&mut req),
|
|
FirmwareState::HubHome(s) => s.on_exit(&mut req),
|
|
FirmwareState::LoadCartridge(s) => s.on_exit(&mut req),
|
|
FirmwareState::GameRunning(s) => s.on_exit(&mut req),
|
|
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
|
|
}
|
|
}
|
|
|
|
pub fn load_cartridge(&mut self, cartridge: Cartridge) {
|
|
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge));
|
|
self.state_initialized = false;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use prometeu_bytecode::assembler::assemble;
|
|
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
|
use prometeu_drivers::hardware::Hardware;
|
|
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
|
|
use prometeu_hal::syscalls::caps;
|
|
use prometeu_system::CrashReport;
|
|
|
|
fn halting_program() -> Vec<u8> {
|
|
let code = assemble("HALT").expect("assemble");
|
|
BytecodeModule {
|
|
version: 0,
|
|
const_pool: vec![],
|
|
functions: vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
}],
|
|
code,
|
|
debug_info: None,
|
|
exports: vec![],
|
|
syscalls: vec![],
|
|
}
|
|
.serialize()
|
|
}
|
|
|
|
fn invalid_game_cartridge() -> Cartridge {
|
|
Cartridge {
|
|
app_id: 7,
|
|
title: "Broken Cart".into(),
|
|
app_version: "1.0.0".into(),
|
|
app_mode: AppMode::Game,
|
|
capabilities: 0,
|
|
program: vec![0, 0, 0, 0],
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
}
|
|
}
|
|
|
|
fn trapping_game_cartridge() -> Cartridge {
|
|
let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = BytecodeModule {
|
|
version: 0,
|
|
const_pool: vec![],
|
|
functions: vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
}],
|
|
code,
|
|
debug_info: None,
|
|
exports: vec![],
|
|
syscalls: vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
}
|
|
.serialize();
|
|
|
|
Cartridge {
|
|
app_id: 8,
|
|
title: "Trap Cart".into(),
|
|
app_version: "1.0.0".into(),
|
|
app_mode: AppMode::Game,
|
|
capabilities: caps::GFX,
|
|
program,
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
}
|
|
}
|
|
|
|
fn valid_cartridge(app_mode: AppMode) -> Cartridge {
|
|
Cartridge {
|
|
app_id: 9,
|
|
title: "Valid Cart".into(),
|
|
app_version: "1.0.0".into(),
|
|
app_mode,
|
|
capabilities: caps::NONE,
|
|
program: halting_program(),
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
|
|
let mut firmware = Firmware::new(None);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
firmware.load_cartridge(invalid_game_cartridge());
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
match &firmware.state {
|
|
FirmwareState::AppCrashes(step) => match &step.report {
|
|
CrashReport::VmInit { error } => {
|
|
assert!(matches!(error, prometeu_vm::VmInitError::InvalidFormat));
|
|
}
|
|
other => panic!("expected VmInit crash report, got {:?}", other),
|
|
},
|
|
other => panic!("expected AppCrashes state, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn game_running_transitions_to_app_crashes_when_runtime_surfaces_trap() {
|
|
let mut firmware = Firmware::new(None);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
firmware.load_cartridge(trapping_game_cartridge());
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
|
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
match &firmware.state {
|
|
FirmwareState::AppCrashes(step) => match &step.report {
|
|
CrashReport::VmTrap { trap } => {
|
|
assert!(trap.message.contains("Expected integer at index 0"));
|
|
}
|
|
other => panic!("expected VmTrap crash report, got {:?}", other),
|
|
},
|
|
other => panic!("expected AppCrashes state, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reset_routes_hub_boot_target_to_splash_screen() {
|
|
let mut firmware = Firmware::new(None);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
firmware.boot_target = BootTarget::Hub;
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
assert!(matches!(firmware.state, FirmwareState::SplashScreen(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn reset_routes_cartridge_boot_target_to_launch_hub() {
|
|
let mut firmware = Firmware::new(None);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
firmware.boot_target =
|
|
BootTarget::Cartridge { path: "missing-cart".into(), debug: false, debug_port: 7777 };
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
assert!(matches!(firmware.state, FirmwareState::LaunchHub(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn load_cartridge_routes_system_apps_back_to_hub_home() {
|
|
let mut firmware = Firmware::new(None);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
firmware.load_cartridge(valid_cartridge(AppMode::System));
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn load_cartridge_routes_game_apps_to_game_running() {
|
|
let mut firmware = Firmware::new(None);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
firmware.load_cartridge(valid_cartridge(AppMode::Game));
|
|
firmware.tick(&signals, &mut hardware);
|
|
|
|
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
|
}
|
|
}
|