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}; /// 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 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 { 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, ) { let new_state = match new_state { FirmwareState::ResumeResidentGame => { let Some(resume_state) = self.resident_game_resume_state() else { return; }; resume_state } state => state, }; let entering_game_from_home = matches!(self.state, FirmwareState::HubHome(_)) && matches!(new_state, FirmwareState::GameRunning(_)); self.on_exit(signals, platform); self.state = new_state; self.state_initialized = false; if entering_game_from_home { self.game_input_barrier_frames = self.game_input_barrier_frames.max(1); } // 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 { 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::ResumeResidentGame => {} 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 { 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::ResumeResidentGame => None, 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 { 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::ResumeResidentGame => {} 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(state) = self.resident_game_resume_state() else { return false; }; self.state = state; self.state_initialized = false; true } fn resident_game_resume_state(&mut self) -> Option { let task_id = self.os.lifecycle().resident_game_task()?; if let Err(error) = self.os.lifecycle().resume_task(task_id) { self.os.warn(LogSource::Pos, format!("Resident Game resume failed: {error:?}")); return None; } let app_id = self .os .sessions() .vm_session_for_task(task_id) .map(|session| session.app_id) .unwrap_or_else(|| self.os.vm().current_app_id()); self.os.vm().transition_render_owner(AppMode::Game, app_id); self.game_input_barrier_frames = self.game_input_barrier_frames.max(1); Some(FirmwareState::GameRunning(GameRunningStep::new(task_id))) } 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 crate::firmware::resident_game_termination::{self, ResidentGameTerminationOutcome}; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_drivers::{GlyphBankPoolAccess, SceneBankPoolAccess, TestPlatform}; use prometeu_hal::RuntimePlatform; use prometeu_hal::app_mode::AppMode; use prometeu_hal::asset::{BankType, SlotRef}; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; 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 { self.add_game_with_identity(name, 42, "Stress", "1.0.0", with_program) } fn add_game_with_identity( &self, name: &str, app_id: u32, title: &str, app_version: &str, with_program: bool, ) -> PathBuf { let program = with_program.then(halting_program); self.add_game_with_program(name, app_id, title, app_version, program) } fn add_game_with_program( &self, name: &str, app_id: u32, title: &str, app_version: &str, program: Option>, ) -> PathBuf { let cart = self.path.join(name); fs::create_dir_all(&cart).expect("test cartridge should be created"); let manifest = format!( r#"{{ "magic": "PMTU", "cartridge_version": 1, "app_id": {app_id}, "title": "{title}", "app_version": "{app_version}", "app_mode": "Game" }}"# ); fs::write(cart.join("manifest.json"), manifest) .expect("test manifest should be written"); if let Some(program) = program { fs::write(cart.join("program.pbx"), 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 repository_test_cartridges_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join("test-cartridges") } 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 { valid_cartridge_with(app_mode, 9, "Valid Cart") } fn valid_cartridge_with(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge { Cartridge { app_id, title: title.into(), app_version: "1.0.0".into(), app_mode, capabilities: caps::NONE, program: halting_program(), assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], } } fn vm_session_tick_index(firmware: &mut Firmware, task_id: TaskId) -> u64 { firmware .os .sessions() .vm_session_for_task(task_id) .expect("VM session should exist") .runtime .tick_index } 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 reset_clears_vm_sessions_and_runtime_identity() { 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_eq!(firmware.os.sessions().vm_session_count(), 1); assert_eq!(firmware.os.vm().current_cartridge_title(), "Valid Cart"); firmware.change_state(FirmwareState::Reset(ResetStep), &signals, &mut platform); assert_eq!(firmware.os.sessions().vm_session_count(), 0); assert_eq!(firmware.os.vm().current_cartridge_title(), ""); } #[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 hub_home_clicking_resident_game_resumes_same_task_without_reloading() { 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 launch_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&launch_click, &mut platform); firmware.tick(&InputSignals::default(), &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(&InputSignals::default(), &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); 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(&InputSignals::default(), &mut platform); firmware.tick(&launch_click, &mut platform); match &firmware.state { FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task), other => panic!("expected resident GameRunning state, got {:?}", other), } assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); 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) ); let held_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&held_click, &mut platform); 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 hub_home_clicking_different_game_terminates_previous_resident_session() { let root = TestGameRoot::new(); root.add_game("stress", true); root.add_game_with_identity("zzz-dummy", 43, "Zzz Dummy", "1.0.0", 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 stress_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&stress_click, &mut platform); firmware.tick(&InputSignals::default(), &mut platform); let first_game_task = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected first GameRunning state, got {:?}", other), }; firmware.request_home_from_host(); firmware.tick(&InputSignals::default(), &mut platform); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(first_game_task)); firmware.tick(&InputSignals::default(), &mut platform); let dummy_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; firmware.tick(&dummy_click, &mut platform); assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(first_game_task), Ok(ProcessState::Stopped) ); assert_eq!( firmware .os .sessions() .vm_session_for_task(first_game_task) .map(|session| session.task_id), None ); let second_game_task = firmware .os .lifecycle() .resident_game_task() .expect("replacement game should be resident"); assert_ne!(second_game_task, first_game_task); assert_eq!( firmware .os .sessions() .vm_session_for_task(second_game_task) .map(|session| session.app_id), Some(43) ); firmware.tick(&InputSignals::default(), &mut platform); match &firmware.state { FirmwareState::GameRunning(step) => assert_eq!(step.task_id, second_game_task), other => panic!("expected replacement GameRunning state, got {:?}", other), } } #[test] fn repository_stress_to_dummy_boy_switch_is_end_to_end() { let library = discover_games_root(repository_test_cartridges_root()); assert!( library .entries() .iter() .any(|entry| entry.app_id == 1 && entry.title == "Stress Console") ); assert!( library.entries().iter().any(|entry| entry.app_id == 2 && entry.title == "Dummy Boy") ); let (mut firmware, mut platform) = hub_home_firmware(); firmware.hub.set_game_library(library); firmware.tick(&InputSignals::default(), &mut platform); let stress_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; firmware.tick(&stress_click, &mut platform); firmware.tick(&InputSignals::default(), &mut platform); let stress_task = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected Stress GameRunning state, got {:?}", other), }; assert_eq!(firmware.os.vm().current_app_id(), 1); assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console"); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, Some(100) ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, Some(1) ); firmware.request_home_from_host(); firmware.tick(&InputSignals::default(), &mut platform); let hub_ownership = firmware.os.vm().active_render_ownership(); assert_eq!(hub_ownership.app_mode, AppMode::Shell); assert_eq!(hub_ownership.app_id, 0); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, Some(100) ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, Some(1) ); firmware.tick(&InputSignals::default(), &mut platform); let dummy_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&dummy_click, &mut platform); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, None ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, None ); assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none()); assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none()); assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(stress_task), Ok(ProcessState::Stopped) ); assert_eq!( firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id), None ); let dummy_task = firmware .os .lifecycle() .resident_game_task() .expect("Dummy Boy should be resident after switch load"); assert_ne!(dummy_task, stress_task); assert_eq!( firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| { (session.app_id, session.title.clone(), session.app_version.clone()) }), Some((2, "Dummy Boy".to_string(), "0.1.0".to_string())) ); firmware.tick(&InputSignals::default(), &mut platform); match &firmware.state { FirmwareState::GameRunning(step) => assert_eq!(step.task_id, dummy_task), other => panic!("expected Dummy Boy GameRunning state, got {:?}", other), } assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(dummy_task)); assert_eq!(firmware.os.vm().current_app_id(), 2); assert_eq!(firmware.os.vm().current_cartridge_title(), "Dummy Boy"); firmware.tick(&InputSignals::default(), &mut platform); let dummy_ownership = firmware.os.vm().active_render_ownership(); assert_eq!(dummy_ownership.app_mode, AppMode::Game); assert_eq!(dummy_ownership.app_id, 2); let front = platform.local_hardware().gfx.front_buffer(); let center = 240 + 135 * 480; assert_eq!(front[center], Color::GREEN.raw()); firmware.request_home_from_host(); firmware.tick(&InputSignals::default(), &mut platform); let hub_ownership = firmware.os.vm().active_render_ownership(); assert_eq!(hub_ownership.app_mode, AppMode::Shell); assert_eq!(hub_ownership.app_id, 0); firmware.tick(&InputSignals::default(), &mut platform); let stress_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; firmware.tick(&stress_click, &mut platform); assert_eq!(firmware.os.lifecycle().task_state(dummy_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(dummy_task), Ok(ProcessState::Stopped) ); assert_eq!( firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| session.task_id), None ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, Some(100) ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, Some(1) ); assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_some()); assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_some()); let second_stress_task = firmware .os .lifecycle() .resident_game_task() .expect("Stress should be resident after switching back"); assert_ne!(second_stress_task, stress_task); assert_ne!(second_stress_task, dummy_task); firmware.tick(&InputSignals::default(), &mut platform); match &firmware.state { FirmwareState::GameRunning(step) => assert_eq!(step.task_id, second_stress_task), other => panic!("expected second Stress GameRunning state, got {:?}", other), } assert_eq!(firmware.os.vm().current_app_id(), 1); assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console"); firmware.tick(&InputSignals::default(), &mut platform); let stress_ownership = firmware.os.vm().active_render_ownership(); assert_eq!(stress_ownership.app_mode, AppMode::Game); assert_eq!(stress_ownership.app_id, 1); } #[test] fn resident_game_termination_noops_when_no_game_is_resident() { let (mut firmware, mut platform) = hub_home_firmware(); let ownership_before = firmware.os.vm().active_render_ownership(); let outcome = resident_game_termination::terminate_resident_game( &mut firmware.os, &mut platform, true, ) .expect("no resident game termination should not fail"); assert_eq!(outcome, ResidentGameTerminationOutcome::NoResidentGame); assert_eq!(firmware.os.vm().active_render_ownership(), ownership_before); assert_eq!(firmware.os.lifecycle().resident_game_task(), None); assert!( !firmware .os .recent_logs(16) .iter() .any(|event| event.msg.contains("Resident game killed from Hub")) ); } #[test] fn resident_game_termination_closes_session_and_cleans_runtime_state() { let library = discover_games_root(repository_test_cartridges_root()); let (mut firmware, mut platform) = hub_home_firmware(); firmware.hub.set_game_library(library); firmware.tick(&InputSignals::default(), &mut platform); let stress_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; firmware.tick(&stress_click, &mut platform); firmware.tick(&InputSignals::default(), &mut platform); let stress_task = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected Stress GameRunning state, got {:?}", other), }; firmware.request_home_from_host(); firmware.tick(&InputSignals::default(), &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task)); assert!(firmware.game_lifecycle_audio_paused()); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, Some(100) ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, Some(1) ); assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_some()); assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_some()); let outcome = resident_game_termination::terminate_resident_game( &mut firmware.os, &mut platform, true, ) .expect("resident game termination should succeed"); assert_eq!( outcome, ResidentGameTerminationOutcome::Terminated { task_id: stress_task, title: "Stress Console".to_string() } ); assert_eq!(firmware.os.lifecycle().resident_game_task(), None); assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(stress_task), Ok(ProcessState::Stopped) ); assert_eq!( firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id), None ); assert!(!firmware.game_lifecycle_audio_paused()); let ownership = firmware.os.vm().active_render_ownership(); assert_eq!(ownership.app_mode, AppMode::Shell); assert_eq!(ownership.app_id, 0); assert_eq!(platform.local_hardware().frame_composer.active_scene_id(), None); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, None ); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, None ); assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none()); assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none()); assert!( firmware.os.recent_logs(32).iter().any(|event| { event.level == LogLevel::Info && event.source == LogSource::Hub && event.msg == "Resident game killed from Hub: Stress Console" }), "manual Hub kill should emit an INFO log" ); } #[test] fn failed_game_switch_returns_home_without_restoring_previous_game() { let root = TestGameRoot::new(); root.add_game("stress", true); root.add_game_with_program("zzz-broken", 43, "Zzz Broken", "1.0.0", Some(vec![0, 0, 0, 0])); 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 stress_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; firmware.tick(&stress_click, &mut platform); firmware.tick(&InputSignals::default(), &mut platform); let first_game_task = match &firmware.state { FirmwareState::GameRunning(step) => step.task_id, other => panic!("expected first GameRunning state, got {:?}", other), }; firmware.request_home_from_host(); firmware.tick(&InputSignals::default(), &mut platform); firmware.tick(&InputSignals::default(), &mut platform); let broken_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; firmware.tick(&broken_click, &mut platform); assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed)); assert_eq!( firmware .os .sessions() .vm_session_for_task(first_game_task) .map(|session| session.task_id), None ); let partial_task = firmware .os .lifecycle() .resident_game_task() .expect("failed target should be resident until recovery tick"); assert_ne!(partial_task, first_game_task); firmware.tick(&InputSignals::default(), &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().resident_game_task(), None); assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed)); assert_eq!(firmware.os.lifecycle().task_state(partial_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(partial_task), Ok(ProcessState::Stopped) ); assert_eq!( firmware.os.sessions().vm_session_for_task(partial_task).map(|session| session.task_id), None ); assert!( firmware .os .recent_logs(16) .iter() .any(|event| event.msg.contains("Failed to switch to game Zzz Broken app_id 43")) ); } #[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_delivers_pause_tick_before_forced_suspension() { 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(); assert_eq!(render_ownership_before_home.app_mode, AppMode::Game); assert_eq!(render_ownership_before_home.app_id, 9); 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 + 1); 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!(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) ); let tick_index_after_suspension = firmware.os.vm().tick_index(); firmware.tick(&signals, &mut platform); assert_eq!(firmware.os.vm().tick_index(), tick_index_after_suspension); } #[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_eq!(hub_ownership.app_mode, AppMode::Shell); assert_eq!(hub_ownership.app_id, 0); 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); 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 tick_index_before_native_shell_updates = firmware.os.vm().tick_index(); for _ in 0..61 { firmware.tick(&signals, &mut platform); } assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); assert_eq!(firmware.os.vm().tick_index(), tick_index_before_native_shell_updates); 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 resident_game_and_vm_shell_sessions_tick_independently() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); firmware.load_cartridge(valid_cartridge_with(AppMode::Game, 9, "Resident 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.tick(&signals, &mut platform); let game_tick_before_home = vm_session_tick_index(&mut firmware, game_task); firmware.request_home_from_host(); firmware.tick(&signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); let game_tick_after_suspend = vm_session_tick_index(&mut firmware, game_task); assert_eq!(game_tick_after_suspend, game_tick_before_home + 1); firmware.load_cartridge(valid_cartridge_with(AppMode::Shell, 77, "VM Shell")); firmware.tick(&signals, &mut platform); let shell_task = match &firmware.state { FirmwareState::ShellRunning(step) => step.task_id, other => panic!("expected ShellRunning state, got {:?}", other), }; assert_ne!(shell_task, game_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 shell_tick_before = vm_session_tick_index(&mut firmware, shell_task); for _ in 0..3 { firmware.tick(&signals, &mut platform); } assert_eq!(vm_session_tick_index(&mut firmware, game_task), game_tick_after_suspend); assert_eq!(vm_session_tick_index(&mut firmware, shell_task), shell_tick_before + 3); 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().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()); firmware.tick(&signals, &mut platform); match &firmware.state { FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task), other => panic!("expected GameRunning state, got {:?}", other), } assert!(vm_session_tick_index(&mut firmware, game_task) > game_tick_after_suspend); } #[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"); } }