2026-07-05 09:31:27 +01:00

1583 lines
62 KiB
Rust

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<CertificationConfig>) -> 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<FirmwareState> {
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<FirmwareState> {
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<Vec<u8>>,
) -> 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<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 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");
}
}