diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index eeba7e41..03e783a5 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -179,11 +179,59 @@ mod tests { use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; - use prometeu_hal::syscalls::caps; - use prometeu_system::CrashReport; + 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, discover_games_root}; + use std::fs; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + + static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0); + + struct TestGameRoot { + path: PathBuf, + } + + impl TestGameRoot { + fn new() -> Self { + let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "prometeu-firmware-game-root-test-{}-{}", + std::process::id(), + unique + )); + fs::create_dir_all(&path).expect("test game root should be created"); + Self { path } + } + + fn add_game(&self, name: &str, with_program: bool) -> PathBuf { + let cart = self.path.join(name); + fs::create_dir_all(&cart).expect("test cartridge should be created"); + let manifest = r#"{ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": 42, + "title": "Stress", + "app_version": "1.0.0", + "app_mode": "Game" + }"#; + fs::write(cart.join("manifest.json"), manifest) + .expect("test manifest should be written"); + if with_program { + fs::write(cart.join("program.pbx"), halting_program()) + .expect("test program should be written"); + } + cart + } + } + + impl Drop for TestGameRoot { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } + } fn halting_program() -> Vec { let code = assemble("HALT").expect("assemble"); @@ -419,7 +467,7 @@ mod tests { #[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: 108, ..Default::default() }; + let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() }; firmware.tick(&signals, &mut platform); @@ -432,7 +480,7 @@ mod tests { 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: 108, ..Default::default() }; + 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 { @@ -455,7 +503,7 @@ mod tests { 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: 108, ..Default::default() }; + 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 { @@ -473,6 +521,108 @@ mod tests { ); } + #[test] + fn hub_home_click_launches_game_library_entry_through_load_cartridge() { + let root = TestGameRoot::new(); + root.add_game("stress", true); + + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.hub.set_game_library(discover_games_root(&root.path)); + firmware.tick(&InputSignals::default(), &mut platform); + + let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&signals, &mut platform); + + match &firmware.state { + FirmwareState::LoadCartridge(step) => { + assert_eq!(step.cartridge.title, "Stress"); + assert_eq!(step.cartridge.app_id, 42); + } + other => panic!("expected LoadCartridge state, got {:?}", other), + } + } + + #[test] + fn games_root_home_click_reaches_game_running() { + let root = TestGameRoot::new(); + root.add_game("stress", true); + + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.hub.set_game_library(discover_games_root(&root.path)); + firmware.tick(&InputSignals::default(), &mut platform); + + let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&signals, &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress"); + assert_eq!(firmware.os.windows().window_count(), 0); + } + + #[test] + fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() { + let root = TestGameRoot::new(); + root.add_game("broken", false); + + let (mut firmware, mut platform) = hub_home_firmware(); + let library = discover_games_root(&root.path); + assert!(library.is_empty()); + assert!(!library.diagnostics().is_empty()); + firmware.hub.set_game_library(library); + firmware.tick(&InputSignals::default(), &mut platform); + + let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&signals, &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.vm().current_cartridge_title(), ""); + } + + #[test] + fn direct_boot_target_still_reaches_game_running_without_games_root() { + let root = TestGameRoot::new(); + let cart = root.add_game("stress", true); + + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + firmware.boot_target = BootTarget::Cartridge { + path: cart.display().to_string(), + debug: false, + debug_port: 7777, + }; + + firmware.tick(&InputSignals::default(), &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress"); + } + + #[test] + fn hub_home_failed_game_launch_remains_home_and_logs_error() { + let root = TestGameRoot::new(); + let cart = root.add_game("stress", true); + + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.hub.set_game_library(discover_games_root(&root.path)); + fs::remove_file(cart.join("program.pbx")).expect("program should be removable"); + firmware.tick(&InputSignals::default(), &mut platform); + + let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&signals, &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert!( + firmware + .os + .recent_logs(16) + .iter() + .any(|entry| entry.msg.contains("Failed to launch Home game")) + ); + } + #[test] fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() { let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware(); @@ -576,4 +726,19 @@ mod tests { assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); assert_eq!(firmware.os.windows().window_count(), 0); } + + #[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"); + } } 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 ac909263..1e39275c 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 @@ -1,5 +1,8 @@ -use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, ShellRunningStep}; +use crate::firmware::firmware_state::{ + AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep, +}; use crate::firmware::prometeu_context::PrometeuContext; +use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::primitives::Rect; use prometeu_system::SystemProfileAction; @@ -21,16 +24,32 @@ impl HubHomeStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - if let Some(SystemProfileAction::LaunchNativeShell(app)) = outcome.action { - let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title()); - let id = ctx.os.windows().add_window( - app.title().to_string(), - WindowOwner::Task(task_id), - Rect { x: 40, y: 20, w: 400, h: 220 }, - app.color(), - ); - ctx.os.windows().set_focus(id); - return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id))); + match outcome.action { + Some(SystemProfileAction::LaunchNativeShell(app)) => { + let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title()); + let id = ctx.os.windows().add_window( + app.title().to_string(), + WindowOwner::Task(task_id), + Rect { x: 40, y: 20, w: 400, h: 220 }, + app.color(), + ); + ctx.os.windows().set_focus(id); + return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id))); + } + Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { + Ok(cartridge) => { + 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 => {} } None diff --git a/crates/console/prometeu-hal/src/cartridge.rs b/crates/console/prometeu-hal/src/cartridge.rs index 972c755f..6ab7e048 100644 --- a/crates/console/prometeu-hal/src/cartridge.rs +++ b/crates/console/prometeu-hal/src/cartridge.rs @@ -297,7 +297,7 @@ pub enum Capability { All, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct CartridgeManifest { pub magic: String, pub cartridge_version: u32, diff --git a/crates/console/prometeu-hal/src/syscalls.rs b/crates/console/prometeu-hal/src/syscalls.rs index 618e20d4..3dec9d38 100644 --- a/crates/console/prometeu-hal/src/syscalls.rs +++ b/crates/console/prometeu-hal/src/syscalls.rs @@ -29,7 +29,6 @@ pub use resolver::{ #[repr(u32)] pub enum Syscall { SystemHasCart = 0x0001, - SystemRunCart = 0x0002, GfxClear = 0x1001, GfxFillRect = 0x1002, GfxDrawLine = 0x1003, diff --git a/crates/console/prometeu-hal/src/syscalls/domains/system.rs b/crates/console/prometeu-hal/src/syscalls/domains/system.rs index 3d8b247a..acc59143 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/system.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/system.rs @@ -1,11 +1,6 @@ use crate::syscalls::{Syscall, SyscallRegistryEntry, caps}; -pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ - SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart") +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = + &[SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart") .rets(1) - .caps(caps::SYSTEM), - SyscallRegistryEntry::builder(Syscall::SystemRunCart, "system", "run_cart") - .caps(caps::SYSTEM) - .non_deterministic() - .cost(50), -]; + .caps(caps::SYSTEM)]; diff --git a/crates/console/prometeu-hal/src/syscalls/registry.rs b/crates/console/prometeu-hal/src/syscalls/registry.rs index 3d0a08a6..74f25f54 100644 --- a/crates/console/prometeu-hal/src/syscalls/registry.rs +++ b/crates/console/prometeu-hal/src/syscalls/registry.rs @@ -13,7 +13,6 @@ impl Syscall { pub fn from_u32(id: u32) -> Option { match id { 0x0001 => Some(Self::SystemHasCart), - 0x0002 => Some(Self::SystemRunCart), 0x1001 => Some(Self::GfxClear), 0x1002 => Some(Self::GfxFillRect), 0x1003 => Some(Self::GfxDrawLine), @@ -75,7 +74,6 @@ impl Syscall { pub fn name(&self) -> &'static str { match self { Self::SystemHasCart => "SystemHasCart", - Self::SystemRunCart => "SystemRunCart", Self::GfxClear => "GfxClear", Self::GfxFillRect => "GfxFillRect", Self::GfxDrawLine => "GfxDrawLine", diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index e4e22814..c4716508 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -72,6 +72,33 @@ fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() { assert_eq!(err, LoadError::UnknownSyscall { module: "gfx2d", name: "set_sprite", version: 1 }); } +#[test] +fn resolver_rejects_userland_system_run_cart_identity() { + assert!(Syscall::from_u32(0x0002).is_none()); + assert!(resolve_syscall("system", "run_cart", 1).is_none()); + + let requested = [SyscallIdentity { module: "system", name: "run_cart", version: 1 }]; + let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err(); + assert_eq!(err, LoadError::UnknownSyscall { module: "system", name: "run_cart", version: 1 }); + + let declared = [prometeu_bytecode::SyscallDecl { + module: "system".into(), + name: "run_cart".into(), + version: 1, + arg_slots: 0, + ret_slots: 0, + }]; + let err = resolve_declared_program_syscalls(&declared, caps::ALL).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::UnknownSyscall { + module: "system".into(), + name: "run_cart".into(), + version: 1, + } + ); +} + #[test] fn resolver_enforces_capabilities() { let requested = [SyscallIdentity { module: "gfx2d", name: "clear", version: 1 }]; diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index c0c052f0..bdce77e5 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -13,6 +13,9 @@ pub use services::async_work::{ AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority, }; pub use services::fs; +pub use services::game_library::{ + GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root, +}; pub use services::memcard::MemcardAsyncLaneOperation; pub use services::process; pub use services::task; 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 947a8551..d7bf48f9 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,4 +1,4 @@ -use crate::{CrashReport, SystemOS}; +use crate::{CrashReport, GameLibrary, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ @@ -6,13 +6,20 @@ use prometeu_hal::{ ShellUiFramePacket, }; use prometeu_vm::VirtualMachine; +use std::path::PathBuf; -const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 108, w: 112, h: 48 }; -const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 108, w: 112, h: 48 }; +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 }; const CLOSE_BUTTON: Rect = Rect { x: 420, y: 24, w: 16, h: 16 }; const VIEWPORT: Rect = Rect { x: 0, y: 0, w: 480, h: 270 }; const HOME_PANEL: Rect = Rect { x: 48, y: 58, w: 384, h: 154 }; const TITLE_PANEL: Rect = Rect { x: 72, y: 34, w: 336, h: 28 }; +const GAME_ROW_X: i32 = 72; +const GAME_ROW_Y: i32 = 90; +const GAME_ROW_W: i32 = 336; +const GAME_ROW_H: i32 = 22; +const GAME_ROW_GAP: i32 = 6; +const MAX_HOME_GAME_ROWS: usize = 3; #[cfg(test)] const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 }; @@ -29,6 +36,7 @@ const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122); /// PrometeuHub: Launcher and system UI environment. pub struct PrometeuHub { home_input_armed: bool, + game_library: GameLibrary, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -72,9 +80,10 @@ impl NativeShellApp { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SystemProfileAction { LaunchNativeShell(NativeShellApp), + LaunchGame { path: PathBuf }, CloseShell, } @@ -91,13 +100,21 @@ impl Default for PrometeuHub { impl PrometeuHub { pub fn new() -> Self { - Self { home_input_armed: false } + Self { home_input_armed: false, game_library: GameLibrary::empty() } } pub fn init(&mut self) { self.home_input_armed = false; } + pub fn set_game_library(&mut self, game_library: GameLibrary) { + self.game_library = game_library; + } + + pub fn game_library(&self) -> &GameLibrary { + &self.game_library + } + pub fn gui_update( &mut self, os: &mut SystemOS, @@ -120,6 +137,7 @@ impl PrometeuHub { input.touch().x(), input.touch().y(), input.touch().f().pressed, + self.game_library.entries(), ) { return Some(action); } @@ -132,7 +150,7 @@ impl PrometeuHub { let pointer_y = platform.input().touch().y(); let packet = if os.windows().window_count() == 0 { - render_home_packet(pointer_x, pointer_y) + render_home_packet(pointer_x, pointer_y, self.game_library.entries()) } else { let mut commands = Vec::new(); for window in os.windows().windows() { @@ -177,7 +195,13 @@ impl PrometeuHub { } } -fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option { +fn action_for_click( + in_shell: bool, + x: i32, + y: i32, + pressed: bool, + game_entries: &[crate::GameLibraryEntry], +) -> Option { if !pressed { return None; } @@ -186,12 +210,31 @@ fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option Rect { + Rect { + x: GAME_ROW_X, + y: GAME_ROW_Y + (GAME_ROW_H + GAME_ROW_GAP) * index as i32, + w: GAME_ROW_W, + h: GAME_ROW_H, + } +} + fn rect_contains(rect: Rect, x: i32, y: i32) -> bool { x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h } @@ -203,7 +246,11 @@ fn activation_input_down(input: &dyn InputPlatform) -> bool { || input.pad().start().down } -fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket { +fn render_home_packet( + pointer_x: i32, + pointer_y: i32, + game_entries: &[crate::GameLibraryEntry], +) -> ShellUiFramePacket { let mut commands = Vec::new(); commands.push(GfxUiCommand::Clear { color: COLOR_BG }); fill_rect(&mut commands, VIEWPORT, COLOR_BG); @@ -217,21 +264,26 @@ fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket { ); draw_panel(&mut commands, HOME_PANEL, COLOR_PANEL, COLOR_BORDER); - draw_buffer_text( - &mut commands, - HOME_PANEL.x + 26, - HOME_PANEL.y + 22, - "Native Shell Apps", - COLOR_TEXT, - ); + draw_buffer_text(&mut commands, HOME_PANEL.x + 26, HOME_PANEL.y + 22, "Games", COLOR_TEXT); draw_buffer_text( &mut commands, HOME_PANEL.x + 26, HOME_PANEL.y + 40, - "first retro ui slice", + "local library", COLOR_MUTED, ); + if game_entries.is_empty() { + draw_buffer_text(&mut commands, GAME_ROW_X, GAME_ROW_Y + 6, "No games found", COLOR_MUTED); + } else { + for (index, entry) in game_entries.iter().take(MAX_HOME_GAME_ROWS).enumerate() { + let rect = game_row_rect(index); + let hovered = rect_contains(rect, pointer_x, pointer_y); + let text = format!("{} #{} {}", entry.title, entry.app_id, entry.app_version); + draw_game_row(&mut commands, rect, &text, hovered); + } + } + for button in HOME_BUTTONS { let hovered = rect_contains(button.rect, pointer_x, pointer_y); draw_button(&mut commands, button.rect, button.label, hovered); @@ -326,9 +378,17 @@ fn draw_button(commands: &mut Vec, rect: Rect, label: &str, hovere Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 }, COLOR_MUTED, ); - fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 12, w: 16, h: 24 }, border); - draw_rect(commands, Rect { x: rect.x + 34, y: rect.y + 12, w: 62, h: 24 }, COLOR_PANEL_DARK); - draw_buffer_text(commands, rect.x + 40, rect.y + 21, label, COLOR_TEXT); + fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 8, w: 14, h: 16 }, border); + draw_rect(commands, Rect { x: rect.x + 34, y: rect.y + 8, w: 62, h: 16 }, COLOR_PANEL_DARK); + draw_buffer_text(commands, rect.x + 40, rect.y + 13, label, COLOR_TEXT); +} + +fn draw_game_row(commands: &mut Vec, rect: Rect, label: &str, hovered: bool) { + let fill = if hovered { COLOR_BUTTON_ACTIVE } else { COLOR_PANEL_DARK }; + let border = if hovered { COLOR_HILITE } else { COLOR_MUTED }; + fill_rect(commands, rect, fill); + draw_rect(commands, rect, border); + draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT); } fn draw_buffer_text(commands: &mut Vec, x: i32, y: i32, text: &str, color: Color) { @@ -342,7 +402,7 @@ mod tests { #[test] fn shell_a_button_click_emits_launch_action() { assert_eq!( - action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true), + action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true, &[]), Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA)) ); } @@ -354,7 +414,8 @@ mod tests { false, SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1, SHELL_B_BUTTON.y, - true + true, + &[] ), Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB)) ); @@ -363,18 +424,30 @@ mod tests { #[test] fn button_hit_test_excludes_right_and_bottom_edges() { assert_eq!( - action_for_click(false, SHELL_A_BUTTON.x + SHELL_A_BUTTON.w, SHELL_A_BUTTON.y, true), + action_for_click( + false, + SHELL_A_BUTTON.x + SHELL_A_BUTTON.w, + SHELL_A_BUTTON.y, + true, + &[] + ), None ); assert_eq!( - action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y + SHELL_A_BUTTON.h, true), + action_for_click( + false, + SHELL_A_BUTTON.x, + SHELL_A_BUTTON.y + SHELL_A_BUTTON.h, + true, + &[] + ), None ); } #[test] fn held_pointer_does_not_emit_click_action() { - assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false), None); + assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[]), None); } #[test] @@ -383,13 +456,38 @@ mod tests { assert!(!hub.home_input_armed); } + #[test] + fn game_row_click_emits_launch_game_action() { + let entry = crate::GameLibraryEntry { + manifest: prometeu_hal::cartridge::CartridgeManifest { + magic: "PMTU".to_string(), + cartridge_version: 1, + app_id: 42, + title: "Stress".to_string(), + app_version: "1.0.0".to_string(), + app_mode: prometeu_hal::app_mode::AppMode::Game, + capabilities: vec![], + }, + title: "Stress".to_string(), + app_id: 42, + app_version: "1.0.0".to_string(), + path: PathBuf::from("test-cartridges/stress-console"), + discovered_at: std::time::SystemTime::UNIX_EPOCH, + }; + + 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() }) + ); + } + #[test] fn close_button_click_emits_close_only_in_shell() { assert_eq!( - action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true), + action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), Some(SystemProfileAction::CloseShell) ); - assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true), None); + assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), None); } #[test] @@ -403,7 +501,7 @@ mod tests { #[test] fn home_render_produces_shell_ui_packet_commands() { - let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y); + let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[]); assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. }))); assert!(packet.commands.iter().any(|command| { diff --git a/crates/console/prometeu-system/src/services/game_library.rs b/crates/console/prometeu-system/src/services/game_library.rs new file mode 100644 index 00000000..bbf9728c --- /dev/null +++ b/crates/console/prometeu-system/src/services/game_library.rs @@ -0,0 +1,270 @@ +use prometeu_hal::app_mode::AppMode; +use prometeu_hal::cartridge::CartridgeManifest; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +#[derive(Debug, Clone)] +pub struct GameLibraryEntry { + pub manifest: CartridgeManifest, + pub title: String, + pub app_id: u32, + pub app_version: String, + pub path: PathBuf, + pub discovered_at: SystemTime, +} + +impl GameLibraryEntry { + fn new(path: PathBuf, manifest: CartridgeManifest, discovered_at: SystemTime) -> Self { + Self { + title: manifest.title.clone(), + app_id: manifest.app_id, + app_version: manifest.app_version.clone(), + manifest, + path, + discovered_at, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GameLibraryDiagnostic { + pub path: PathBuf, + pub reason: GameLibraryDiagnosticReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GameLibraryDiagnosticReason { + RootNotDirectory, + CandidateReadFailed, + MissingManifest, + InvalidManifest, + UnsupportedVersion, + MissingProgram, + NonGameAppMode, +} + +#[derive(Debug, Clone, Default)] +pub struct GameLibrary { + entries: Vec, + diagnostics: Vec, +} + +impl GameLibrary { + pub fn empty() -> Self { + Self::default() + } + + pub fn entries(&self) -> &[GameLibraryEntry] { + &self.entries + } + + pub fn diagnostics(&self) -> &[GameLibraryDiagnostic] { + &self.diagnostics + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +pub fn discover_games_root(root: impl AsRef) -> GameLibrary { + let root = root.as_ref(); + let discovered_at = SystemTime::now(); + + if !root.is_dir() { + return GameLibrary { + entries: Vec::new(), + diagnostics: vec![GameLibraryDiagnostic { + path: root.to_path_buf(), + reason: GameLibraryDiagnosticReason::RootNotDirectory, + }], + }; + } + + let read_dir = match fs::read_dir(root) { + Ok(read_dir) => read_dir, + Err(_) => { + return GameLibrary { + entries: Vec::new(), + diagnostics: vec![GameLibraryDiagnostic { + path: root.to_path_buf(), + reason: GameLibraryDiagnosticReason::CandidateReadFailed, + }], + }; + } + }; + + let mut entries = Vec::new(); + let mut diagnostics = Vec::new(); + + for item in read_dir { + let Ok(item) = item else { + diagnostics.push(GameLibraryDiagnostic { + path: root.to_path_buf(), + reason: GameLibraryDiagnosticReason::CandidateReadFailed, + }); + continue; + }; + + let path = item.path(); + if !path.is_dir() { + continue; + } + + match discover_candidate(&path, discovered_at) { + Ok(entry) => entries.push(entry), + Err(reason) => diagnostics.push(GameLibraryDiagnostic { path, reason }), + } + } + + entries.sort_by(|left, right| { + left.title + .cmp(&right.title) + .then_with(|| left.app_id.cmp(&right.app_id)) + .then_with(|| left.path.cmp(&right.path)) + }); + + GameLibrary { entries, diagnostics } +} + +fn discover_candidate( + path: &Path, + discovered_at: SystemTime, +) -> Result { + let manifest_path = path.join("manifest.json"); + if !manifest_path.exists() { + return Err(GameLibraryDiagnosticReason::MissingManifest); + } + + let manifest_content = fs::read_to_string(manifest_path) + .map_err(|_| GameLibraryDiagnosticReason::InvalidManifest)?; + let manifest: CartridgeManifest = serde_json::from_str(&manifest_content) + .map_err(|_| GameLibraryDiagnosticReason::InvalidManifest)?; + + if manifest.magic != "PMTU" { + return Err(GameLibraryDiagnosticReason::InvalidManifest); + } + if manifest.cartridge_version != 1 { + return Err(GameLibraryDiagnosticReason::UnsupportedVersion); + } + if manifest.app_mode != AppMode::Game { + return Err(GameLibraryDiagnosticReason::NonGameAppMode); + } + if !path.join("program.pbx").exists() { + return Err(GameLibraryDiagnosticReason::MissingProgram); + } + + Ok(GameLibraryEntry::new(path.to_path_buf(), manifest, discovered_at)) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::UNIX_EPOCH; + + static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0); + + struct TestRoot { + path: PathBuf, + } + + impl TestRoot { + fn new() -> Self { + let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "prometeu-game-library-test-{}-{}", + std::process::id(), + unique + )); + fs::create_dir_all(&path).expect("test root should be created"); + Self { path } + } + + fn add_candidate(&self, name: &str, manifest: serde_json::Value, with_program: bool) { + let candidate = self.path.join(name); + fs::create_dir_all(&candidate).expect("candidate should be created"); + fs::write(candidate.join("manifest.json"), manifest.to_string()) + .expect("manifest should be written"); + if with_program { + fs::write(candidate.join("program.pbx"), [0_u8]) + .expect("program should be written"); + } + } + } + + impl Drop for TestRoot { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } + } + + fn manifest(title: &str, app_id: u32, app_mode: &str) -> serde_json::Value { + json!({ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": app_id, + "title": title, + "app_version": "1.0.0", + "app_mode": app_mode + }) + } + + #[test] + fn discovery_returns_only_immediate_valid_game_directories() { + let root = TestRoot::new(); + root.add_candidate("zeta", manifest("Zeta", 2, "Game"), true); + root.add_candidate("alpha", manifest("Alpha", 1, "Game"), true); + root.add_candidate("shell", manifest("Shell", 3, "Shell"), true); + root.add_candidate("missing-program", manifest("Missing Program", 4, "Game"), false); + fs::write(root.path.join("loose-file.txt"), "ignored").expect("file should be written"); + + let nested_parent = root.path.join("nested-parent"); + fs::create_dir_all(nested_parent.join("nested-game")).expect("nested dir should exist"); + fs::write( + nested_parent.join("nested-game").join("manifest.json"), + manifest("Nested", 5, "Game").to_string(), + ) + .expect("nested manifest should be written"); + fs::write(nested_parent.join("nested-game").join("program.pbx"), [0_u8]) + .expect("nested program should be written"); + + let library = discover_games_root(&root.path); + + assert_eq!(library.entries().len(), 2); + assert_eq!(library.entries()[0].title, "Alpha"); + assert_eq!(library.entries()[0].app_id, 1); + assert_eq!(library.entries()[0].app_version, "1.0.0"); + assert_eq!(library.entries()[0].manifest.app_mode, AppMode::Game); + assert!(library.entries()[0].path.ends_with("alpha")); + assert!(library.entries()[0].discovered_at >= UNIX_EPOCH); + assert_eq!(library.entries()[1].title, "Zeta"); + + assert!(library.diagnostics().iter().any(|diagnostic| { + diagnostic.path.ends_with("shell") + && diagnostic.reason == GameLibraryDiagnosticReason::NonGameAppMode + })); + assert!(library.diagnostics().iter().any(|diagnostic| { + diagnostic.path.ends_with("missing-program") + && diagnostic.reason == GameLibraryDiagnosticReason::MissingProgram + })); + assert!(library.diagnostics().iter().any(|diagnostic| { + diagnostic.path.ends_with("nested-parent") + && diagnostic.reason == GameLibraryDiagnosticReason::MissingManifest + })); + } + + #[test] + fn missing_root_returns_diagnostic_and_no_entries() { + let root = TestRoot::new(); + let missing = root.path.join("missing"); + + let library = discover_games_root(&missing); + + assert!(library.is_empty()); + assert_eq!(library.diagnostics().len(), 1); + assert_eq!(library.diagnostics()[0].reason, GameLibraryDiagnosticReason::RootNotDirectory); + } +} diff --git a/crates/console/prometeu-system/src/services/mod.rs b/crates/console/prometeu-system/src/services/mod.rs index efe827c6..1af5a6c7 100644 --- a/crates/console/prometeu-system/src/services/mod.rs +++ b/crates/console/prometeu-system/src/services/mod.rs @@ -1,5 +1,6 @@ pub mod async_work; pub mod fs; +pub mod game_library; pub mod memcard; pub mod process; pub mod task; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs index b650f02f..4e766e2f 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs @@ -168,13 +168,9 @@ impl NativeInterface for VmRuntimeHost<'_> { VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id)) })?; - match syscall { - Syscall::SystemHasCart => { - ret.push_bool(true); - return Ok(()); - } - Syscall::SystemRunCart => return Ok(()), - _ => {} + if syscall == Syscall::SystemHasCart { + ret.push_bool(true); + return Ok(()); } self.ensure_game_profile_syscall(syscall)?; @@ -183,7 +179,6 @@ impl NativeInterface for VmRuntimeHost<'_> { match syscall { Syscall::SystemHasCart => unreachable!(), - Syscall::SystemRunCart => unreachable!(), Syscall::GfxClear => { let color = self.get_color(expect_int(args, 0)?)?; self.gfx2d_commands.push(Gfx2dCommand::Clear { color }); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs index 6fd57e59..5f08db87 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs @@ -487,20 +487,15 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); - let mut asset_ready = false; - for _ in 0..1_000 { - match platform.local_hardware().assets.status(handle) { - LoadStatus::READY => { - asset_ready = true; - break; - } - LoadStatus::PENDING | LoadStatus::LOADING => { - std::thread::yield_now(); - } - other => panic!("unexpected asset status before commit: {:?}", other), - } + let start = Instant::now(); + let mut status = platform.local_hardware().assets.status(handle); + while matches!(status, LoadStatus::PENDING | LoadStatus::LOADING) + && start.elapsed() < std::time::Duration::from_secs(5) + { + std::thread::sleep(std::time::Duration::from_millis(10)); + status = platform.local_hardware().assets.status(handle); } - assert!(asset_ready, "asset did not become ready before commit"); + assert_eq!(status, LoadStatus::READY, "asset did not become ready before commit"); assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok); platform.local_hardware().assets.apply_commits(); diff --git a/crates/host/prometeu-host-desktop-winit/src/lib.rs b/crates/host/prometeu-host-desktop-winit/src/lib.rs index a91c231f..0ce99800 100644 --- a/crates/host/prometeu-host-desktop-winit/src/lib.rs +++ b/crates/host/prometeu-host-desktop-winit/src/lib.rs @@ -33,6 +33,10 @@ struct Cli { #[arg(long, value_name = "PATH")] fs_root: Option, + /// Root directory for local game cartridge discovery + #[arg(long, value_name = "PATH")] + games_root: Option, + /// Path to the capability configuration file #[arg(long, value_name = "PATH")] cap: Option, @@ -55,6 +59,9 @@ pub fn run() -> Result<(), Box> { let event_loop = EventLoop::new()?; let mut runner = HostRunner::new(fs_root, cap_config); + if let Some(games_root) = cli.games_root { + runner.set_games_root(games_root); + } runner.set_boot_target(boot_target); event_loop.run_app(&mut runner)?; diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index feb56c4a..c6a9cc8c 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -12,7 +12,7 @@ use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks} use prometeu_firmware::{BootTarget, Firmware, FirmwareState}; use prometeu_hal::RuntimePlatform; use prometeu_hal::telemetry::CertificationConfig; -use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig}; +use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root}; use std::sync::Arc; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -151,6 +151,18 @@ impl HostRunner { self.debugger.setup_boot_target(&boot_target, &mut self.firmware); } + pub(crate) fn set_games_root(&mut self, games_root: String) { + let library = discover_games_root(&games_root); + for diagnostic in library.diagnostics() { + eprintln!( + "games-root: omitted candidate {}: {:?}", + diagnostic.path.display(), + diagnostic.reason + ); + } + self.firmware.hub.set_game_library(library); + } + /// Creates a new desktop runner instance. pub(crate) fn new(fs_root: Option, cap_config: Option) -> Self { let target_fps = 60; diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d40b78be..48b236ba 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,5 @@ -{"type":"meta","next_id":{"DSC":43,"AGD":44,"DEC":35,"PLN":129,"LSN":51,"CLSN":1}} +{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":37,"PLN":136,"LSN":51,"CLSN":1}} +{"type":"discussion","id":"DSC-0043","status":"open","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-03","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[],"plans":[],"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":"open","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-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} @@ -13,7 +14,7 @@ {"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} {"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]} {"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]} {"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} diff --git a/discussion/workflow/agendas/AGD-0003-system-run-cart.md b/discussion/workflow/agendas/AGD-0003-system-run-cart.md index c377c0ab..b0e1b2ad 100644 --- a/discussion/workflow/agendas/AGD-0003-system-run-cart.md +++ b/discussion/workflow/agendas/AGD-0003-system-run-cart.md @@ -2,7 +2,7 @@ id: AGD-0003 ticket: system-run-cart title: Agenda - System Run Cart -status: open +status: accepted created: 2026-03-27 resolved: decision: @@ -13,11 +13,14 @@ tags: [] ## Problema -A syscall `system.run_cart` existe na ABI pública, mas hoje não produz efeito real no runtime. +`system.run_cart` existe na ABI pública, mas hoje não produz efeito real no runtime. +Isso expõe duas questões: a superfície atual é um placebo e, mesmo quando houver +fluxo real, rodar cartuchos não deve ser uma operação userland. A autoridade +para iniciar cartuchos deve pertencer ao sistema/firmware. Isso cria uma falsa promessa de plataforma: -- o programa consegue declarar e chamar a syscall; +- o programa consegue declarar e chamar uma operação que aparenta iniciar cartuchos; - o loader aceita sua existência; - o runtime retorna sucesso vazio; - nenhuma troca de cartucho, transição de firmware, validação de alvo ou política de segurança ocorre. @@ -25,56 +28,92 @@ Isso cria uma falsa promessa de plataforma: ## Dor - A ABI expõe uma capability de sistema que, na prática, nao existe. +- Mesmo se implementada, a operação não deve ser uma syscall userland comum: + apps não devem iniciar cartuchos diretamente. - O guest pode acreditar que solicitou uma troca de app quando nada aconteceu. - Isso corrói a confiabilidade da plataforma: a interface pública deixa de ser contrato e vira placebo. - Qualquer ferramenta, SDK ou documentação construída sobre essa syscall passa a modelar um comportamento inexistente. ## Alvo da Discussao -Definir o contrato real de `system.run_cart` do ponto de vista: +Definir o contrato real da troca de cartucho iniciada pelo sistema do ponto de vista: - do guest; - do runtime; - do firmware; - do host/cartridge loader. -Ao final da discussão, deve ficar claro se a syscall: +Ao final da discussão, deve ficar claro se a superfície atual: -- permanece na ABI e ganha implementação completa; ou +- permanece apenas como mecanismo interno do sistema e sai da ABI userland; ou +- permanece na ABI somente como superfície privilegiada impossível de chamar por apps comuns; ou - sai temporariamente da ABI até existir fluxo fechado. +Esta discussao nao deve depender do fechamento previo do formato `.pmc`. +Para o primeiro fluxo fechado, `system.run_cart` pode resolver um alvo por +um loader ja disponivel, incluindo cartucho em diretorio ou fixture de teste. +O suporte ao artefato empacotado `.pmc` permanece responsabilidade da +`DSC-0003 / packed-cartridge-loader-pmc` e nao deve bloquear a prova inicial +de troca `Shell/Hub -> Game`. + +Tambem precisa ficar claro que `run_cart`, se existir, nao e o orquestrador da +troca. No boot direto por linha de comando, carregar um cartucho e simples +porque o runtime nasce ja apontado para aquele jogo. Com o SO em execucao, o +caso real e diferente: pode haver um Game residente/rodando, o usuario volta +para Home, e o sistema decide iniciar outro Game. Nesse caso o SO precisa +orquestrar fechamento/suspensao descartada do app atual, limpeza de estado, +resolucao do novo alvo, carregamento e transicao para o proximo estado de +firmware. O carregador de cartucho e apenas uma etapa desse fluxo. + +A direcao preferida e que a orquestracao de troca de cartucho viva no +SystemOS. Porem, essa agenda nao deve capturar o caso `Game -> Home -> mesmo +Game`: esse e o escopo da `AGD-0041`, que precisa fechar foreground, +pausa/suspensao e retorno ao mesmo Game. A troca `Home -> outro Game`, com +fechamento do jogo atual e carregamento de outro cartucho, pode merecer uma +agenda propria depois da `AGD-0041`, derivada do contrato de foreground que ela +fechar. + ## O Que Precisa Ser Definido 1. Semântica da operação. - O que significa "run cart": trocar imediatamente, agendar para o próximo frame, retornar ao firmware, reiniciar VM atual, ou apenas emitir uma intenção? + O que significa "run cart" quando existe SO: trocar imediatamente, + agendar para o próximo frame, iniciar uma transicao orquestrada pelo + sistema, ou apenas carregar bytes de cartucho como etapa interna? 2. Origem do alvo. Como o guest identifica o cartucho alvo: id numérico, nome canônico, path virtual, handle, manifesto, ou nenhum argumento nesta primeira versão? + Para v1, uma opcao aceitavel e limitar a resolucao a alvos conhecidos pelo host/test harness e carregaveis pelo loader de diretorio, deixando `.pmc` fora do caminho critico. 3. Modelo de segurança. - Quem pode chamar isso, sob qual capability, e com quais restrições para evitar que um app force navegação arbitrária do sistema? + Nenhum app/userland deve chamar `run_cart`. O sistema/firmware e o caminho + host/CLI/debug devem ser as unicas autoridades para iniciar cartuchos. + Se apps precisarem solicitar navegacao no futuro, isso deve ser uma + superficie nova e mediada pelo sistema. 4. Ponto de integração. - Onde a transição vive: `VirtualMachineRuntime`, firmware, host, ou combinação dos três? + Onde a transição vive: `VirtualMachineRuntime`, firmware, host, SystemOS, + ou combinação dos três? Para esta agenda, a decisao minima e separar o boot + direto host/CLI/debug da ABI guest. A orquestracao `Home -> outro Game` + pertence a `DSC-0043 / AGD-0044`. 5. Efeitos observáveis. - O que acontece com: - - estado do app atual; - - FS aberto; - - assets carregados; - - telemetria; - - crash reports; - - logs. + O que acontece quando um guest tenta chamar a superficie removida, e como + ferramentas/boot direto continuam carregando cartuchos sem depender de uma + syscall userland? 6. Contrato de erro. - Quais falhas retornam trap ao guest e quais viram evento de sistema. + Remocao da ABI significa erro de build/declaracao, syscall desconhecida, + ou bloqueio em registro/capability ate a limpeza completa das declaracoes? ## O Que Necessita Para Resolver -- fluxo fechado entre syscall -> runtime -> firmware -> loader -> novo app; -- modelo explícito para seleção do alvo; -- definição de lifecycle da troca de cartucho; -- testes de integração cobrindo transição bem-sucedida, alvo inválido, capability insuficiente e cleanup de estado. +- remocao de `system.run_cart` da ABI userland; +- definicao de que boot direto continua por caminho host/CLI/debug, nao por + syscall guest; +- separacao explicita entre boot direto, loader interno e orquestracao + `Home -> outro Game` da `DSC-0043 / AGD-0044`; +- testes garantindo que guest/userland nao pode declarar/chamar `run_cart`; +- testes garantindo que boot direto de cartucho continua funcional. ## Fora de Escopo @@ -82,13 +121,82 @@ Ao final da discussão, deve ficar claro se a syscall: - UX final do launcher/hub; - resolução remota ou download de cartuchos; - política de marketplace/distribuição. +- obrigatoriedade de boot a partir de `.pmc` no primeiro fluxo funcional. + +## Sugestao / Recomendacao + +Recomendo remover `system.run_cart` da ABI userland para v1. Nao ha necessidade +de compatibilidade com a superficie atual: ela nao deve permanecer como stub, +syscall reservada chamavel por guest, nem sucesso vazio. + +Rodar cartuchos deve ser uma operacao interna do SystemOS/firmware ou do +caminho host/CLI/debug, nunca uma syscall de app. O boot direto deve continuar +existindo porque e o fluxo essencial para testar cartuchos em desenvolvimento e +tambem representa o modo futuro de executar um jogo unico, como em uma +integracao tipo Steam. + +Recomendo tambem separar dois conceitos: + +- **boot direto:** usado por linha de comando/debug, inicia o runtime ja com um + cartucho alvo e pode continuar simples; +- **troca via SO:** fluxo orquestrado pelo SystemOS, que fecha o app + atual quando necessario, limpa estado cartridge-scoped, resolve o proximo + alvo e so entao chama o carregador. + +Essa agenda nao deve implementar o fluxo `Game -> Home -> mesmo Game`; esse +trabalho pertence a `AGD-0041`. Depois dela, a troca `Home -> outro Game` deve +ser aberta ou refinada como discussao propria, porque adiciona fechamento do +jogo atual, limpeza cartridge-scoped, resolucao de novo alvo e falhas de load. + +Se a plataforma precisar de uma ação solicitável por apps no futuro, ela deve +ser modelada separadamente como pedido de navegação mediado pelo sistema, com +nome e erros que não prometam boot direto. + +## Respostas Atuais + +- `system.run_cart` deve ser removido da ABI userland. +- Nao precisamos manter compatibilidade com a superficie atual. +- Nao deve existir syscall publica para app iniciar cartucho. +- Boot direto por linha de comando, host, debug ou fluxo equivalente de jogo + unico deve permanecer. +- O carregamento interno pode continuar usando loader ja disponivel, incluindo + cartucho em diretorio/fixture. +- `.pmc`, catalogo, UX final de launcher e troca `Game A -> Home -> Game B` + ficam fora desta agenda. +- Se no futuro apps precisarem pedir navegacao, isso deve ser uma nova + superficie mediada pelo sistema, nao a volta de `run_cart` userland. ## Critério de Saida Desta Agenda Esta agenda só pode virar PR de implementação quando existir decisão escrita para: -- assinatura final da syscall; -- local canônico da troca de estado; -- política de erro; -- limpeza de recursos; -- cobertura mínima de testes de integração. +- retirada da superfície userland atual; +- preservacao explicita do boot direto host/CLI/debug; +- comportamento esperado para declaracao/chamada guest obsoleta; +- fronteira com `AGD-0041` e `AGD-0044`; +- cobertura minima de testes para remocao userland e boot direto. + +## Discussao + +- 2026-07-03: A agenda deve ser aberta antes da `AGD-0041` como habilitadora + de validacao do ciclo `Shell/Hub -> Game`. Ficou definido como premissa de + agenda que o primeiro `system.run_cart` testavel nao depende de `.pmc`; ele + pode usar cartucho em diretorio ou fixture resolvida pelo host/test harness. +- 2026-07-03: Direcao ajustada: `run_cart` nao deve ser uma operacao userland. + A autoridade de iniciar cartuchos deve pertencer ao sistema/firmware; apps + podem, no maximo, solicitar navegacao por uma superficie separada e mediada. +- 2026-07-03: Direcao refinada: `run_cart` simples nao resolve o caso com SO. + Boot direto por linha de comando continua sendo um caminho simples, mas + iniciar outro jogo a partir do Home exige um orquestrador do SO/firmware que + fecha o app atual, limpa estado e so entao carrega o novo cartucho. +- 2026-07-03: Escopo refinado: `Game -> Home -> mesmo Game` pertence a + `AGD-0041`. A agenda atual nao deve absorver esse trabalho. Depois da + `AGD-0041`, a troca `Home -> outro Game` pode ser aberta ou refinada como + agenda propria para o orquestrador de troca de cartucho no SystemOS. +- 2026-07-03: A troca `Game A -> Home -> Game B` foi separada para + `DSC-0043 / AGD-0044`, mantendo esta agenda focada na superficie + `run_cart`/loader e sua remocao ou restricao como operacao userland. +- 2026-07-03: Direcao aceita para esta agenda: remover `system.run_cart` da ABI + userland sem necessidade de compatibilidade, manter boot direto para + CLI/debug/teste de cartuchos e futuro modo de jogo unico, e deixar navegacao + mediada por apps para uma superficie futura separada se ela vier a existir. diff --git a/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md b/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md new file mode 100644 index 00000000..e0fe0855 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md @@ -0,0 +1,145 @@ +--- +id: AGD-0044 +ticket: system-os-cartridge-switch-orchestrator +title: SystemOS Cartridge Switch Orchestrator +status: open +created: 2026-07-03 +resolved: +decision: +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Contexto + +A `AGD-0041` cobre o caso `Game -> Home/Shell -> mesmo Game`: o usuario sai do +foreground do jogo, o SO assume a tela, Shell/Hub pode rodar, e depois o mesmo +Game e retomado. + +Existe um segundo caso, diferente, que nao deve ser escondido dentro da +`AGD-0041`: `Game A -> Home -> Game B`. + +Nesse fluxo, o usuario esta com um Game rodando, aperta Home, volta para o SO e +pede para iniciar outro jogo. O SystemOS nao deve simplesmente chamar um loader +por cima do estado atual. Ele precisa orquestrar a substituicao do app corrente: +fechar ou descartar o Game residente, limpar estado cartridge-scoped, resolver o +novo alvo, carregar o proximo cartucho e entrar no novo estado de firmware. + +Essa agenda tambem se relaciona com a `AGD-0003 / system-run-cart`: `run_cart`, +se existir, nao e userland e nao e o orquestrador. O carregador de cartucho e +apenas uma etapa dentro de uma transicao comandada pelo SystemOS. + +## Problema + +Sem um orquestrador explicito de troca de cartucho no SystemOS, a plataforma +pode cair em estados incoerentes: + +- carregar um novo Game sem fechar corretamente o Game anterior; +- reaproveitar estado VM, assets, input, audio, render ou telemetria do cartucho + anterior; +- misturar boot direto por linha de comando com troca de app sob o SO; +- deixar o loader decidir lifecycle, quando ele deveria apenas materializar o + cartucho alvo; +- tentar implementar `Home -> outro Game` antes de existir o contrato de + foreground/pausa definido pela `AGD-0041`. + +## Pontos Criticos + +- Esta agenda deve ser executada depois da `AGD-0041`. +- `Game -> Home -> mesmo Game` fica fora desta agenda. +- O SystemOS deve ser a autoridade da troca entre cartuchos sob o SO. +- O loader nao deve possuir politica de fechamento, pausa, foreground ou + cleanup. +- Boot direto por CLI/debug continua sendo caminho separado e mais simples. +- A troca para outro Game deve tratar falha de load sem deixar o sistema em + estado parcialmente substituido. +- `.pmc`, catalogo completo e UX final de launcher continuam fora de escopo, + salvo se forem necessarios para definir o contrato minimo. + +## Opcoes + +### Opcao A - Adiar tudo para depois da AGD-0041 + +- **Abordagem:** manter esta agenda como registro de escopo futuro; primeiro + fechar e implementar o contrato `Game -> Home -> mesmo Game`. +- **Pro:** evita misturar pausa/resume com substituicao de cartucho; reduz risco + de reabrir arquitetura durante a `AGD-0041`. +- **Contra:** a plataforma continua sem troca `Home -> outro Game` ate uma etapa + posterior. +- **Manutenibilidade:** alta, porque cada agenda fecha um eixo de lifecycle. + +### Opcao B - Definir agora o contrato conceitual, implementar depois + +- **Abordagem:** fechar uma decisao curta ja dizendo que o orquestrador pertence + ao SystemOS, que `run_cart` nao e userland, e que a execucao depende da + `AGD-0041`; o plano de codigo fica para depois. +- **Pro:** reduz ambiguidade enquanto trabalhamos na `AGD-0041`. +- **Contra:** pode produzir uma decisao com pouca evidencia operacional antes + do contrato de foreground estar fechado. +- **Manutenibilidade:** boa se a decisao for claramente marcada como dependente + da `AGD-0041`. + +### Opcao C - Implementar o orquestrador completo agora + +- **Abordagem:** tentar resolver `Game A -> Home -> Game B` antes ou junto da + `AGD-0041`. +- **Pro:** ataca o fluxo final diretamente. +- **Contra:** mistura pausa/resume, fechamento, cleanup, resolucao de alvo e + falha de load em uma unica frente; alto risco de contrato instavel. +- **Manutenibilidade:** baixa para v1, porque a troca completa depende do + contrato de foreground ainda aberto. + +## Sugestao / Recomendacao + +Recomendo a Opcao A agora, com esta agenda aberta como marcador explicito de +escopo futuro. + +A ordem preferida passa a ser: + +1. fechar a `AGD-0041` para definir foreground, pausa/suspensao, Shell no + foreground e retorno ao mesmo Game; +2. voltar para esta agenda e decidir o contrato do orquestrador de troca de + cartucho no SystemOS; +3. so entao planejar a execucao de `Game A -> Home -> Game B`. + +Se durante a `AGD-0041` aparecer uma decisao que exige antecipar parte do +orquestrador, esta agenda deve ser reaberta/atualizada com a dependencia +concreta em vez de esconder a troca de cartucho dentro da agenda de foreground. + +## Perguntas em Aberto + +- [ ] Depois da `AGD-0041`, qual estado representa um Game que sera descartado + em vez de retomado? +- [ ] O fechamento do Game atual e observavel para o firmware/game, ou e uma + destruicao interna do SystemOS? +- [ ] Quais estados cartridge-scoped devem ser limpos antes do novo load: VM, + assets, FS handles, input, audio, render ownership, telemetry, logs? +- [ ] Se o novo cartucho falha ao carregar, o sistema volta ao Home, mostra uma + tela de erro, ou tenta preservar o Game anterior? +- [ ] Como a resolucao do novo alvo e representada sem depender de catalogo + completo ou `.pmc`? +- [ ] Qual evidencia minima prova `Game A -> Home -> Game B` sem depender de UX + final de launcher? + +## Criterio para Encerrar + +A agenda pode virar decisao quando: + +- a `AGD-0041` tiver fechado o contrato de foreground/pausa/resume; +- estiver claro que o orquestrador pertence ao SystemOS; +- `run_cart`/loader estiver limitado a etapa interna, nao userland; +- existir regra para fechar/descartar o Game atual; +- existir contrato de limpeza de estado cartridge-scoped; +- existir politica para falha de carregamento do novo Game; +- houver criterio de teste minimo para `Game A -> Home -> Game B`. + +## Discussao + +- 2026-07-03: Agenda criada a partir da separacao entre `AGD-0041` + (`Game -> Home -> mesmo Game`) e `AGD-0003` (`run_cart`/loader nao userland). + O novo escopo cobre a troca posterior `Game A -> Home -> Game B`, com + orquestracao pelo SystemOS depois que o contrato de foreground estiver + fechado. + +## Resolution + +Ainda em aberto. diff --git a/discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md b/discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md new file mode 100644 index 00000000..3f1980f7 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md @@ -0,0 +1,161 @@ +--- +id: AGD-0045 +ticket: system-run-cart +title: SystemOS Library Root and Home Game Launch +status: accepted +created: 2026-07-03 +resolved: +decision: +tags: [runtime, os, hub, cartridge, launcher, library, game, architecture] +--- + +## Contexto + +A `DEC-0035` fechou que `system.run_cart` nao deve existir como syscall +userland e que boot direto por host/CLI/debug deve permanecer. + +Isso resolveu a autoridade negativa, mas ainda nao resolveu a experiencia +desejada para o SO: apontar o runtime para um diretorio raiz de biblioteca, +fazer a Home listar os cartuchos encontrados ali e permitir que o usuario clique +em um jogo para carrega-lo. + +O objetivo pratico e um "Switch feio": uma lista simples na Home, sem UX final, +sem marketplace e sem catalogo rico, mas capaz de provar que o SO, e nao o +guest, escolhe e carrega um cartucho a partir de uma biblioteca local. + +## Problema + +Hoje temos dois caminhos separados: + +- boot direto por `--run ` funciona e carrega um cartucho unico; +- Hub/Home existe, mas mostra acoes fixas de Shell nativo e nao lista jogos de + uma biblioteca real. + +Sem uma biblioteca de cartuchos visivel para o SO, o usuario nao consegue +iniciar jogos a partir da Home. Isso deixa a agenda `system-run-cart` incompleta +do ponto de vista de produto: removemos o `run_cart` userland, mas ainda falta a +alternativa correta, que e o SO iniciar cartuchos por acao de usuario. + +## Pontos Criticos + +- O root da biblioteca deve ser fornecido pelo host como `--games-root `. +- O scan inicial deve cobrir subdiretorios imediatos com `manifest.json`. +- O loader de diretorio existente deve continuar sendo a autoridade para validar + cartucho completo; a biblioteca pode ler metadados minimos para listagem. +- A Home pode mostrar uma lista feia/clicavel; polimento visual fica fora. +- O clique em um Game deve iniciar `LoadCartridge` pelo firmware/SystemOS, nao + por syscall guest. +- V1 nao precisa tratar retorno de um Game para Home, porque isso pertence a + `AGD-0041`. +- `.pmc`, outros tipos de app, catalogo remoto, marketplace, icones, busca e + metadados ricos ficam fora. + +## Opcoes + +### Opcao A - Biblioteca apenas para listagem + +- **Abordagem:** adicionar `--games-root`, escanear manifestos e mostrar jogos + na Home, mas sem permitir launch ainda. +- **Pro:** baixa complexidade; prova descoberta/catalogo local. +- **Contra:** nao entrega o objetivo principal de carregar jogo pela Home. +- **Manutenibilidade:** boa, mas valor funcional limitado. + +### Opcao B - Home lista biblioteca e carrega Game quando nao ha Game ativo + +- **Abordagem:** adicionar `--games-root`, escanear cartuchos locais, renderizar + lista simples na Home e permitir clique para carregar um Game via + `LoadCartridge`. A primeira versao cobre Home inicial/sem Game residente. +- **Pro:** entrega o fluxo desejado sem abrir troca completa entre jogos. +- **Contra:** exige passar biblioteca/acao selecionada entre host, Hub, + firmware e loader. +- **Manutenibilidade:** boa se a fronteira ficar clara: biblioteca descobre, + Hub escolhe, firmware carrega. + +### Opcao C - Biblioteca mais troca completa entre jogos + +- **Abordagem:** Home lista biblioteca e tambem permite `Game A -> Home -> + Game B` ja no primeiro passo. +- **Pro:** cobre a experiencia final. +- **Contra:** mistura biblioteca com pausa, fechamento, cleanup e falha de load; + depende de `AGD-0041` e `AGD-0044`. +- **Manutenibilidade:** baixa para v1. + +## Sugestao / Recomendacao + +Recomendo a Opcao B. + +O v1 deve permitir: + +- host recebe `--games-root `; +- o SO/Hub recebe uma lista de cartuchos descobertos; +- a Home mostra uma lista simples/clicavel de jogos; +- clicar em um Game carrega o cartucho via firmware `LoadCartridge`; +- boot direto por `--run ` continua separado e intacto; +- nenhum guest/app chama `run_cart`; +- o escopo v1 acontece na Home inicial; retorno de Game para Home fica para + `AGD-0041`, e troca `Game A -> Home -> Game B` fica para `AGD-0044`. + +Detalhes aceitos para v1: + +- scan apenas de subdiretorios imediatos; +- somente cartuchos `app_mode: Game`; +- `.pmc` fica para futuro; +- cartuchos invalidos sao logados e omitidos da lista; +- o modelo interno deve ser uma entry de biblioteca expansivel, nao apenas um + summary de UI. A entry pode manter o manifest carregado em memoria, path + interno necessario para launch e metadados operacionais como instante de + descoberta/instalacao. A UI v1 mostra apenas `title`, `app_id` e + `app_version`, mas futuros metadados ingeridos pela biblioteca poderao ser + usados pelo catalogo e pela propria UI; +- clique carrega imediatamente, sem confirmacao; +- falha de load volta para Home e registra log/erro. + +Essa agenda deve produzir a alternativa correta ao `run_cart` userland: uma +acao de sistema, iniciada pela Home, que carrega cartucho escolhido pelo usuario +a partir de uma biblioteca local. + +## Perguntas em Aberto + +- [x] O argumento deve se chamar `--games-root`. +- [x] A biblioteca deve escanear apenas subdiretorios imediatos. +- [x] A listagem v1 deve incluir apenas `app_mode: Game`. +- [x] O scan deve logar cartuchos invalidos e omiti-los da lista. +- [x] O Hub/SystemOS deve receber uma entry de biblioteca expansivel, capaz de + manter o manifest carregado, path interno para launch e metadados + operacionais como timestamp de descoberta/instalacao. A UI v1 so precisa + mostrar `title`, `app_id` e `app_version`. +- [x] O clique deve carregar imediatamente, sem confirmacao. +- [x] Se um cartucho falha ao carregar, o sistema volta para Home e registra + erro/log. +- [x] Retorno de Game para Home nao pertence a esta agenda; fica para + `AGD-0041`. +- [x] O teste minimo deve provar o fluxo `games-root -> Home list -> click -> + LoadCartridge -> GameRunning`? + +## Criterio para Encerrar + +A agenda pode virar decisao quando estiver claro: + +- nome e semantica do argumento de host para root da biblioteca; +- escopo do scan e metadados minimos; +- fronteira entre biblioteca, Hub, firmware e loader; +- politica v1 para cartuchos invalidos; +- regra v1 para launch quando nao ha Game ativo; +- fronteira explicita com `AGD-0041` e `AGD-0044`; +- testes minimos para discovery e launch pela Home. + +## Discussao + +- 2026-07-03: Agenda criada como continuacao da `DSC-0004` apos a `DEC-0035`. + A decisao anterior removeu `run_cart` userland; esta agenda define o caminho + correto para o SO/Home carregar jogos a partir de uma biblioteca local. +- 2026-07-03: Respostas aceitas: usar `--games-root`, escanear apenas + subdiretorios imediatos, listar somente Games, deixar `.pmc` e outros apps + para depois, logar/omitir invalidos, usar summary com `title`, `app_id` e + `app_version`, carregar imediatamente no clique, e deixar retorno para Home + sob responsabilidade da `AGD-0041`. +- 2026-07-03: Modelo de dados refinado: a biblioteca deve produzir entries + expansíveis, nao somente summaries de UI. Cada entry pode manter manifest em + memoria, path interno de launch e metadados operacionais como timestamp de + descoberta/instalacao; a UI v1 escolhe quais campos mostrar, mas o modelo + deve permitir que metadados futuros sejam ingeridos e usados pelo catalogo/UI. diff --git a/discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md b/discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md new file mode 100644 index 00000000..5e212b26 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md @@ -0,0 +1,136 @@ +--- +id: DEC-0035 +ticket: system-run-cart +title: Remove Userland system.run_cart and Preserve Direct Boot +status: accepted +created: 2026-07-03 +ref_agenda: AGD-0003 +tags: [] +--- + +## Status + +Accepted. + +## Contexto + +`system.run_cart` existe na ABI publica, mas nao possui comportamento real de +troca de cartucho. Isso cria uma falsa promessa: um guest pode declarar/chamar +uma operacao que aparenta iniciar outro cartucho, enquanto o runtime retorna +sucesso vazio ou nao executa uma transicao completa. + +A discussao fechou tres separacoes: + +- boot direto por host/CLI/debug e necessario e deve permanecer; +- apps/userland nao devem iniciar cartuchos diretamente; +- troca `Game A -> Home -> Game B` exige um orquestrador de SystemOS proprio, + separado em `DSC-0043 / AGD-0044`. + +`AGD-0041` permanece responsavel por `Game -> Home -> mesmo Game`, foreground, +pausa/suspensao e retorno ao mesmo Game. + +## Decisao + +`system.run_cart` MUST be removed from the userland guest ABI. + +No app, cartridge, Shell app, or other userland guest code MAY directly request +cartridge boot through `system.run_cart`. + +The platform MUST NOT preserve compatibility for the current userland +`system.run_cart` surface. It MUST NOT remain as a callable stub, reserved +guest syscall that returns success, or capability-gated userland escape hatch. + +Direct boot MUST remain supported through host/CLI/debug or equivalent +single-game launch paths. This direct boot path is a host/system entrypoint, +not a guest syscall. + +Internal cartridge loading MAY continue to use the existing loader path, +including directory cartridges and test fixtures. `.pmc` support is not required +by this decision. + +If apps ever need to request navigation in the future, that MUST be designed as +a separate SystemOS-mediated surface with different naming and semantics. It +MUST NOT reintroduce userland `run_cart` as direct cartridge boot. + +## Rationale + +Direct cartridge boot is essential for development, debugging, automated tests, +and future distribution modes where the runtime launches as a single game. That +use case does not require a guest-visible syscall. + +Userland cartridge boot would give app code authority over system navigation, +lifecycle, cleanup, target resolution, and failure policy. Those concerns belong +to SystemOS/firmware and later to the cartridge switch orchestrator, not to a VM +guest syscall. + +Keeping a stub or compatibility surface would preserve the current ambiguity. +The public ABI should not advertise an operation that either does nothing or +looks like direct app-controlled navigation. + +## Invariantes / Contrato + +- Guest/userland MUST NOT expose or call `system.run_cart`. +- The public syscall registry, declarations, generated bindings, and docs MUST + stop presenting `system.run_cart` as an app-callable operation. +- Booting a cartridge directly MUST remain available from host/CLI/debug flows. +- Direct boot MUST enter the runtime with a selected cartridge target; it MUST + NOT depend on a guest syscall issued after VM startup. +- Loader code MAY remain reusable by host/firmware/SystemOS internals. +- Loader code MUST NOT become lifecycle orchestration authority. +- `Game -> Home -> same Game` remains owned by `AGD-0041`. +- `Game A -> Home -> Game B` remains owned by `DSC-0043 / AGD-0044`. +- `.pmc`, app catalog, launcher UX, marketplace/distribution, and full cartridge + replacement under Home are outside this decision. + +## Impactos + +- **Spec / ABI:** remove or retire `system.run_cart` from the userland syscall + surface. Any syscall table, PBS declarations, generated metadata, or docs + must stop advertising it to apps. +- **Runtime / VM:** guest dispatch must no longer treat `system.run_cart` as a + valid userland operation. If obsolete bytecode or declarations still reach the + runtime during migration, they must fail explicitly rather than report success. +- **Firmware / Host:** direct boot remains supported for command-line, + debugger, test harness, and future single-game launch flows. +- **SystemOS:** no new cart-switch orchestrator is required by this decision. + That work is deferred to `DSC-0043 / AGD-0044`. +- **Tooling:** tools must continue to support direct cartridge launch for + development and test workflows, without relying on a guest syscall. +- **Tests:** coverage must prove both sides: userland cannot declare/call + `system.run_cart`, and direct boot of a cartridge still works. + +## Alternativas Descartadas + +- **Implement `system.run_cart` as a normal syscall:** rejected because it gives + userland direct navigation authority and mixes guest execution with system + lifecycle. +- **Keep a compatibility stub:** rejected because no compatibility is required + and a stub preserves the false contract. +- **Make `system.run_cart` privileged but still guest-visible:** rejected for v1 + because it keeps the ambiguous idea that some guest profile may directly boot + cartuchos. +- **Implement full `Home -> another Game` now:** rejected for this decision; it + requires SystemOS orchestration after the `AGD-0041` foreground contract. + +## Referencias + +- Agenda: `AGD-0003` +- Foreground/pause scope: `AGD-0041` +- Future cartridge switch orchestrator: `DSC-0043 / AGD-0044` +- Related lessons: `LSN-0041` SystemOS lifecycle authority, `LSN-0042` + SystemOS service ownership boundary, `LSN-0043` SystemOS domain facades. + +## Propagacao Necessaria + +- Remove `SystemRunCart` from userland ABI declarations and generated syscall + metadata. +- Remove or rewrite runtime dispatch paths that return success for + `system.run_cart`. +- Preserve host/CLI/debug direct boot paths and add regression tests for them. +- Add negative tests for guest/userland declaration or call attempts. +- Update any documentation/spec text that describes `system.run_cart` as + app-callable. + +## Revision Log + +- 2026-07-03: Initial draft from `AGD-0003`. diff --git a/discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md b/discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md new file mode 100644 index 00000000..ed7eda2c --- /dev/null +++ b/discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md @@ -0,0 +1,156 @@ +--- +id: DEC-0036 +ticket: system-run-cart +title: SystemOS Games Root and Home Game Launch +status: accepted +created: 2026-07-03 +ref_agenda: AGD-0045 +tags: [] +--- + +## Status + +Accepted. + +## Contexto + +`DEC-0035` removed `system.run_cart` from the userland guest ABI and preserved +direct host/CLI/debug cartridge boot. That decision intentionally did not +deliver the Home/SO product flow: pointing PROMETEU at a local games directory, +listing available games in Home, and launching a selected game by user action. + +This decision defines the first system-owned replacement for userland +`run_cart`: a local games-root library discovered by the host/SystemOS and +exposed through a simple Home UI. + +## Decisao + +PROMETEU MUST support a host-provided games library root through +`--games-root `. + +The games root is a directory containing game cartridge directories as immediate +children. V1 MUST scan only immediate child directories. Recursive discovery is +out of scope. + +The v1 games library MUST include only cartridges whose manifest declares +`app_mode: Game`. Shell/System apps, `.pmc` packages, remote catalogs, +marketplace/distribution metadata, rich icons, search, and non-game apps are out +of scope. + +The library MUST produce expandable internal entries, not UI-only summaries. +Each entry MUST retain enough internal data to launch the cartridge and to grow +as a catalog model. At minimum, each entry MUST include: + +- loaded manifest data; +- `title`; +- `app_id`; +- `app_version`; +- internal cartridge path; +- operational discovery metadata, including a timestamp or equivalent discovery + marker. + +The Home UI v1 SHOULD display only `title`, `app_id`, and `app_version`. It MAY +hide path and operational metadata. + +Invalid cartridge candidates MUST be logged and omitted from the Home game list. + +Selecting a listed Game in Home MUST immediately request a system-owned launch +through firmware/SystemOS, resulting in `LoadCartridge` for that cartridge. This +MUST NOT use a guest syscall and MUST NOT reintroduce userland `run_cart`. + +If launch fails, the machine MUST return to or remain in Home and record an +error/log. A polished error UI is out of scope. + +Booting directly with `--run ` MUST remain separate and unchanged. + +Returning from a running Game to Home is outside this decision and belongs to +`AGD-0041`. Switching from `Game A -> Home -> Game B` after a game is already +active belongs to `DSC-0043 / AGD-0044`. + +## Rationale + +The previous `run_cart` surface was wrong because it made cartridge launch look +like an app/userland capability. The desired behavior is still valid, but the +authority must live in the system: Home presents available games and the +firmware/SystemOS loads the selected cartridge. + +Using `--games-root` keeps the v1 product model honest: this is a local game +library, not a general app root. Other app types can be introduced later without +forcing the first launcher model to pretend it is a full marketplace. + +Immediate child scanning keeps discovery deterministic and testable. `.pmc` and +recursive layouts can be added after the directory-cartridge path is proven. + +An expandable entry model avoids prematurely freezing the UI shape as the data +model. The UI is intentionally simple, but the catalog model needs room for +future metadata such as package origin, installation/discovery timestamps, rich +presentation fields, and packaged cartridge details. + +## Invariantes / Contrato + +- `--games-root ` is the v1 host argument for a local games library. +- The games root is Game-only in v1. +- Discovery scans immediate child directories only. +- Directory candidates without valid Game cartridge metadata are logged and + omitted. +- Library entries are internal catalog records, not direct UI DTOs. +- The UI v1 displays a simple clickable list and may show only `title`, + `app_id`, and `app_version`. +- The internal entry retains cartridge path and loaded manifest data for launch. +- Home selection is a system action that enters firmware/SystemOS loading. It is + not a guest syscall. +- `--run ` direct boot remains independent from `--games-root`. +- Return-to-Home and game-to-game switching are not implemented by this + decision. + +## Impactos + +- **Host:** add a `--games-root ` argument and pass the resolved root into + runtime/firmware/SystemOS initialization. +- **SystemOS / Hub:** add a local game library model and expose entries to Home. + Home must render a minimal clickable list and emit a system-owned launch + action for a selected entry. +- **Firmware:** consume the Home launch action and transition to + `LoadCartridge` with the selected cartridge path. +- **Loader:** continue to validate complete directory cartridges. Discovery may + read manifest metadata, but full launch still uses the existing loader. +- **Runtime / VM:** no userland ABI is added. No guest syscall participates in + launch. +- **Specs / Docs:** document `--games-root`, Game-only v1 discovery, and the + distinction between direct boot, games library launch, and future game + switching. +- **Tests:** cover discovery, invalid omission/logging, Home action emission, + and the end-to-end path from games root selection to `GameRunning`. + +## Alternativas Descartadas + +- **List only, no launch:** rejected because it does not satisfy the product + goal of starting games from Home. +- **General app root:** rejected for v1 because Shell/System apps need separate + lifecycle and UI contracts. +- **Recursive discovery:** rejected for v1 to keep semantics deterministic. +- **`.pmc` discovery:** rejected for v1 because packed cartridge loading is a + separate open agenda. +- **Game-to-game switch now:** rejected because it depends on foreground and + cartridge-switch orchestration work in `AGD-0041` and `AGD-0044`. + +## Referencias + +- Source agenda: `AGD-0045` +- Userland run-cart removal: `DEC-0035` +- Foreground/Home return scope: `AGD-0041` +- Future cartridge switch orchestration: `DSC-0043 / AGD-0044` + +## Propagacao Necessaria + +- Add or update runtime specs for games-root discovery and Home launch. +- Add host CLI argument plumbing for `--games-root`. +- Add a catalog/library entry model. +- Add Hub/Home list rendering and click handling. +- Add firmware/SystemOS action routing to `LoadCartridge`. +- Add tests for discovery, invalid candidates, action emission, and + `games-root -> Home click -> GameRunning`. + +## Revision Log + +- 2026-07-03: Initial draft from `AGD-0045`. diff --git a/discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md b/discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md new file mode 100644 index 00000000..52c071ae --- /dev/null +++ b/discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md @@ -0,0 +1,90 @@ +--- +id: PLN-0129 +ticket: system-run-cart +title: Remove Userland Run Cart ABI Surface +status: done +created: 2026-07-03 +ref_decisions: [DEC-0035] +tags: [] +--- + +## Briefing + +`DEC-0035` removes `system.run_cart` from the userland guest ABI. This plan +covers the public ABI, syscall registry/declaration surface, generated metadata, +and documentation/spec propagation. It does not change runtime dispatch behavior +or direct boot tests; those are covered by `PLN-0130` and `PLN-0131`. + +## Objective + +Remove `system.run_cart` from every app-callable/userland ABI surface while +preserving non-guest direct boot entrypoints. + +## Dependencies + +- Source decision: `DEC-0035`. +- Must stay scoped away from `AGD-0041` and `AGD-0044`. +- Coordinate with `PLN-0130` so runtime dispatch does not retain a callable + success path after the declaration is removed. + +## Scope + +- Remove or retire `Syscall::SystemRunCart` from userland-visible syscall + declarations in `crates/console/prometeu-hal/src/syscalls.rs`. +- Remove the `system.run_cart` registry entry from + `crates/console/prometeu-hal/src/syscalls/domains/system.rs` and + `crates/console/prometeu-hal/src/syscalls/registry.rs`, unless implementation + discovers an internal-only registry distinct from guest ABI. +- Remove generated/static bytecode or VM-facing declarations that expose + `system.run_cart`, including known references in + `crates/console/prometeu-bytecode/src/model.rs` and + `crates/console/prometeu-vm/src/virtual_machine.rs` if they include it. +- Update docs/specs that mention `system.run_cart` as app-callable. +- Update `AGD-0004` references only if they become factually misleading after + removal; do not close or reinterpret that agenda. + +## Non-Goals + +- Do not implement `Home -> another Game`. +- Do not add a replacement app navigation syscall. +- Do not implement `.pmc`. +- Do not change direct host/CLI/debug boot behavior. +- Do not add compatibility stubs for obsolete guest code. + +## Execution Method + +1. Search for `SystemRunCart`, `run_cart`, and `"system", "run_cart"` across + `crates/`, `docs/`, and `discussion/`. +2. Remove `system.run_cart` from userland syscall domain declarations and + lookup/name registries. +3. Remove generated/static guest-facing metadata entries that advertise + `system.run_cart`. +4. Update docs/specs to state that cartridges are booted by host/system paths, + not by guest syscall. +5. Compile the workspace or targeted crates to find stale references. + +## Acceptance Criteria + +- No userland ABI declaration advertises `system.run_cart`. +- No generated guest metadata exposes `run_cart` as callable. +- Documentation/spec text no longer describes app-callable cartridge boot. +- Any retained code path is clearly internal-only and is not reachable through + guest syscall lookup. + +## Tests + +- Add or update syscall registry tests to assert `system.run_cart` is absent + from the public/userland registry. +- Run targeted tests for `prometeu-hal`, `prometeu-bytecode`, and + `prometeu-vm` if available. +- Run repository validation commands used for syscall declaration consistency. + +## Affected Artifacts + +- `crates/console/prometeu-hal/src/syscalls.rs` +- `crates/console/prometeu-hal/src/syscalls/domains/system.rs` +- `crates/console/prometeu-hal/src/syscalls/registry.rs` +- `crates/console/prometeu-bytecode/src/model.rs` +- `crates/console/prometeu-vm/src/virtual_machine.rs` +- `docs/specs/runtime/` +- `discussion/workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md` diff --git a/discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md b/discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md new file mode 100644 index 00000000..71be6a70 --- /dev/null +++ b/discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md @@ -0,0 +1,79 @@ +--- +id: PLN-0130 +ticket: system-run-cart +title: Remove Runtime Run Cart Stub Dispatch +status: done +created: 2026-07-03 +ref_decisions: [DEC-0035] +tags: [] +--- + +## Briefing + +`DEC-0035` forbids a callable userland `system.run_cart` stub. This plan covers +runtime and VM dispatch cleanup so obsolete calls cannot silently succeed. +Public ABI removal is handled by `PLN-0129`; direct boot preservation is handled +by `PLN-0131`. + +## Objective + +Remove the runtime no-op dispatch path for `SystemRunCart` and ensure stale or +obsolete userland attempts fail explicitly rather than returning success. + +## Dependencies + +- Source decision: `DEC-0035`. +- Depends on the ABI direction in `PLN-0129`, but can be implemented in the same + PR if the public enum/registry removal and dispatch cleanup move together. + +## Scope + +- Remove the `Syscall::SystemRunCart => return Ok(())` path from + `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. +- Update any match arms made unreachable or obsolete by removing + `SystemRunCart`. +- Ensure stale numeric syscall ids or stale bytecode declarations fail with the + repository's existing unknown/invalid syscall behavior. +- Add negative coverage for userland attempts to invoke the removed operation, + using the closest existing VM runtime syscall tests. + +## Non-Goals + +- Do not implement a replacement navigation operation. +- Do not implement the `Home -> another Game` orchestrator. +- Do not alter direct host/CLI/debug cartridge boot paths. +- Do not change `system.has_cart` behavior unless required by compile fallout. + +## Execution Method + +1. Inspect `dispatch.rs` for all `SystemRunCart` match arms. +2. Remove the no-op success path and any unreachable arms tied only to + `SystemRunCart`. +3. If the public enum variant is removed by `PLN-0129`, update exhaustiveness + and registry mapping accordingly. +4. Add or update VM runtime tests that demonstrate `system.run_cart` is not + callable from guest/userland. +5. Run targeted runtime tests and compile checks. + +## Acceptance Criteria + +- There is no runtime path where a guest `system.run_cart` call returns `Ok(())`. +- Stale/obsolete attempts fail explicitly through existing invalid syscall or + declaration validation behavior. +- `system.has_cart` and unrelated system syscalls keep their current behavior. +- Runtime dispatch remains exhaustive and clear after removing `SystemRunCart`. + +## Tests + +- Add a negative test in `crates/console/prometeu-system/src/services/vm_runtime` + for stale userland `run_cart` attempts, if the test harness can express it. +- Run `cargo test` for `prometeu-system` and any impacted syscall crates. +- Run a workspace compile/test command if targeted tests reveal cross-crate enum + fallout. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs` +- `crates/console/prometeu-system/src/services/vm_runtime/tests.rs` +- `crates/console/prometeu-hal/src/syscalls.rs` +- `crates/console/prometeu-hal/src/syscalls/registry.rs` diff --git a/discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md b/discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md new file mode 100644 index 00000000..75660a8c --- /dev/null +++ b/discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md @@ -0,0 +1,83 @@ +--- +id: PLN-0131 +ticket: system-run-cart +title: Preserve Direct Cartridge Boot Coverage +status: done +created: 2026-07-03 +ref_decisions: [DEC-0035] +tags: [] +--- + +## Briefing + +`DEC-0035` removes userland `system.run_cart`, but direct cartridge boot remains +mandatory for development, debugging, automated tests, and future single-game +launch scenarios. This plan protects that path while the guest syscall is +removed. + +## Objective + +Preserve and test host/CLI/debug direct cartridge boot without relying on a +guest syscall. + +## Dependencies + +- Source decision: `DEC-0035`. +- Can be implemented after or alongside `PLN-0129` and `PLN-0130`. +- Must not depend on `.pmc`, app catalog, launcher UX, `AGD-0041`, or + `AGD-0044`. + +## Scope + +- Identify direct boot entrypoints in `crates/tools/prometeu-cli/src/main.rs`, + `crates/host/prometeu-host-desktop-winit/src/lib.rs`, + `crates/host/prometeu-host-desktop-winit/src/debugger.rs`, and firmware + cartridge loading code. +- Add or preserve tests showing a cartridge can be loaded directly into + firmware/runtime from host or CLI paths. +- Ensure these paths do not call or require `system.run_cart`. +- Document the distinction between direct boot and guest navigation where + runtime specs describe boot behavior. + +## Non-Goals + +- Do not implement `Home -> another Game`. +- Do not add app-callable navigation. +- Do not require `.pmc`. +- Do not redesign cartridge loader internals beyond what is required to keep + direct boot working. + +## Execution Method + +1. Trace the CLI/debug direct boot path from cartridge path argument through + loader, firmware `load_cartridge`, and runtime initialization. +2. Add regression tests at the lowest practical layer proving direct boot still + reaches `GameRunning` for a valid game cartridge. +3. Add a guard or assertion that direct boot does not depend on `system.run_cart` + metadata. +4. Update docs/specs to describe direct boot as host/system-controlled. +5. Run targeted CLI/firmware/host tests. + +## Acceptance Criteria + +- A valid game cartridge can still boot directly from host/CLI/debug paths. +- Direct boot does not depend on `system.run_cart` being in the guest syscall + registry. +- Existing debugger/handshake metadata that reports cartridge info still works. +- The direct boot behavior is documented as host/system entry, not guest ABI. + +## Tests + +- Add or preserve firmware tests around `load_cartridge` and `GameRunning`. +- Add or preserve host/CLI tests for cartridge path boot when practical. +- Run `cargo test` for `prometeu-firmware`, `prometeu-host-desktop-winit`, and + `prometeu-cli` where available. + +## Affected Artifacts + +- `crates/tools/prometeu-cli/src/main.rs` +- `crates/host/prometeu-host-desktop-winit/src/lib.rs` +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` +- `docs/specs/runtime/` diff --git a/discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md b/discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md new file mode 100644 index 00000000..7bd714c1 --- /dev/null +++ b/discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md @@ -0,0 +1,103 @@ +--- +id: PLN-0132 +ticket: system-run-cart +title: Specify Games Root Home Launch Contract +status: done +created: 2026-07-03 +ref_decisions: [DEC-0036] +tags: [] +--- + +## Briefing + +`DEC-0036` establishes a new system-owned game library launch path: +the host receives `--games-root `, SystemOS/Home lists valid Game +directory cartridges from that root, and selecting one starts the cartridge +through firmware loading rather than a guest syscall. + +This plan updates the normative specs before code is changed. + +## Objective + +Document the v1 games-root and Home launch contract in the canonical runtime +specs so implementation plans cannot drift back into userland `run_cart` or +general app-library semantics. + +## Dependencies + +- Source decision: `DEC-0036`. +- Prior decision: `DEC-0035`, which removed userland `system.run_cart` and + preserved direct `--run` boot. +- This plan must be completed before `PLN-0133`, `PLN-0134`, and `PLN-0135`. + +## Scope + +- Update the runtime specs to define `--games-root ` as the v1 local games + library root. +- Specify that discovery scans only immediate child directories. +- Specify that v1 library discovery includes only valid directory cartridges + with `app_mode: Game`. +- Specify that `.pmc`, recursive discovery, Shell/System apps, marketplace + metadata, rich presentation, search, and non-game apps are out of scope. +- Specify the internal library entry contract: loaded manifest data, `title`, + `app_id`, `app_version`, internal cartridge path, and discovery metadata. +- Specify that Home v1 may display only `title`, `app_id`, and `app_version`. +- Specify that selecting a Home game emits a system-owned launch request that + transitions through firmware/SystemOS into `LoadCartridge`. +- Specify that invalid candidates are logged and omitted. +- Specify that launch failure remains or returns to Home and logs the failure. +- Reaffirm that direct `--run ` boot remains independent and unchanged. + +## Non-Goals + +- No Rust implementation. +- No Home UI changes. +- No `.pmc` discovery or packaged-cartridge spec changes. +- No recursive library layout. +- No return-to-Home behavior for a running Game. +- No Game A -> Home -> Game B orchestration. +- No guest syscall or userland ABI addition. + +## Execution Method + +1. Update [docs/specs/runtime/14-boot-profiles.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/14-boot-profiles.md) with: + - direct `--run` boot remaining a single-cartridge boot profile; + - `--games-root ` as a SystemOS/Home library profile; + - the distinction between direct boot, Home launch, and future game-switch + orchestration. +2. Update [docs/specs/runtime/12-firmware-pos-and-prometeuhub.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md) with: + - Home's responsibility to display the local games library; + - the system-owned launch action boundary; + - failure handling that leaves the machine in Home and records an error. +3. Update [docs/specs/runtime/13-cartridge.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/13-cartridge.md) with: + - directory-cartridge discovery requirements for library candidates; + - Game-only filtering by manifest `app_mode`; + - invalid candidate omission and diagnostics. +4. Update [docs/specs/runtime/16-host-abi-and-syscalls.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/16-host-abi-and-syscalls.md) only if needed to reinforce that no guest syscall owns cartridge launch. +5. Keep all spec text in English. + +## Acceptance Criteria + +- The specs define `--games-root ` and do not describe it as a generic app + root. +- The specs state that v1 scans immediate child directories only. +- The specs state that only valid Game directory cartridges enter the Home list. +- The specs state that Home launch is system-owned and enters `LoadCartridge`. +- The specs preserve `--run ` as a separate direct boot path. +- The specs explicitly exclude `.pmc`, recursive discovery, return-to-Home, and + game-switch orchestration from this implementation slice. +- No spec reintroduces userland `run_cart`. + +## Tests + +- Run `discussion validate`. +- Run targeted text checks for `--games-root`, `LoadCartridge`, and absence of + `system.run_cart` as an active public launch mechanism in edited specs. + +## Affected Artifacts + +- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` +- `docs/specs/runtime/13-cartridge.md` +- `docs/specs/runtime/14-boot-profiles.md` +- `docs/specs/runtime/16-host-abi-and-syscalls.md` +- `discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md` diff --git a/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md b/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md new file mode 100644 index 00000000..65bc21f2 --- /dev/null +++ b/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md @@ -0,0 +1,118 @@ +--- +id: PLN-0133 +ticket: system-run-cart +title: Implement Games Root Library Discovery +status: done +created: 2026-07-03 +ref_decisions: [DEC-0036] +tags: [] +--- + +## Briefing + +`DEC-0036` requires a host-provided games root and an expandable internal +library model before Home can render or launch games. This plan implements the +library discovery and host plumbing only; UI launch wiring is handled by +`PLN-0134`. + +## Objective + +Add a tested games-root discovery model that scans immediate child directory +cartridges, filters to valid Game manifests, records diagnostics for invalid +candidates, and carries enough metadata for Home rendering and later launch. + +## Dependencies + +- Source decision: `DEC-0036`. +- `PLN-0132` should be completed first so code follows the documented contract. +- Existing cartridge manifest and directory loader behavior in the console + crates. +- Existing host CLI and `HostRunner` boot setup. + +## Scope + +- Add `--games-root ` to the desktop host CLI. +- Pass the optional games root into the host runner / firmware initialization + path without changing direct `--run` or `--debug` boot behavior. +- Add a SystemOS-owned games library model for directory cartridge discovery. +- Scan only immediate child directories of the root. +- Read and retain manifest data for each valid candidate. +- Include only candidates whose manifest declares `app_mode: Game`. +- Retain internal cartridge path and operational discovery metadata on each + entry. +- Emit diagnostics for invalid, unreadable, or non-Game candidates and omit + them from the returned entries. +- Keep discovery independent from full cartridge launch: discovery reads + metadata; launch still uses the existing loader in `PLN-0134`. + +## Non-Goals + +- No Home list rendering. +- No click handling or `LoadCartridge` transition. +- No `.pmc` discovery. +- No recursive scan. +- No Shell/System app catalog. +- No marketplace metadata, icons, search, sorting UI, or polished errors. +- No new guest syscall. + +## Execution Method + +1. Introduce a library/discovery module in the SystemOS side, preferably under + `crates/console/prometeu-system/src/services/` unless the existing module + layout has a more local owner. +2. Define an internal entry type with at least: + - loaded cartridge manifest data; + - `title`; + - `app_id`; + - `app_version`; + - internal cartridge path; + - discovery metadata, using `SystemTime` or another explicit discovery + marker. +3. Define discovery diagnostics that can be logged by the caller and asserted in + tests. +4. Implement a scanner that: + - accepts an optional root path; + - returns an empty library when no root is configured; + - iterates only immediate child directories; + - parses `manifest.json` using structured manifest types, not ad hoc string + matching; + - filters to `app_mode: Game`; + - sorts entries deterministically for stable Home presentation and tests. +5. Add `--games-root ` to + [crates/host/prometeu-host-desktop-winit/src/lib.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/lib.rs) + and pass the path into + [crates/host/prometeu-host-desktop-winit/src/runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs). +6. Store or expose the discovered library to firmware/SystemOS in a way that + `PLN-0134` can consume without re-scanning inside the Home click path. +7. Add focused unit tests using temporary cartridge roots with: + - one valid Game directory cartridge; + - one valid non-Game cartridge; + - one invalid directory; + - one nested directory that must not be discovered. + +## Acceptance Criteria + +- `--games-root ` is accepted by the desktop host CLI. +- Absence of `--games-root` leaves existing Hub boot behavior unchanged. +- Direct `--run ` and `--debug ` behavior remains unchanged. +- Discovery returns only immediate valid Game directory cartridges. +- Each library entry retains manifest data, path, title, app id, app version, + and discovery metadata. +- Invalid candidates are represented in diagnostics and excluded from entries. +- Discovery does not fully load or boot cartridge programs. + +## Tests + +- Add and run focused games-library discovery tests in the owning console crate. +- Run `cargo test -p prometeu-system` if the library lives in + `prometeu-system`. +- Run `cargo test -p prometeu-host-desktop-winit` for CLI/runner plumbing. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/` +- `crates/console/prometeu-system/src/lib.rs` or module exports as needed +- `crates/host/prometeu-host-desktop-winit/src/lib.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md` diff --git a/discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md b/discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md new file mode 100644 index 00000000..001a2bf0 --- /dev/null +++ b/discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md @@ -0,0 +1,104 @@ +--- +id: PLN-0134 +ticket: system-run-cart +title: Wire Home Game List Launch Flow +status: done +created: 2026-07-03 +ref_decisions: [DEC-0036] +tags: [] +--- + +## Briefing + +After `PLN-0133`, SystemOS can discover a local Game library but Home still +does not present or launch those entries. This plan wires the discovered +library into Prometeu Hub/Home and routes a selected game to firmware +`LoadCartridge`. + +## Objective + +Make Home display a minimal clickable games list and launch the selected Game +through a system-owned firmware/SystemOS path, without reintroducing userland +`run_cart`. + +## Dependencies + +- Source decision: `DEC-0036`. +- `PLN-0132` for documented contract. +- `PLN-0133` for `--games-root` plumbing and the internal library entry model. +- Existing Prometeu Hub Home profile and firmware state transitions. + +## Scope + +- Provide the discovered games library to Prometeu Hub/Home. +- Render a minimal Home list showing `title`, `app_id`, and `app_version`. +- Add Home hit-testing/click handling for game entries. +- Extend the system profile action model with a system-owned game launch action. +- Handle that action in firmware/SystemOS by loading the selected cartridge via + the existing cartridge loader and entering `LoadCartridge`. +- On load failure, log/record the error and keep or return the machine to Home. +- Preserve existing native shell launch behavior. + +## Non-Goals + +- No polished launcher UI. +- No icons, screenshots, search, filtering, or marketplace metadata. +- No return-to-Home while a Game is running. +- No Game A -> Home -> Game B foreground orchestration. +- No `.pmc` launch support. +- No guest syscall. +- No direct-boot behavior changes. + +## Execution Method + +1. Extend the SystemOS/Hub action type currently represented by + `SystemProfileAction::LaunchNativeShell` with a game-launch action that + carries a stable reference to the selected library entry or cartridge path. +2. Update the Prometeu Hub state in + [crates/console/prometeu-system/src/programs/prometeu_hub.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/programs/prometeu_hub.rs) + or its local modules to accept the discovered game entries. +3. Render the Home games list with stable dimensions and simple clickable rows. + Rows must display only `title`, `app_id`, and `app_version` for v1. +4. Add hit-testing so selecting a row emits the game-launch action immediately. +5. Update + [crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs) + to consume the new action. +6. In the firmware action handler: + - call the existing cartridge loader for the selected path; + - on success, return `FirmwareState::LoadCartridge(...)`; + - on failure, log/record the error and remain in the Home path. +7. Ensure shell launch still works exactly as before. +8. Add unit or integration tests for: + - Home list presentation data; + - click-to-action emission; + - firmware action handling success; + - firmware action handling failure. + +## Acceptance Criteria + +- Starting the host with `--games-root ` exposes valid Game entries in + Home. +- The Home list displays `title`, `app_id`, and `app_version`. +- Clicking a listed Game routes through a system action and enters + `LoadCartridge`. +- The launch path does not use a guest syscall and does not mention + `system.run_cart`. +- Invalid or failing launch paths remain in Home and record an error. +- Existing native shell Home actions continue to work. +- Direct `--run` remains separate and unchanged. + +## Tests + +- Run `cargo test -p prometeu-system` for Hub action and presentation behavior. +- Run `cargo test -p prometeu-firmware` for firmware transition behavior. +- Run `cargo test -p prometeu-host-desktop-winit` if runner integration changes. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/programs/prometeu_hub.rs` +- `crates/console/prometeu-system/src/services/` +- `crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md` diff --git a/discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md b/discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md new file mode 100644 index 00000000..95721ff1 --- /dev/null +++ b/discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md @@ -0,0 +1,99 @@ +--- +id: PLN-0135 +ticket: system-run-cart +title: Validate Games Root Launch End To End +status: done +created: 2026-07-03 +ref_decisions: [DEC-0036] +tags: [] +--- + +## Briefing + +`PLN-0132` through `PLN-0134` specify, discover, display, and launch games +from a local games root. This plan closes the slice by adding end-to-end +validation evidence and manual smoke instructions for the product behavior the +user wants: point the SO at a directory, click a listed game, and boot it. + +## Objective + +Prove the complete `--games-root -> Home list -> click -> LoadCartridge -> +GameRunning` path while preserving direct `--run` boot and the absence of +userland `run_cart`. + +## Dependencies + +- Source decision: `DEC-0036`. +- `PLN-0132`, `PLN-0133`, and `PLN-0134` must be complete. +- Existing test cartridge fixtures, especially `test-cartridges/stress-console`. + +## Scope + +- Add automated integration coverage for the full games-root Home launch path + where the existing host/firmware test seams allow it. +- Add or document a minimal local games-root fixture using existing directory + cartridges. +- Validate invalid-candidate omission at the end-to-end boundary. +- Validate direct `--run` still works after games-root wiring. +- Capture manual smoke-test commands for release-mode functional verification. + +## Non-Goals + +- No new product UI polish. +- No screenshot automation unless the existing test harness already supports it + cleanly. +- No return-to-Home after a running Game. +- No Game A -> Home -> Game B orchestration. +- No `.pmc` validation. +- No broad performance tuning beyond confirming release-mode smoke behavior. + +## Execution Method + +1. Add an integration-style firmware or host test that constructs a games root + with a valid Game directory cartridge and boots to Hub/Home with that root. +2. Drive the Home selection path through the same action path used by user + input, not by calling the cartridge loader directly. +3. Assert that the machine reaches `LoadCartridge` or `GameRunning`, depending + on the most stable observable state available in the test harness. +4. Add a negative case with an invalid candidate and assert it is omitted or + reported as a diagnostic without appearing in Home. +5. Add a regression assertion that no public guest syscall named + `system.run_cart` is required by this path. +6. Re-run direct boot tests or add a small regression test proving + `--run test-cartridges/stress-console` remains independent from + `--games-root`. +7. Record manual smoke commands in the relevant spec or developer-facing doc: + - `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges` + - `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console` + +## Acceptance Criteria + +- Automated tests cover a valid games-root Home launch path. +- Automated tests cover invalid candidate omission or diagnostics at the + end-to-end boundary. +- Automated or documented validation proves direct `--run` still works. +- Manual smoke instructions use release mode for realistic interactive speed. +- Validation evidence shows no guest syscall participates in launch. +- `discussion validate` passes. + +## Tests + +- Run `cargo test -p prometeu-system`. +- Run `cargo test -p prometeu-firmware`. +- Run `cargo test -p prometeu-host-desktop-winit`. +- Run `cargo test -p prometeu-hal syscalls` to keep the removed syscall surface + guarded. +- Manually smoke test: + `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges`. +- Manually smoke test: + `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/` +- `crates/console/prometeu-firmware/` +- `crates/host/prometeu-host-desktop-winit/` +- `crates/console/prometeu-hal/src/syscalls/tests.rs` +- Runtime specs or developer docs containing smoke commands +- `discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md` diff --git a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md index a4b90b86..86a25bab 100644 --- a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md +++ b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md @@ -39,6 +39,10 @@ The VM does not own the machine lifecycle. Firmware does. `Hub`, `Task(TaskId)`, and `Overlay`. - **Shell task window**: a `WindowManager` window whose owner is `WindowOwner::Task(TaskId)`. +- **Games root**: a host-provided local directory used by Hub/Home to discover + Game directory cartridges. +- **Library entry**: an internal catalog record retained by Hub/SystemOS for a + discovered cartridge. ## 3 POS Responsibilities @@ -65,9 +69,20 @@ PrometeuHub is responsible for: - reading cartridge metadata needed for launch decisions; - deciding launch behavior based on `AppMode`; - managing the system window surface for system-mode apps. +- presenting the host-provided local games library when `--games-root ` is + configured; +- emitting system-owned launch actions for selected Home entries. The Hub does not execute bytecode directly. It always delegates execution setup to POS. +For v1, the local games library is Game-only. Hub/Home must not expose Shell or +System cartridges from the games root. The internal library entry must retain +the loaded manifest data, `title`, `app_id`, `app_version`, the cartridge path, +and discovery metadata such as a timestamp or equivalent discovery marker. + +The Home UI may display only `title`, `app_id`, and `app_version`. Path and +operational metadata may remain internal. + ## 5 App Modes ### Game @@ -154,6 +169,16 @@ If VM initialization fails, firmware transitions to the crash path. POS selects the cartridge and its execution context, but the cartridge's initial callable is not chosen by firmware metadata. Execution starts from the cartridge boot protocol defined in [`13-cartridge.md`](13-cartridge.md), currently `func_id = 0`. +When Home launches a discovered Game entry, the request is a system-owned action +from PrometeuHub to POS/firmware. Firmware must load the selected cartridge +through the normal cartridge loader and transition to `LoadCartridge` on +success. This path must not use a guest syscall and must not reintroduce +userland `run_cart`. + +If the selected cartridge cannot be loaded, firmware must leave or return the +machine in the Hub/Home flow and record the failure through diagnostics or +logging. A polished Home error surface is not required by v1. + ## 8 Firmware States The current firmware state model includes: diff --git a/docs/specs/runtime/13-cartridge.md b/docs/specs/runtime/13-cartridge.md index 4c921315..6f8c1158 100644 --- a/docs/specs/runtime/13-cartridge.md +++ b/docs/specs/runtime/13-cartridge.md @@ -175,10 +175,30 @@ Runtime loading policy: This is the working runtime/dev form used by the current loader. +Directory cartridges are also the only form discovered by the v1 local Home +games library. When a host provides `--games-root `, discovery scans only +immediate child directories under that root. Each child directory is a candidate +cartridge. + +For the v1 Home games library: + +- a candidate must have a valid `manifest.json`; +- the manifest must declare `app_mode: "Game"`; +- the library retains the loaded manifest data, `title`, `app_id`, + `app_version`, the internal cartridge path, and discovery metadata; +- invalid candidates are logged or reported as diagnostics and omitted from the + Home list; +- valid non-Game cartridges are omitted from the Game library. + +Discovery reads metadata for cataloging. It does not replace the normal +cartridge loader used when firmware starts the selected cartridge. + ### Packaged `.pmc` form This form is recognized conceptually by the loader boundary, but its actual load path is not implemented yet. +Packaged `.pmc` cartridges are not discovered by the v1 Home games library. + The cartridge spec therefore distinguishes: - supported current load form; diff --git a/docs/specs/runtime/14-boot-profiles.md b/docs/specs/runtime/14-boot-profiles.md index 5b9d7c59..ec9611c6 100644 --- a/docs/specs/runtime/14-boot-profiles.md +++ b/docs/specs/runtime/14-boot-profiles.md @@ -18,6 +18,9 @@ enum BootTarget { This is a firmware/host orchestration contract, not a guest-visible ABI. +The local Home games library is not a separate firmware boot target. It is host +configuration consumed by the Hub profile after boot. + ## 2 Boot Modes ### Hub @@ -27,6 +30,10 @@ When the boot target is `Hub`: - firmware boots into the Hub flow; - no cartridge is auto-launched. +If the host also provides `--games-root `, the Hub may discover and render +valid Game cartridges from that root. Selecting one is a system-owned Home +action that requests cartridge loading after the Hub is already running. + ### Cartridge When the boot target is `Cartridge`: @@ -59,9 +66,39 @@ Typical host-facing boot intents are: - default start -> enter Hub; - run cartridge -> boot with cartridge target; - debug cartridge -> boot with cartridge target plus debug mode parameters. +- games-root start -> enter Hub with a local Game library for Home. The CLI is an entry surface for boot target selection; the firmware contract remains the same underneath. +Host CLI, debugger, and equivalent single-game launch flows are the supported +direct cartridge boot surfaces. Direct boot is not requested by guest bytecode +through a `system.run_cart` syscall or any other app-callable cartridge boot +ABI. + +`--run ` and `--debug ` are direct cartridge boot profiles. They +select one cartridge before normal Hub/Home interaction and remain independent +from the local library. + +`--games-root ` is a Hub/Home library configuration. It does not +auto-launch a cartridge and does not create a guest-visible launch API. + +The v1 games root contract is: + +- the root contains directory cartridges as immediate child directories; +- recursive discovery is not performed; +- only valid cartridges whose manifest declares `app_mode: "Game"` are listed; +- Shell/System apps, packaged `.pmc` cartridges, remote catalogs, marketplace + metadata, search, icons, and rich presentation are outside this profile; +- invalid candidates are logged and omitted from the Home list; +- Home entries may display only `title`, `app_id`, and `app_version`; +- selecting a Home entry requests firmware/SystemOS loading for that cartridge + and transitions through `LoadCartridge`; +- launch failure leaves or returns the machine to Home and records an error. + +Game-to-game switching after a game is already active is not part of this boot +profile. That orchestration belongs to the foreground/home and cartridge switch +contracts. + ## 5 Firmware State Relationship Boot target selection feeds into firmware states such as: @@ -90,7 +127,22 @@ When booting a cartridge in debug mode: Debug mode changes orchestration, not the cartridge contract. -## 7 Relationship to Other Specs +## 7 Smoke Validation + +The local Home library path should be smoke tested in release mode for realistic +interactive speed: + +```sh +cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges +``` + +Direct single-cartridge boot remains a separate validation path: + +```sh +cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console +``` + +## 8 Relationship to Other Specs - [`12-firmware-pos-and-prometeuhub.md`](12-firmware-pos-and-prometeuhub.md) defines the firmware state machine that consumes boot targets. - [`13-cartridge.md`](13-cartridge.md) defines cartridge structure. diff --git a/docs/specs/runtime/16-host-abi-and-syscalls.md b/docs/specs/runtime/16-host-abi-and-syscalls.md index 5944b4c4..c759a232 100644 --- a/docs/specs/runtime/16-host-abi-and-syscalls.md +++ b/docs/specs/runtime/16-host-abi-and-syscalls.md @@ -52,6 +52,11 @@ This identity is: Input queries are VM-owned intrinsic calls in v1 and are outside the syscall identity space. +Cartridge boot is also outside the guest syscall identity space. A cartridge, +Shell app, or other userland guest must not declare or call `system.run_cart`. +Direct cartridge boot is a host/system boot-profile entrypoint, not an +app-callable syscall. + Syscall identity is transport, not the conceptual boundary for runtime profiles. `Game` and `System` may both use syscall transport internally or at their ABI edge, but that does not make their public authoring surfaces the same