diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index f00f3966..d0c7c293 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -52,45 +52,53 @@ impl HubHomeStep { ctx.os.windows().set_focus(id); return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id))); } - Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { - Ok(cartridge) => { - if cartridge.app_mode == AppMode::Game - && let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task() - { - let resident_app_id = ctx - .os - .sessions() - .vm_session_for_task(resident_task_id) - .map(|session| session.app_id) - .unwrap_or(0); + Some(SystemProfileAction::LaunchGame { target }) => { + match CartridgeLoader::load(&target.path) { + Ok(cartridge) => { + if cartridge.app_mode == AppMode::Game + && let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task() + { + let resident_app_id = ctx + .os + .sessions() + .vm_session_for_task(resident_task_id) + .map(|session| session.app_id) + .unwrap_or(0); - if resident_app_id == cartridge.app_id { - return Some(FirmwareState::ResumeResidentGame); + if resident_app_id == cartridge.app_id { + return Some(FirmwareState::ResumeResidentGame); + } + + ctx.os.log( + LogLevel::Warn, + LogSource::Hub, + 0, + format!( + "Cannot launch Home game {} while resident game app_id {} is active", + cartridge.title, resident_app_id + ), + ); + return None; } + return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new( + cartridge, + ))); + } + Err(error) => { ctx.os.log( - LogLevel::Warn, + LogLevel::Error, LogSource::Hub, 0, format!( - "Cannot launch Home game {} while resident game app_id {} is active", - cartridge.title, resident_app_id + "Failed to launch Home game {}: {:?}", + target.path.display(), + error ), ); - return None; } - - return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge))); } - Err(error) => { - ctx.os.log( - LogLevel::Error, - LogSource::Hub, - 0, - format!("Failed to launch Home game {}: {:?}", path.display(), error), - ); - } - }, + } Some(SystemProfileAction::CloseShell) | None => {} } diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 20be73b7..4ef78157 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -20,7 +20,7 @@ pub use services::foreground::{ }; pub use services::fs; pub use services::game_library::{ - GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root, + GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, GameSwitchTarget, discover_games_root, }; pub use services::memcard::MemcardAsyncLaneOperation; pub use services::process; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 28038739..b2f2ce26 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -1,12 +1,11 @@ use crate::task::TaskId; -use crate::{CrashReport, GameLibrary, SystemOS}; +use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform, ShellUiFramePacket, }; -use std::path::PathBuf; const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 }; const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 172, w: 112, h: 32 }; @@ -83,7 +82,7 @@ impl NativeShellApp { #[derive(Debug, Clone, PartialEq, Eq)] pub enum SystemProfileAction { LaunchNativeShell(NativeShellApp), - LaunchGame { path: PathBuf }, + LaunchGame { target: GameSwitchTarget }, CloseShell, } @@ -249,7 +248,7 @@ fn action_for_click( .find(|(index, _entry)| rect_contains(game_row_rect(*index), x, y)) .map(|(_index, entry)| entry) { - return Some(SystemProfileAction::LaunchGame { path: entry.path.clone() }); + return Some(SystemProfileAction::LaunchGame { target: entry.switch_target() }); } HOME_BUTTONS @@ -434,6 +433,7 @@ mod tests { use crate::windows::WindowOwner; use prometeu_drivers::TestPlatform; use prometeu_hal::{InputSignals, RuntimePlatform}; + use std::path::PathBuf; fn focused_native_shell_fixture( signals: &InputSignals, @@ -535,9 +535,19 @@ mod tests { discovered_at: std::time::SystemTime::UNIX_EPOCH, }; + let action = + action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)); + assert_eq!( - action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)), - Some(SystemProfileAction::LaunchGame { path: entry.path.clone() }) + action, + Some(SystemProfileAction::LaunchGame { + target: GameSwitchTarget { + path: entry.path.clone(), + app_id: 42, + title: "Stress".to_string(), + app_version: "1.0.0".to_string(), + } + }) ); } diff --git a/crates/console/prometeu-system/src/services/game_library.rs b/crates/console/prometeu-system/src/services/game_library.rs index bbf9728c..9d357203 100644 --- a/crates/console/prometeu-system/src/services/game_library.rs +++ b/crates/console/prometeu-system/src/services/game_library.rs @@ -25,6 +25,29 @@ impl GameLibraryEntry { discovered_at, } } + + pub fn switch_target(&self) -> GameSwitchTarget { + GameSwitchTarget::from_entry(self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GameSwitchTarget { + pub path: PathBuf, + pub app_id: u32, + pub title: String, + pub app_version: String, +} + +impl GameSwitchTarget { + pub fn from_entry(entry: &GameLibraryEntry) -> Self { + Self { + path: entry.path.clone(), + app_id: entry.app_id, + title: entry.title.clone(), + app_version: entry.app_version.clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -256,6 +279,31 @@ mod tests { })); } + #[test] + fn switch_target_preserves_resolved_identity() { + let path = PathBuf::from("test-cartridges/stress-console"); + let entry = GameLibraryEntry::new( + path.clone(), + CartridgeManifest { + magic: "PMTU".to_string(), + cartridge_version: 1, + app_id: 42, + title: "Stress".to_string(), + app_version: "1.0.0".to_string(), + app_mode: AppMode::Game, + capabilities: vec![], + }, + UNIX_EPOCH, + ); + + let target = entry.switch_target(); + + assert_eq!(target.path, path); + assert_eq!(target.app_id, 42); + assert_eq!(target.title, "Stress"); + assert_eq!(target.app_version, "1.0.0"); + } + #[test] fn missing_root_returns_diagnostic_and_no_entries() { let root = TestRoot::new(); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 9c08136e..8902af04 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md index dc71d06a..b8f88cca 100644 --- a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md +++ b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md @@ -2,7 +2,7 @@ id: PLN-0157 ticket: system-os-cartridge-switch-orchestrator title: Introduce SystemOS Game Switch Target Contract -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture]