use crate::firmware::boot_target::BootTarget; use crate::firmware::firmware_state::{ FirmwareState, GameRunningStep, LoadCartridgeStep, ResetStep, }; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::LogSource; use prometeu_hal::telemetry::CertificationConfig; use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_system::{PrometeuHub, ResidentGameState, SystemOS}; 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: SystemOS, /// 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, game_input_barrier_frames: u8, } impl Firmware { /// Initializes the firmware in the `Reset` state. pub fn new(cap_config: Option) -> Self { Self { vm: VirtualMachine::default(), os: SystemOS::new(cap_config), hub: PrometeuHub::new(), state: FirmwareState::Reset(ResetStep), boot_target: BootTarget::Hub, state_initialized: false, game_input_barrier_frames: 0, } } /// 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, platform: &mut dyn RuntimePlatform) { let barriered_signals = InputSignals::default(); let effective_signals = if self.game_input_barrier_frames > 0 { self.game_input_barrier_frames -= 1; &barriered_signals } else { signals }; // 0. Process asset commits at the beginning of the frame boundary. platform.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. platform.input_mut().pad_mut().begin_frame(effective_signals); platform.input_mut().touch_mut().begin_frame(effective_signals); // 2. State machine lifecycle management. if !self.state_initialized { self.on_enter(effective_signals, platform); self.state_initialized = true; } // 3. Update the current state and check for transitions. if let Some(next_state) = self.on_update(effective_signals, platform) { self.change_state(next_state, effective_signals, platform); } } /// Transitions the system to a new state, handling lifecycle hooks. pub fn change_state( &mut self, new_state: FirmwareState, signals: &InputSignals, platform: &mut dyn RuntimePlatform, ) { self.on_exit(signals, platform); self.state = new_state; self.state_initialized = false; // Enter the new state immediately to avoid "empty" frames during transitions. self.on_enter(signals, platform); self.state_initialized = true; } /// Dispatches the `on_enter` event to the current state implementation. fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { let mut req = PrometeuContext { vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, signals, platform, }; 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::ShellRunning(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, platform: &mut dyn RuntimePlatform, ) -> Option { let mut req = PrometeuContext { vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, signals, platform, }; 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::ShellRunning(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, platform: &mut dyn RuntimePlatform) { let mut req = PrometeuContext { vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, signals, platform, }; 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::ShellRunning(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; } pub fn request_home_from_host(&mut self) { self.os.info(LogSource::Pos, "Host requested Home/SystemOS".to_string()); if let FirmwareState::GameRunning(step) = &self.state { if let Err(error) = self.os.lifecycle().request_home_from_game(step.task_id) { self.os.warn( LogSource::Pos, format!("Home/SystemOS request could not pause Game: {error:?}"), ); } else { self.game_input_barrier_frames = 1; } } } pub fn resume_resident_game_from_home(&mut self) -> bool { let Some(task_id) = self.os.lifecycle().resident_game_task() else { return false; }; if self.os.lifecycle().resume_task(task_id).is_err() { return false; } let app_id = self.os.vm().current_app_id(); self.os.vm().transition_render_owner(AppMode::Game, app_id); self.state = FirmwareState::GameRunning(GameRunningStep::new(task_id)); self.state_initialized = false; self.game_input_barrier_frames = 1; true } pub fn game_lifecycle_audio_paused(&mut self) -> bool { matches!( self.os.lifecycle().resident_game().map(|game| game.state), Some( ResidentGameState::PauseRequested { .. } | ResidentGameState::PausedSuspended | ResidentGameState::ResumeRequested, ) ) } } #[cfg(test)] mod tests { use super::*; use crate::firmware::firmware_state::HubHomeStep; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_drivers::TestPlatform; use prometeu_hal::RuntimePlatform; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::syscalls::{Syscall, caps}; use prometeu_system::process::ProcessState; use prometeu_system::task::{TaskId, TaskState}; use prometeu_system::windows::WindowOwner; use prometeu_system::{ CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState, discover_games_root, }; use std::fs; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0); struct TestGameRoot { path: PathBuf, } impl TestGameRoot { fn new() -> Self { let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!( "prometeu-firmware-game-root-test-{}-{}", std::process::id(), unique )); fs::create_dir_all(&path).expect("test game root should be created"); Self { path } } fn add_game(&self, name: &str, with_program: bool) -> PathBuf { let cart = self.path.join(name); fs::create_dir_all(&cart).expect("test cartridge should be created"); let manifest = r#"{ "magic": "PMTU", "cartridge_version": 1, "app_id": 42, "title": "Stress", "app_version": "1.0.0", "app_mode": "Game" }"#; fs::write(cart.join("manifest.json"), manifest) .expect("test manifest should be written"); if with_program { fs::write(cart.join("program.pbx"), halting_program()) .expect("test program should be written"); } cart } } impl Drop for TestGameRoot { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.path); } } fn halting_program() -> Vec { 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: "gfx2d".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![], } } fn load_shell_running_firmware() -> (Firmware, TestPlatform, InputSignals, TaskId) { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Shell)); firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; (firmware, platform, signals, task_id) } fn hub_home_firmware() -> (Firmware, TestPlatform) { let mut firmware = Firmware::new(None); firmware.state = FirmwareState::HubHome(HubHomeStep); firmware.state_initialized = false; (firmware, TestPlatform::new()) } fn focus_window(firmware: &mut Firmware, owner: WindowOwner) { let mut windows = firmware.os.windows(); windows.remove_all_windows(); let id = windows.add_window( "Test Window".to_string(), owner, Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE, ); windows.set_focus(id); } #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(invalid_game_cartridge()); firmware.tick(&signals, &mut platform); 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 platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(trapping_game_cartridge()); firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected GameRunning state, got {:?}", other), }; firmware.tick(&signals, &mut platform); 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), } assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Crashed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Crashed) ); } #[test] fn reset_routes_hub_boot_target_to_splash_screen() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.boot_target = BootTarget::Hub; firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::SplashScreen(_))); } #[test] fn reset_routes_cartridge_boot_target_to_launch_hub() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.boot_target = BootTarget::Cartridge { path: "missing-cart".into(), debug: false, debug_port: 7777 }; firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::LaunchHub(_))); } #[test] fn load_cartridge_routes_system_apps_to_system_running() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Shell)); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); assert!(firmware.os.windows().focused_window().is_some()); assert_eq!(firmware.os.windows().window_count(), 1); } #[test] fn system_running_ticks_focused_system_app_through_hub_pipeline() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Shell)); firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; let tick_index_before_system_update = firmware.os.vm().tick_index(); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); assert!(firmware.os.windows().focused_window_belongs_to_task(task_id)); assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1); } #[test] fn hub_home_does_not_launch_shell_before_home_input_is_armed() { let (mut firmware, mut platform) = hub_home_firmware(); let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() }; firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.windows().window_count(), 0); } #[test] fn hub_home_click_launches_shell_a_as_native_shell_task_window() { let (mut firmware, mut platform) = hub_home_firmware(); firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() }; firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; assert!(firmware.os.windows().focused_window_belongs_to_task(task_id)); assert_eq!(firmware.os.windows().window_count(), 1); assert_eq!(firmware.os.windows().windows()[0].title, "ShellA"); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Running) ); } #[test] fn hub_home_click_launches_shell_b_as_native_shell_task_window() { let (mut firmware, mut platform) = hub_home_firmware(); firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 256, y_pos: 172, ..Default::default() }; firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; assert!(firmware.os.windows().focused_window_belongs_to_task(task_id)); assert_eq!(firmware.os.windows().window_count(), 1); assert_eq!(firmware.os.windows().windows()[0].title, "ShellB"); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Running) ); } #[test] fn hub_home_click_launches_game_library_entry_through_load_cartridge() { let root = TestGameRoot::new(); root.add_game("stress", true); let (mut firmware, mut platform) = hub_home_firmware(); firmware.hub.set_game_library(discover_games_root(&root.path)); firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&signals, &mut platform); match &firmware.state { FirmwareState::LoadCartridge(step) => { assert_eq!(step.cartridge.title, "Stress"); assert_eq!(step.cartridge.app_id, 42); } other => panic!("expected LoadCartridge state, got {:?}", other), } } #[test] fn games_root_home_click_reaches_game_running() { let root = TestGameRoot::new(); root.add_game("stress", true); let (mut firmware, mut platform) = hub_home_firmware(); firmware.hub.set_game_library(discover_games_root(&root.path)); firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&signals, &mut platform); firmware.tick(&InputSignals::default(), &mut platform); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress"); assert_eq!(firmware.os.windows().window_count(), 0); } #[test] fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() { let root = TestGameRoot::new(); root.add_game("broken", false); let (mut firmware, mut platform) = hub_home_firmware(); let library = discover_games_root(&root.path); assert!(library.is_empty()); assert!(!library.diagnostics().is_empty()); firmware.hub.set_game_library(library); firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.vm().current_cartridge_title(), ""); } #[test] fn direct_boot_target_still_reaches_game_running_without_games_root() { let root = TestGameRoot::new(); let cart = root.add_game("stress", true); let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); firmware.boot_target = BootTarget::Cartridge { path: cart.display().to_string(), debug: false, debug_port: 7777, }; firmware.tick(&InputSignals::default(), &mut platform); firmware.tick(&InputSignals::default(), &mut platform); firmware.tick(&InputSignals::default(), &mut platform); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress"); } #[test] fn hub_home_failed_game_launch_remains_home_and_logs_error() { let root = TestGameRoot::new(); let cart = root.add_game("stress", true); let (mut firmware, mut platform) = hub_home_firmware(); firmware.hub.set_game_library(discover_games_root(&root.path)); fs::remove_file(cart.join("program.pbx")).expect("program should be removable"); firmware.tick(&InputSignals::default(), &mut platform); let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert!( firmware .os .recent_logs(16) .iter() .any(|entry| entry.msg.contains("Failed to launch Home game")) ); } #[test] fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() { let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware(); let close_signals = InputSignals { start_signal: true, ..Default::default() }; firmware.tick(&close_signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Stopped) ); } #[test] fn shell_running_close_click_uses_lifecycle_and_returns_hub_home() { let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware(); let close_signals = InputSignals { f_signal: true, x_pos: 420, y_pos: 24, ..Default::default() }; firmware.tick(&close_signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Stopped) ); } #[test] fn shell_running_closes_task_and_returns_hub_home_without_focused_window() { let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); firmware.os.windows().remove_all_windows(); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Stopped) ); } #[test] fn shell_running_closes_task_and_returns_hub_home_for_focused_hub_window() { let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); focus_window(&mut firmware, WindowOwner::Hub); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Stopped) ); } #[test] fn shell_running_closes_task_and_returns_hub_home_for_focused_overlay_window() { let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); focus_window(&mut firmware, WindowOwner::Overlay); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Stopped) ); } #[test] fn shell_running_closes_task_and_returns_hub_home_for_other_task_window() { let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware(); focus_window(&mut firmware, WindowOwner::Task(TaskId(task_id.0 + 1))); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Stopped) ); } #[test] fn load_cartridge_routes_game_apps_to_game_running() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Game)); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert_eq!(firmware.os.windows().window_count(), 0); } #[test] fn host_home_request_pauses_then_suspends_game_before_next_vm_tick() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Game)); firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected GameRunning state, got {:?}", other), }; let tick_index_before_home = firmware.os.vm().tick_index(); let render_ownership_before_home = firmware.os.vm().active_render_ownership(); firmware.request_home_from_host(); assert!(firmware.game_lifecycle_audio_paused()); assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); assert_eq!( firmware.os.lifecycle().resident_game().map(|game| game.state), Some(ResidentGameState::PauseRequested { remaining_ticks: 1 }) ); assert_eq!( firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::Pause) ); let held_signals = InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals }; firmware.tick(&held_signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Suspended)); assert_eq!( firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Suspended) ); assert_eq!(firmware.os.vm().tick_index(), tick_index_before_home); assert!(!platform.input().pad().any()); assert!(!platform.input().touch().f().down); let render_ownership_after_home = firmware.os.vm().active_render_ownership(); assert_eq!(render_ownership_after_home.app_mode, AppMode::Shell); assert_eq!(render_ownership_after_home.app_id, 0); assert!(render_ownership_after_home.epoch > render_ownership_before_home.epoch); assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty()); assert_eq!( firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::Pause) ); } #[test] fn resident_game_resume_restores_render_owner_after_resume_event_and_barriers_input() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Game)); firmware.tick(&signals, &mut platform); let task_id = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected GameRunning state, got {:?}", other), }; firmware.request_home_from_host(); firmware.tick(&signals, &mut platform); let hub_ownership = firmware.os.vm().active_render_ownership(); assert!(firmware.resume_resident_game_from_home()); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert!(!firmware.game_lifecycle_audio_paused()); assert_eq!( firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::ResumeForeground) ); let game_ownership = firmware.os.vm().active_render_ownership(); assert_eq!(game_ownership.app_mode, AppMode::Game); assert_eq!(game_ownership.app_id, 9); assert!(game_ownership.epoch > hub_ownership.epoch); let held_signals = InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals }; firmware.tick(&held_signals, &mut platform); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground)); assert!(!platform.input().pad().any()); assert!(!platform.input().touch().f().down); assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty()); assert_eq!( firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::ResumeForeground) ); } #[test] fn game_home_shell_hub_same_game_flow_is_end_to_end_deterministic() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Game)); firmware.tick(&signals, &mut platform); let game_task = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected GameRunning state, got {:?}", other), }; firmware.request_home_from_host(); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); firmware.tick(&signals, &mut platform); let shell_a_click = InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() }; firmware.tick(&shell_a_click, &mut platform); let shell_task = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell_task)); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); let close_shell = InputSignals { start_signal: true, ..Default::default() }; firmware.tick(&close_shell, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub); assert_eq!(firmware.os.lifecycle().task_state(shell_task), Some(TaskState::Closed)); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); assert!(firmware.resume_resident_game_from_home()); match &firmware.state { FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task), other => panic!("expected GameRunning state, got {:?}", other), } assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(game_task)); assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Foreground)); assert_eq!( firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::ResumeForeground) ); } #[test] fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() { assert!(Syscall::from_u32(0x0002).is_none()); let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge(AppMode::Game)); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert_eq!(firmware.os.vm().current_cartridge_title(), "Valid Cart"); } }