dev/system-run-cart #34
@ -179,11 +179,59 @@ mod tests {
|
|||||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
use prometeu_hal::syscalls::caps;
|
use prometeu_hal::syscalls::{Syscall, caps};
|
||||||
use prometeu_system::CrashReport;
|
|
||||||
use prometeu_system::process::ProcessState;
|
use prometeu_system::process::ProcessState;
|
||||||
use prometeu_system::task::{TaskId, TaskState};
|
use prometeu_system::task::{TaskId, TaskState};
|
||||||
use prometeu_system::windows::WindowOwner;
|
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<u8> {
|
fn halting_program() -> Vec<u8> {
|
||||||
let code = assemble("HALT").expect("assemble");
|
let code = assemble("HALT").expect("assemble");
|
||||||
@ -419,7 +467,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn hub_home_does_not_launch_shell_before_home_input_is_armed() {
|
fn hub_home_does_not_launch_shell_before_home_input_is_armed() {
|
||||||
let (mut firmware, mut platform) = hub_home_firmware();
|
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);
|
firmware.tick(&signals, &mut platform);
|
||||||
|
|
||||||
@ -432,7 +480,7 @@ mod tests {
|
|||||||
let (mut firmware, mut platform) = hub_home_firmware();
|
let (mut firmware, mut platform) = hub_home_firmware();
|
||||||
firmware.tick(&InputSignals::default(), &mut platform);
|
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);
|
firmware.tick(&signals, &mut platform);
|
||||||
|
|
||||||
let task_id = match &firmware.state {
|
let task_id = match &firmware.state {
|
||||||
@ -455,7 +503,7 @@ mod tests {
|
|||||||
let (mut firmware, mut platform) = hub_home_firmware();
|
let (mut firmware, mut platform) = hub_home_firmware();
|
||||||
firmware.tick(&InputSignals::default(), &mut platform);
|
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);
|
firmware.tick(&signals, &mut platform);
|
||||||
|
|
||||||
let task_id = match &firmware.state {
|
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]
|
#[test]
|
||||||
fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() {
|
fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() {
|
||||||
let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware();
|
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!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
||||||
assert_eq!(firmware.os.windows().window_count(), 0);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 crate::firmware::prometeu_context::PrometeuContext;
|
||||||
|
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
use prometeu_system::SystemProfileAction;
|
use prometeu_system::SystemProfileAction;
|
||||||
@ -21,16 +24,32 @@ impl HubHomeStep {
|
|||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(SystemProfileAction::LaunchNativeShell(app)) = outcome.action {
|
match outcome.action {
|
||||||
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
|
Some(SystemProfileAction::LaunchNativeShell(app)) => {
|
||||||
let id = ctx.os.windows().add_window(
|
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
|
||||||
app.title().to_string(),
|
let id = ctx.os.windows().add_window(
|
||||||
WindowOwner::Task(task_id),
|
app.title().to_string(),
|
||||||
Rect { x: 40, y: 20, w: 400, h: 220 },
|
WindowOwner::Task(task_id),
|
||||||
app.color(),
|
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)));
|
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
|
None
|
||||||
|
|||||||
@ -297,7 +297,7 @@ pub enum Capability {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct CartridgeManifest {
|
pub struct CartridgeManifest {
|
||||||
pub magic: String,
|
pub magic: String,
|
||||||
pub cartridge_version: u32,
|
pub cartridge_version: u32,
|
||||||
|
|||||||
@ -29,7 +29,6 @@ pub use resolver::{
|
|||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Syscall {
|
pub enum Syscall {
|
||||||
SystemHasCart = 0x0001,
|
SystemHasCart = 0x0001,
|
||||||
SystemRunCart = 0x0002,
|
|
||||||
GfxClear = 0x1001,
|
GfxClear = 0x1001,
|
||||||
GfxFillRect = 0x1002,
|
GfxFillRect = 0x1002,
|
||||||
GfxDrawLine = 0x1003,
|
GfxDrawLine = 0x1003,
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] =
|
||||||
SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart")
|
&[SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart")
|
||||||
.rets(1)
|
.rets(1)
|
||||||
.caps(caps::SYSTEM),
|
.caps(caps::SYSTEM)];
|
||||||
SyscallRegistryEntry::builder(Syscall::SystemRunCart, "system", "run_cart")
|
|
||||||
.caps(caps::SYSTEM)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(50),
|
|
||||||
];
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@ impl Syscall {
|
|||||||
pub fn from_u32(id: u32) -> Option<Self> {
|
pub fn from_u32(id: u32) -> Option<Self> {
|
||||||
match id {
|
match id {
|
||||||
0x0001 => Some(Self::SystemHasCart),
|
0x0001 => Some(Self::SystemHasCart),
|
||||||
0x0002 => Some(Self::SystemRunCart),
|
|
||||||
0x1001 => Some(Self::GfxClear),
|
0x1001 => Some(Self::GfxClear),
|
||||||
0x1002 => Some(Self::GfxFillRect),
|
0x1002 => Some(Self::GfxFillRect),
|
||||||
0x1003 => Some(Self::GfxDrawLine),
|
0x1003 => Some(Self::GfxDrawLine),
|
||||||
@ -75,7 +74,6 @@ impl Syscall {
|
|||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::SystemHasCart => "SystemHasCart",
|
Self::SystemHasCart => "SystemHasCart",
|
||||||
Self::SystemRunCart => "SystemRunCart",
|
|
||||||
Self::GfxClear => "GfxClear",
|
Self::GfxClear => "GfxClear",
|
||||||
Self::GfxFillRect => "GfxFillRect",
|
Self::GfxFillRect => "GfxFillRect",
|
||||||
Self::GfxDrawLine => "GfxDrawLine",
|
Self::GfxDrawLine => "GfxDrawLine",
|
||||||
|
|||||||
@ -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 });
|
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]
|
#[test]
|
||||||
fn resolver_enforces_capabilities() {
|
fn resolver_enforces_capabilities() {
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", name: "clear", version: 1 }];
|
let requested = [SyscallIdentity { module: "gfx2d", name: "clear", version: 1 }];
|
||||||
|
|||||||
@ -13,6 +13,9 @@ pub use services::async_work::{
|
|||||||
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
|
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
|
||||||
};
|
};
|
||||||
pub use services::fs;
|
pub use services::fs;
|
||||||
|
pub use services::game_library::{
|
||||||
|
GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root,
|
||||||
|
};
|
||||||
pub use services::memcard::MemcardAsyncLaneOperation;
|
pub use services::memcard::MemcardAsyncLaneOperation;
|
||||||
pub use services::process;
|
pub use services::process;
|
||||||
pub use services::task;
|
pub use services::task;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::{CrashReport, SystemOS};
|
use crate::{CrashReport, GameLibrary, SystemOS};
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
use prometeu_hal::{
|
use prometeu_hal::{
|
||||||
@ -6,13 +6,20 @@ use prometeu_hal::{
|
|||||||
ShellUiFramePacket,
|
ShellUiFramePacket,
|
||||||
};
|
};
|
||||||
use prometeu_vm::VirtualMachine;
|
use prometeu_vm::VirtualMachine;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const SHELL_A_BUTTON: Rect = Rect { x: 112, 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: 108, w: 112, h: 48 };
|
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 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 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 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 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)]
|
#[cfg(test)]
|
||||||
const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 };
|
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.
|
/// PrometeuHub: Launcher and system UI environment.
|
||||||
pub struct PrometeuHub {
|
pub struct PrometeuHub {
|
||||||
home_input_armed: bool,
|
home_input_armed: bool,
|
||||||
|
game_library: GameLibrary,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[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 {
|
pub enum SystemProfileAction {
|
||||||
LaunchNativeShell(NativeShellApp),
|
LaunchNativeShell(NativeShellApp),
|
||||||
|
LaunchGame { path: PathBuf },
|
||||||
CloseShell,
|
CloseShell,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,13 +100,21 @@ impl Default for PrometeuHub {
|
|||||||
|
|
||||||
impl PrometeuHub {
|
impl PrometeuHub {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { home_input_armed: false }
|
Self { home_input_armed: false, game_library: GameLibrary::empty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&mut self) {
|
pub fn init(&mut self) {
|
||||||
self.home_input_armed = false;
|
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(
|
pub fn gui_update(
|
||||||
&mut self,
|
&mut self,
|
||||||
os: &mut SystemOS,
|
os: &mut SystemOS,
|
||||||
@ -120,6 +137,7 @@ impl PrometeuHub {
|
|||||||
input.touch().x(),
|
input.touch().x(),
|
||||||
input.touch().y(),
|
input.touch().y(),
|
||||||
input.touch().f().pressed,
|
input.touch().f().pressed,
|
||||||
|
self.game_library.entries(),
|
||||||
) {
|
) {
|
||||||
return Some(action);
|
return Some(action);
|
||||||
}
|
}
|
||||||
@ -132,7 +150,7 @@ impl PrometeuHub {
|
|||||||
let pointer_y = platform.input().touch().y();
|
let pointer_y = platform.input().touch().y();
|
||||||
|
|
||||||
let packet = if os.windows().window_count() == 0 {
|
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 {
|
} else {
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
for window in os.windows().windows() {
|
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<SystemProfileAction> {
|
fn action_for_click(
|
||||||
|
in_shell: bool,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
pressed: bool,
|
||||||
|
game_entries: &[crate::GameLibraryEntry],
|
||||||
|
) -> Option<SystemProfileAction> {
|
||||||
if !pressed {
|
if !pressed {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -186,12 +210,31 @@ fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option<Sys
|
|||||||
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
|
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(entry) = game_entries
|
||||||
|
.iter()
|
||||||
|
.take(MAX_HOME_GAME_ROWS)
|
||||||
|
.enumerate()
|
||||||
|
.find(|(index, _entry)| rect_contains(game_row_rect(*index), x, y))
|
||||||
|
.map(|(_index, entry)| entry)
|
||||||
|
{
|
||||||
|
return Some(SystemProfileAction::LaunchGame { path: entry.path.clone() });
|
||||||
|
}
|
||||||
|
|
||||||
HOME_BUTTONS
|
HOME_BUTTONS
|
||||||
.iter()
|
.iter()
|
||||||
.find(|button| rect_contains(button.rect, x, y))
|
.find(|button| rect_contains(button.rect, x, y))
|
||||||
.map(|button| SystemProfileAction::LaunchNativeShell(button.app))
|
.map(|button| SystemProfileAction::LaunchNativeShell(button.app))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn game_row_rect(index: usize) -> 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 {
|
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
|
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
|
|| 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();
|
let mut commands = Vec::new();
|
||||||
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
||||||
fill_rect(&mut commands, VIEWPORT, 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_panel(&mut commands, HOME_PANEL, COLOR_PANEL, COLOR_BORDER);
|
||||||
draw_buffer_text(
|
draw_buffer_text(&mut commands, HOME_PANEL.x + 26, HOME_PANEL.y + 22, "Games", COLOR_TEXT);
|
||||||
&mut commands,
|
|
||||||
HOME_PANEL.x + 26,
|
|
||||||
HOME_PANEL.y + 22,
|
|
||||||
"Native Shell Apps",
|
|
||||||
COLOR_TEXT,
|
|
||||||
);
|
|
||||||
draw_buffer_text(
|
draw_buffer_text(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
HOME_PANEL.x + 26,
|
HOME_PANEL.x + 26,
|
||||||
HOME_PANEL.y + 40,
|
HOME_PANEL.y + 40,
|
||||||
"first retro ui slice",
|
"local library",
|
||||||
COLOR_MUTED,
|
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 {
|
for button in HOME_BUTTONS {
|
||||||
let hovered = rect_contains(button.rect, pointer_x, pointer_y);
|
let hovered = rect_contains(button.rect, pointer_x, pointer_y);
|
||||||
draw_button(&mut commands, button.rect, button.label, hovered);
|
draw_button(&mut commands, button.rect, button.label, hovered);
|
||||||
@ -326,9 +378,17 @@ fn draw_button(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hovere
|
|||||||
Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 },
|
Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 },
|
||||||
COLOR_MUTED,
|
COLOR_MUTED,
|
||||||
);
|
);
|
||||||
fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 12, w: 16, h: 24 }, border);
|
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 + 12, w: 62, h: 24 }, COLOR_PANEL_DARK);
|
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 + 21, label, COLOR_TEXT);
|
draw_buffer_text(commands, rect.x + 40, rect.y + 13, label, COLOR_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_game_row(commands: &mut Vec<GfxUiCommand>, 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<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
||||||
@ -342,7 +402,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shell_a_button_click_emits_launch_action() {
|
fn shell_a_button_click_emits_launch_action() {
|
||||||
assert_eq!(
|
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))
|
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -354,7 +414,8 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
|
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
|
||||||
SHELL_B_BUTTON.y,
|
SHELL_B_BUTTON.y,
|
||||||
true
|
true,
|
||||||
|
&[]
|
||||||
),
|
),
|
||||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
|
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
|
||||||
);
|
);
|
||||||
@ -363,18 +424,30 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn button_hit_test_excludes_right_and_bottom_edges() {
|
fn button_hit_test_excludes_right_and_bottom_edges() {
|
||||||
assert_eq!(
|
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
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn held_pointer_does_not_emit_click_action() {
|
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]
|
#[test]
|
||||||
@ -383,13 +456,38 @@ mod tests {
|
|||||||
assert!(!hub.home_input_armed);
|
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]
|
#[test]
|
||||||
fn close_button_click_emits_close_only_in_shell() {
|
fn close_button_click_emits_close_only_in_shell() {
|
||||||
assert_eq!(
|
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)
|
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]
|
#[test]
|
||||||
@ -403,7 +501,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn home_render_produces_shell_ui_packet_commands() {
|
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!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. })));
|
||||||
assert!(packet.commands.iter().any(|command| {
|
assert!(packet.commands.iter().any(|command| {
|
||||||
|
|||||||
270
crates/console/prometeu-system/src/services/game_library.rs
Normal file
270
crates/console/prometeu-system/src/services/game_library.rs
Normal file
@ -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<GameLibraryEntry>,
|
||||||
|
diagnostics: Vec<GameLibraryDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Path>) -> 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<GameLibraryEntry, GameLibraryDiagnosticReason> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
pub mod async_work;
|
pub mod async_work;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
pub mod game_library;
|
||||||
pub mod memcard;
|
pub mod memcard;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod task;
|
pub mod task;
|
||||||
|
|||||||
@ -168,13 +168,9 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
|
VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match syscall {
|
if syscall == Syscall::SystemHasCart {
|
||||||
Syscall::SystemHasCart => {
|
ret.push_bool(true);
|
||||||
ret.push_bool(true);
|
return Ok(());
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Syscall::SystemRunCart => return Ok(()),
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ensure_game_profile_syscall(syscall)?;
|
self.ensure_game_profile_syscall(syscall)?;
|
||||||
@ -183,7 +179,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
|
|
||||||
match syscall {
|
match syscall {
|
||||||
Syscall::SystemHasCart => unreachable!(),
|
Syscall::SystemHasCart => unreachable!(),
|
||||||
Syscall::SystemRunCart => unreachable!(),
|
|
||||||
Syscall::GfxClear => {
|
Syscall::GfxClear => {
|
||||||
let color = self.get_color(expect_int(args, 0)?)?;
|
let color = self.get_color(expect_int(args, 0)?)?;
|
||||||
self.gfx2d_commands.push(Gfx2dCommand::Clear { color });
|
self.gfx2d_commands.push(Gfx2dCommand::Clear { color });
|
||||||
|
|||||||
@ -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");
|
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
|
||||||
|
|
||||||
let mut asset_ready = false;
|
let start = Instant::now();
|
||||||
for _ in 0..1_000 {
|
let mut status = platform.local_hardware().assets.status(handle);
|
||||||
match platform.local_hardware().assets.status(handle) {
|
while matches!(status, LoadStatus::PENDING | LoadStatus::LOADING)
|
||||||
LoadStatus::READY => {
|
&& start.elapsed() < std::time::Duration::from_secs(5)
|
||||||
asset_ready = true;
|
{
|
||||||
break;
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
status = platform.local_hardware().assets.status(handle);
|
||||||
LoadStatus::PENDING | LoadStatus::LOADING => {
|
|
||||||
std::thread::yield_now();
|
|
||||||
}
|
|
||||||
other => panic!("unexpected asset status before commit: {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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);
|
assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok);
|
||||||
platform.local_hardware().assets.apply_commits();
|
platform.local_hardware().assets.apply_commits();
|
||||||
|
|||||||
@ -33,6 +33,10 @@ struct Cli {
|
|||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
fs_root: Option<String>,
|
fs_root: Option<String>,
|
||||||
|
|
||||||
|
/// Root directory for local game cartridge discovery
|
||||||
|
#[arg(long, value_name = "PATH")]
|
||||||
|
games_root: Option<String>,
|
||||||
|
|
||||||
/// Path to the capability configuration file
|
/// Path to the capability configuration file
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
cap: Option<String>,
|
cap: Option<String>,
|
||||||
@ -55,6 +59,9 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let event_loop = EventLoop::new()?;
|
let event_loop = EventLoop::new()?;
|
||||||
|
|
||||||
let mut runner = HostRunner::new(fs_root, cap_config);
|
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);
|
runner.set_boot_target(boot_target);
|
||||||
|
|
||||||
event_loop.run_app(&mut runner)?;
|
event_loop.run_app(&mut runner)?;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks}
|
|||||||
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
|
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
|
||||||
use prometeu_hal::RuntimePlatform;
|
use prometeu_hal::RuntimePlatform;
|
||||||
use prometeu_hal::telemetry::CertificationConfig;
|
use prometeu_hal::telemetry::CertificationConfig;
|
||||||
use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig};
|
use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
@ -151,6 +151,18 @@ impl HostRunner {
|
|||||||
self.debugger.setup_boot_target(&boot_target, &mut self.firmware);
|
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.
|
/// Creates a new desktop runner instance.
|
||||||
pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self {
|
pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self {
|
||||||
let target_fps = 60;
|
let target_fps = 60;
|
||||||
|
|||||||
@ -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-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-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":[]}
|
{"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-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-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-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-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-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":[]}
|
{"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":[]}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: AGD-0003
|
id: AGD-0003
|
||||||
ticket: system-run-cart
|
ticket: system-run-cart
|
||||||
title: Agenda - System Run Cart
|
title: Agenda - System Run Cart
|
||||||
status: open
|
status: accepted
|
||||||
created: 2026-03-27
|
created: 2026-03-27
|
||||||
resolved:
|
resolved:
|
||||||
decision:
|
decision:
|
||||||
@ -13,11 +13,14 @@ tags: []
|
|||||||
|
|
||||||
## Problema
|
## 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:
|
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 loader aceita sua existência;
|
||||||
- o runtime retorna sucesso vazio;
|
- o runtime retorna sucesso vazio;
|
||||||
- nenhuma troca de cartucho, transição de firmware, validação de alvo ou política de segurança ocorre.
|
- 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
|
## Dor
|
||||||
|
|
||||||
- A ABI expõe uma capability de sistema que, na prática, nao existe.
|
- 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.
|
- 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.
|
- 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.
|
- Qualquer ferramenta, SDK ou documentação construída sobre essa syscall passa a modelar um comportamento inexistente.
|
||||||
|
|
||||||
## Alvo da Discussao
|
## 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 guest;
|
||||||
- do runtime;
|
- do runtime;
|
||||||
- do firmware;
|
- do firmware;
|
||||||
- do host/cartridge loader.
|
- 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.
|
- 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
|
## O Que Precisa Ser Definido
|
||||||
|
|
||||||
1. Semântica da operação.
|
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.
|
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?
|
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.
|
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.
|
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.
|
5. Efeitos observáveis.
|
||||||
O que acontece com:
|
O que acontece quando um guest tenta chamar a superficie removida, e como
|
||||||
- estado do app atual;
|
ferramentas/boot direto continuam carregando cartuchos sem depender de uma
|
||||||
- FS aberto;
|
syscall userland?
|
||||||
- assets carregados;
|
|
||||||
- telemetria;
|
|
||||||
- crash reports;
|
|
||||||
- logs.
|
|
||||||
|
|
||||||
6. Contrato de erro.
|
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
|
## O Que Necessita Para Resolver
|
||||||
|
|
||||||
- fluxo fechado entre syscall -> runtime -> firmware -> loader -> novo app;
|
- remocao de `system.run_cart` da ABI userland;
|
||||||
- modelo explícito para seleção do alvo;
|
- definicao de que boot direto continua por caminho host/CLI/debug, nao por
|
||||||
- definição de lifecycle da troca de cartucho;
|
syscall guest;
|
||||||
- testes de integração cobrindo transição bem-sucedida, alvo inválido, capability insuficiente e cleanup de estado.
|
- 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
|
## Fora de Escopo
|
||||||
|
|
||||||
@ -82,13 +121,82 @@ Ao final da discussão, deve ficar claro se a syscall:
|
|||||||
- UX final do launcher/hub;
|
- UX final do launcher/hub;
|
||||||
- resolução remota ou download de cartuchos;
|
- resolução remota ou download de cartuchos;
|
||||||
- política de marketplace/distribuição.
|
- 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
|
## Critério de Saida Desta Agenda
|
||||||
|
|
||||||
Esta agenda só pode virar PR de implementação quando existir decisão escrita para:
|
Esta agenda só pode virar PR de implementação quando existir decisão escrita para:
|
||||||
|
|
||||||
- assinatura final da syscall;
|
- retirada da superfície userland atual;
|
||||||
- local canônico da troca de estado;
|
- preservacao explicita do boot direto host/CLI/debug;
|
||||||
- política de erro;
|
- comportamento esperado para declaracao/chamada guest obsoleta;
|
||||||
- limpeza de recursos;
|
- fronteira com `AGD-0041` e `AGD-0044`;
|
||||||
- cobertura mínima de testes de integração.
|
- 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.
|
||||||
|
|||||||
@ -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.
|
||||||
@ -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 <cart>` 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 <dir>`.
|
||||||
|
- 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 <dir>`;
|
||||||
|
- 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 <cart>` 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.
|
||||||
@ -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`.
|
||||||
@ -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 <dir>`.
|
||||||
|
|
||||||
|
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 <cart>` 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 <dir>` 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 <cart>` 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 <dir>` 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`.
|
||||||
@ -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`
|
||||||
@ -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`
|
||||||
@ -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/`
|
||||||
@ -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 <dir>`, 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 <dir>` 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 <cart>` 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 <dir>` 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 <dir>` 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 <cart>` 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`
|
||||||
@ -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 <PATH>` 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 <PATH>` 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 <PATH>` is accepted by the desktop host CLI.
|
||||||
|
- Absence of `--games-root` leaves existing Hub boot behavior unchanged.
|
||||||
|
- Direct `--run <cart>` and `--debug <cart>` 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`
|
||||||
@ -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 <dir>` 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`
|
||||||
@ -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`
|
||||||
@ -39,6 +39,10 @@ The VM does not own the machine lifecycle. Firmware does.
|
|||||||
`Hub`, `Task(TaskId)`, and `Overlay`.
|
`Hub`, `Task(TaskId)`, and `Overlay`.
|
||||||
- **Shell task window**: a `WindowManager` window whose owner is
|
- **Shell task window**: a `WindowManager` window whose owner is
|
||||||
`WindowOwner::Task(TaskId)`.
|
`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
|
## 3 POS Responsibilities
|
||||||
|
|
||||||
@ -65,9 +69,20 @@ PrometeuHub is responsible for:
|
|||||||
- reading cartridge metadata needed for launch decisions;
|
- reading cartridge metadata needed for launch decisions;
|
||||||
- deciding launch behavior based on `AppMode`;
|
- deciding launch behavior based on `AppMode`;
|
||||||
- managing the system window surface for system-mode apps.
|
- managing the system window surface for system-mode apps.
|
||||||
|
- presenting the host-provided local games library when `--games-root <dir>` 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.
|
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
|
## 5 App Modes
|
||||||
|
|
||||||
### Game
|
### 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`.
|
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
|
## 8 Firmware States
|
||||||
|
|
||||||
The current firmware state model includes:
|
The current firmware state model includes:
|
||||||
|
|||||||
@ -175,10 +175,30 @@ Runtime loading policy:
|
|||||||
|
|
||||||
This is the working runtime/dev form used by the current loader.
|
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 <dir>`, 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
|
### Packaged `.pmc` form
|
||||||
|
|
||||||
This form is recognized conceptually by the loader boundary, but its actual load path is not implemented yet.
|
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:
|
The cartridge spec therefore distinguishes:
|
||||||
|
|
||||||
- supported current load form;
|
- supported current load form;
|
||||||
|
|||||||
@ -18,6 +18,9 @@ enum BootTarget {
|
|||||||
|
|
||||||
This is a firmware/host orchestration contract, not a guest-visible ABI.
|
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
|
## 2 Boot Modes
|
||||||
|
|
||||||
### Hub
|
### Hub
|
||||||
@ -27,6 +30,10 @@ When the boot target is `Hub`:
|
|||||||
- firmware boots into the Hub flow;
|
- firmware boots into the Hub flow;
|
||||||
- no cartridge is auto-launched.
|
- no cartridge is auto-launched.
|
||||||
|
|
||||||
|
If the host also provides `--games-root <dir>`, 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
|
### Cartridge
|
||||||
|
|
||||||
When the boot target is `Cartridge`:
|
When the boot target is `Cartridge`:
|
||||||
@ -59,9 +66,39 @@ Typical host-facing boot intents are:
|
|||||||
- default start -> enter Hub;
|
- default start -> enter Hub;
|
||||||
- run cartridge -> boot with cartridge target;
|
- run cartridge -> boot with cartridge target;
|
||||||
- debug cartridge -> boot with cartridge target plus debug mode parameters.
|
- 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.
|
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 <cart>` and `--debug <cart>` are direct cartridge boot profiles. They
|
||||||
|
select one cartridge before normal Hub/Home interaction and remain independent
|
||||||
|
from the local library.
|
||||||
|
|
||||||
|
`--games-root <dir>` 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
|
## 5 Firmware State Relationship
|
||||||
|
|
||||||
Boot target selection feeds into firmware states such as:
|
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.
|
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.
|
- [`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.
|
- [`13-cartridge.md`](13-cartridge.md) defines cartridge structure.
|
||||||
|
|||||||
@ -52,6 +52,11 @@ This identity is:
|
|||||||
|
|
||||||
Input queries are VM-owned intrinsic calls in v1 and are outside the syscall identity space.
|
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
|
Syscall identity is transport, not the conceptual boundary for runtime
|
||||||
profiles. `Game` and `System` may both use syscall transport internally or at
|
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
|
their ABI edge, but that does not make their public authoring surfaces the same
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user