Merge pull request 'dev/foreground-stack-game-pause-shell-vm-backed' (#35) from dev/foreground-stack-game-pause-shell-vm-backed into master
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good

Reviewed-on: #35
This commit is contained in:
bquarkz 2026-07-04 23:46:58 +00:00
commit 313b896e7b
47 changed files with 3053 additions and 1642 deletions

View File

@ -1,11 +1,14 @@
use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
use crate::firmware::firmware_state::{
FirmwareState, GameRunningStep, LoadCartridgeStep, ResetStep,
};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::LogSource;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_system::{PrometeuHub, SystemOS};
use prometeu_vm::VirtualMachine;
use prometeu_system::{PrometeuHub, ResidentGameState, SystemOS};
/// PROMETEU Firmware.
///
@ -23,8 +26,6 @@ use prometeu_vm::VirtualMachine;
/// 2. Delegates the logic update to the current active state.
/// 3. Handles state transitions (e.g., from Loading to Playing).
pub struct Firmware {
/// The execution engine (PVM) for user applications.
pub vm: VirtualMachine,
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
pub os: SystemOS,
/// The internal state of the system launcher (Hub).
@ -35,18 +36,19 @@ pub struct Firmware {
pub boot_target: BootTarget,
/// State-machine lifecycle tracker.
state_initialized: bool,
game_input_barrier_frames: u8,
}
impl Firmware {
/// Initializes the firmware in the `Reset` state.
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self {
vm: VirtualMachine::default(),
os: SystemOS::new(cap_config),
hub: PrometeuHub::new(),
state: FirmwareState::Reset(ResetStep),
boot_target: BootTarget::Hub,
state_initialized: false,
game_input_barrier_frames: 0,
}
}
@ -55,23 +57,31 @@ impl Firmware {
/// This method is called exactly once per Host frame (60Hz).
/// It updates peripheral signals and delegates the logic to the current state.
pub fn tick(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
let barriered_signals = InputSignals::default();
let effective_signals = if self.game_input_barrier_frames > 0 {
self.game_input_barrier_frames -= 1;
&barriered_signals
} else {
signals
};
// 0. Process asset commits at the beginning of the frame boundary.
platform.assets_mut().apply_commits();
// 1. Update the peripheral state using the latest signals from the Host.
// This ensures input is consistent throughout the entire update.
platform.input_mut().pad_mut().begin_frame(signals);
platform.input_mut().touch_mut().begin_frame(signals);
platform.input_mut().pad_mut().begin_frame(effective_signals);
platform.input_mut().touch_mut().begin_frame(effective_signals);
// 2. State machine lifecycle management.
if !self.state_initialized {
self.on_enter(signals, platform);
self.on_enter(effective_signals, platform);
self.state_initialized = true;
}
// 3. Update the current state and check for transitions.
if let Some(next_state) = self.on_update(signals, platform) {
self.change_state(next_state, signals, platform);
if let Some(next_state) = self.on_update(effective_signals, platform) {
self.change_state(next_state, effective_signals, platform);
}
}
@ -82,9 +92,23 @@ impl Firmware {
signals: &InputSignals,
platform: &mut dyn RuntimePlatform,
) {
let new_state = match new_state {
FirmwareState::ResumeResidentGame => {
let Some(resume_state) = self.resident_game_resume_state() else {
return;
};
resume_state
}
state => state,
};
let entering_game_from_home = matches!(self.state, FirmwareState::HubHome(_))
&& matches!(new_state, FirmwareState::GameRunning(_));
self.on_exit(signals, platform);
self.state = new_state;
self.state_initialized = false;
if entering_game_from_home {
self.game_input_barrier_frames = self.game_input_barrier_frames.max(1);
}
// Enter the new state immediately to avoid "empty" frames during transitions.
self.on_enter(signals, platform);
@ -94,7 +118,6 @@ impl Firmware {
/// Dispatches the `on_enter` event to the current state implementation.
fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
@ -107,6 +130,7 @@ impl Firmware {
FirmwareState::LaunchHub(s) => s.on_enter(&mut req),
FirmwareState::HubHome(s) => s.on_enter(&mut req),
FirmwareState::LoadCartridge(s) => s.on_enter(&mut req),
FirmwareState::ResumeResidentGame => {}
FirmwareState::GameRunning(s) => s.on_enter(&mut req),
FirmwareState::ShellRunning(s) => s.on_enter(&mut req),
FirmwareState::AppCrashes(s) => s.on_enter(&mut req),
@ -121,7 +145,6 @@ impl Firmware {
platform: &mut dyn RuntimePlatform,
) -> Option<FirmwareState> {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
@ -134,6 +157,7 @@ impl Firmware {
FirmwareState::LaunchHub(s) => s.on_update(&mut req),
FirmwareState::HubHome(s) => s.on_update(&mut req),
FirmwareState::LoadCartridge(s) => s.on_update(&mut req),
FirmwareState::ResumeResidentGame => None,
FirmwareState::GameRunning(s) => s.on_update(&mut req),
FirmwareState::ShellRunning(s) => s.on_update(&mut req),
FirmwareState::AppCrashes(s) => s.on_update(&mut req),
@ -143,7 +167,6 @@ impl Firmware {
/// Dispatches the `on_exit` event to the current state implementation.
fn on_exit(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
@ -156,6 +179,7 @@ impl Firmware {
FirmwareState::LaunchHub(s) => s.on_exit(&mut req),
FirmwareState::HubHome(s) => s.on_exit(&mut req),
FirmwareState::LoadCartridge(s) => s.on_exit(&mut req),
FirmwareState::ResumeResidentGame => {}
FirmwareState::GameRunning(s) => s.on_exit(&mut req),
FirmwareState::ShellRunning(s) => s.on_exit(&mut req),
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
@ -166,6 +190,60 @@ impl Firmware {
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge));
self.state_initialized = false;
}
pub fn request_home_from_host(&mut self) {
self.os.info(LogSource::Pos, "Host requested Home/SystemOS".to_string());
if let FirmwareState::GameRunning(step) = &self.state {
if let Err(error) = self.os.lifecycle().request_home_from_game(step.task_id) {
self.os.warn(
LogSource::Pos,
format!("Home/SystemOS request could not pause Game: {error:?}"),
);
} else {
self.game_input_barrier_frames = 1;
}
}
}
pub fn resume_resident_game_from_home(&mut self) -> bool {
let Some(state) = self.resident_game_resume_state() else {
return false;
};
self.state = state;
self.state_initialized = false;
true
}
fn resident_game_resume_state(&mut self) -> Option<FirmwareState> {
let task_id = self.os.lifecycle().resident_game_task()?;
if let Err(error) = self.os.lifecycle().resume_task(task_id) {
self.os.warn(LogSource::Pos, format!("Resident Game resume failed: {error:?}"));
return None;
}
let app_id = self
.os
.sessions()
.vm_session_for_task(task_id)
.map(|session| session.app_id)
.unwrap_or_else(|| self.os.vm().current_app_id());
self.os.vm().transition_render_owner(AppMode::Game, app_id);
self.game_input_barrier_frames = self.game_input_barrier_frames.max(1);
Some(FirmwareState::GameRunning(GameRunningStep::new(task_id)))
}
pub fn game_lifecycle_audio_paused(&mut self) -> bool {
matches!(
self.os.lifecycle().resident_game().map(|game| game.state),
Some(
ResidentGameState::PauseRequested { .. }
| ResidentGameState::PausedSuspended
| ResidentGameState::ResumeRequested,
)
)
}
}
#[cfg(test)]
@ -175,6 +253,7 @@ mod tests {
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::TestPlatform;
use prometeu_hal::RuntimePlatform;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color;
@ -183,7 +262,10 @@ mod tests {
use prometeu_system::process::ProcessState;
use prometeu_system::task::{TaskId, TaskState};
use prometeu_system::windows::WindowOwner;
use prometeu_system::{CrashReport, discover_games_root};
use prometeu_system::{
CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState,
discover_games_root,
};
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
@ -302,9 +384,13 @@ mod tests {
}
fn valid_cartridge(app_mode: AppMode) -> Cartridge {
valid_cartridge_with(app_mode, 9, "Valid Cart")
}
fn valid_cartridge_with(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge {
Cartridge {
app_id: 9,
title: "Valid Cart".into(),
app_id,
title: title.into(),
app_version: "1.0.0".into(),
app_mode,
capabilities: caps::NONE,
@ -315,6 +401,16 @@ mod tests {
}
}
fn vm_session_tick_index(firmware: &mut Firmware, task_id: TaskId) -> u64 {
firmware
.os
.sessions()
.vm_session_for_task(task_id)
.expect("VM session should exist")
.runtime
.tick_index
}
fn load_shell_running_firmware() -> (Firmware, TestPlatform, InputSignals, TaskId) {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
@ -429,6 +525,24 @@ mod tests {
assert!(matches!(firmware.state, FirmwareState::LaunchHub(_)));
}
#[test]
fn reset_clears_vm_sessions_and_runtime_identity() {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Game));
firmware.tick(&signals, &mut platform);
assert_eq!(firmware.os.sessions().vm_session_count(), 1);
assert_eq!(firmware.os.vm().current_cartridge_title(), "Valid Cart");
firmware.change_state(FirmwareState::Reset(ResetStep), &signals, &mut platform);
assert_eq!(firmware.os.sessions().vm_session_count(), 0);
assert_eq!(firmware.os.vm().current_cartridge_title(), "");
}
#[test]
fn load_cartridge_routes_system_apps_to_system_running() {
let mut firmware = Firmware::new(None);
@ -560,6 +674,60 @@ mod tests {
assert_eq!(firmware.os.windows().window_count(), 0);
}
#[test]
fn hub_home_clicking_resident_game_resumes_same_task_without_reloading() {
let root = TestGameRoot::new();
root.add_game("stress", true);
let (mut firmware, mut platform) = hub_home_firmware();
firmware.hub.set_game_library(discover_games_root(&root.path));
firmware.tick(&InputSignals::default(), &mut platform);
let launch_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
firmware.tick(&launch_click, &mut platform);
firmware.tick(&InputSignals::default(), &mut platform);
let game_task = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected GameRunning state, got {:?}", other),
};
firmware.request_home_from_host();
firmware.tick(&InputSignals::default(), &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended));
firmware.tick(&InputSignals::default(), &mut platform);
firmware.tick(&launch_click, &mut platform);
match &firmware.state {
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task),
other => panic!("expected resident GameRunning state, got {:?}", other),
}
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(game_task));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Foreground));
assert_eq!(
firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::ResumeForeground)
);
let held_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
firmware.tick(&held_click, &mut platform);
assert!(!platform.input().pad().any());
assert!(!platform.input().touch().f().down);
assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty());
assert_eq!(
firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::ResumeForeground)
);
}
#[test]
fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() {
let root = TestGameRoot::new();
@ -727,6 +895,236 @@ mod tests {
assert_eq!(firmware.os.windows().window_count(), 0);
}
#[test]
fn host_home_request_delivers_pause_tick_before_forced_suspension() {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Game));
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected GameRunning state, got {:?}", other),
};
let tick_index_before_home = firmware.os.vm().tick_index();
let render_ownership_before_home = firmware.os.vm().active_render_ownership();
firmware.request_home_from_host();
assert!(firmware.game_lifecycle_audio_paused());
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id));
assert_eq!(
firmware.os.lifecycle().resident_game().map(|game| game.state),
Some(ResidentGameState::PauseRequested { remaining_ticks: 1 })
);
assert_eq!(
firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::Pause)
);
let held_signals =
InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals };
firmware.tick(&held_signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Suspended));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(task_id),
Ok(ProcessState::Suspended)
);
assert_eq!(firmware.os.vm().tick_index(), tick_index_before_home + 1);
assert!(!platform.input().pad().any());
assert!(!platform.input().touch().f().down);
let render_ownership_after_home = firmware.os.vm().active_render_ownership();
assert_eq!(render_ownership_after_home.app_mode, AppMode::Shell);
assert_eq!(render_ownership_after_home.app_id, 0);
assert!(render_ownership_after_home.epoch > render_ownership_before_home.epoch);
assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty());
assert_eq!(
firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::Pause)
);
let tick_index_after_suspension = firmware.os.vm().tick_index();
firmware.tick(&signals, &mut platform);
assert_eq!(firmware.os.vm().tick_index(), tick_index_after_suspension);
}
#[test]
fn resident_game_resume_restores_render_owner_after_resume_event_and_barriers_input() {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Game));
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected GameRunning state, got {:?}", other),
};
firmware.request_home_from_host();
firmware.tick(&signals, &mut platform);
let hub_ownership = firmware.os.vm().active_render_ownership();
assert!(firmware.resume_resident_game_from_home());
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
assert!(!firmware.game_lifecycle_audio_paused());
assert_eq!(
firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::ResumeForeground)
);
let game_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(game_ownership.app_mode, AppMode::Game);
assert_eq!(game_ownership.app_id, 9);
assert!(game_ownership.epoch > hub_ownership.epoch);
let held_signals =
InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals };
firmware.tick(&held_signals, &mut platform);
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground));
assert!(!platform.input().pad().any());
assert!(!platform.input().touch().f().down);
assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty());
assert_eq!(
firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::ResumeForeground)
);
}
#[test]
fn game_home_shell_hub_same_game_flow_is_end_to_end_deterministic() {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Game));
firmware.tick(&signals, &mut platform);
let game_task = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected GameRunning state, got {:?}", other),
};
firmware.request_home_from_host();
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended));
firmware.tick(&signals, &mut platform);
let shell_a_click =
InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() };
firmware.tick(&shell_a_click, &mut platform);
let shell_task = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell_task));
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended));
let tick_index_before_native_shell_updates = firmware.os.vm().tick_index();
for _ in 0..61 {
firmware.tick(&signals, &mut platform);
}
assert!(matches!(firmware.state, FirmwareState::ShellRunning(_)));
assert_eq!(firmware.os.vm().tick_index(), tick_index_before_native_shell_updates);
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended));
let close_shell = InputSignals { start_signal: true, ..Default::default() };
firmware.tick(&close_shell, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
assert_eq!(firmware.os.lifecycle().task_state(shell_task), Some(TaskState::Closed));
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert!(firmware.resume_resident_game_from_home());
match &firmware.state {
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task),
other => panic!("expected GameRunning state, got {:?}", other),
}
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(game_task));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Foreground));
assert_eq!(
firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::ResumeForeground)
);
}
#[test]
fn resident_game_and_vm_shell_sessions_tick_independently() {
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge_with(AppMode::Game, 9, "Resident Game"));
firmware.tick(&signals, &mut platform);
let game_task = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected GameRunning state, got {:?}", other),
};
firmware.tick(&signals, &mut platform);
let game_tick_before_home = vm_session_tick_index(&mut firmware, game_task);
firmware.request_home_from_host();
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended));
let game_tick_after_suspend = vm_session_tick_index(&mut firmware, game_task);
assert_eq!(game_tick_after_suspend, game_tick_before_home + 1);
firmware.load_cartridge(valid_cartridge_with(AppMode::Shell, 77, "VM Shell"));
firmware.tick(&signals, &mut platform);
let shell_task = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
assert_ne!(shell_task, game_task);
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended));
let shell_tick_before = vm_session_tick_index(&mut firmware, shell_task);
for _ in 0..3 {
firmware.tick(&signals, &mut platform);
}
assert_eq!(vm_session_tick_index(&mut firmware, game_task), game_tick_after_suspend);
assert_eq!(vm_session_tick_index(&mut firmware, shell_task), shell_tick_before + 3);
let close_shell = InputSignals { start_signal: true, ..Default::default() };
firmware.tick(&close_shell, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(shell_task), Some(TaskState::Closed));
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task));
assert!(firmware.resume_resident_game_from_home());
firmware.tick(&signals, &mut platform);
match &firmware.state {
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task),
other => panic!("expected GameRunning state, got {:?}", other),
}
assert!(vm_session_tick_index(&mut firmware, game_task) > game_tick_after_suspend);
}
#[test]
fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() {
assert!(Syscall::from_u32(0x0002).is_none());

View File

@ -14,6 +14,7 @@ pub enum FirmwareState {
LaunchHub(LaunchHubStep),
HubHome(HubHomeStep),
LoadCartridge(LoadCartridgeStep),
ResumeResidentGame,
GameRunning(GameRunningStep),
ShellRunning(ShellRunningStep),
AppCrashes(AppCrashesStep),

View File

@ -1,8 +1,9 @@
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_system::CrashReport;
use prometeu_system::task::{TaskId, TaskState};
use prometeu_system::{CrashReport, ResidentGameState};
#[derive(Debug, Clone)]
pub struct GameRunningStep {
@ -36,13 +37,35 @@ impl GameRunningStep {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform);
let pause_requested = matches!(
ctx.os.lifecycle().resident_game().map(|game| game.state),
Some(ResidentGameState::PauseRequested { .. })
);
let result = ctx.os.vm().tick_session(self.task_id, ctx.signals, ctx.platform);
if let Some(report) = result {
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
if pause_requested {
match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) {
Ok(true) => {
ctx.os.vm().transition_render_owner(AppMode::Shell, 0);
return Some(FirmwareState::HubHome(HubHomeStep));
}
Ok(false) => {}
Err(_) => {
let report = CrashReport::VmPanic {
message: "failed to advance game pause budget".to_string(),
pc: None,
};
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
}
}
None
}

View File

@ -2,6 +2,7 @@ use crate::firmware::firmware_state::{
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::primitives::Rect;
@ -19,14 +20,29 @@ impl HubHomeStep {
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform);
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.platform);
if let Some(report) = outcome.crash {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
match outcome.action {
Some(SystemProfileAction::LaunchNativeShell(app)) => {
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
let task_id = match ctx
.os
.sessions()
.try_create_native_shell_task(app.app_id(), app.title())
{
Ok(task_id) => task_id,
Err(error) => {
ctx.os.log(
LogLevel::Warn,
LogSource::Hub,
0,
format!("Failed to launch native shell {}: {:?}", app.title(), error),
);
return None;
}
};
let id = ctx.os.windows().add_window(
app.title().to_string(),
WindowOwner::Task(task_id),
@ -38,6 +54,32 @@ impl HubHomeStep {
}
Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) {
Ok(cartridge) => {
if cartridge.app_mode == AppMode::Game
&& let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task()
{
let resident_app_id = ctx
.os
.sessions()
.vm_session_for_task(resident_task_id)
.map(|session| session.app_id)
.unwrap_or(0);
if resident_app_id == cartridge.app_id {
return Some(FirmwareState::ResumeResidentGame);
}
ctx.os.log(
LogLevel::Warn,
LogSource::Hub,
0,
format!(
"Cannot launch Home game {} while resident game app_id {} is active",
cartridge.title, resident_app_id
),
);
return None;
}
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
}
Err(error) => {

View File

@ -14,11 +14,13 @@ use prometeu_system::windows::WindowOwner;
pub struct LoadCartridgeStep {
pub cartridge: Cartridge,
init_error: Option<CrashReport>,
loaded_task_id: Option<prometeu_system::task::TaskId>,
loaded_app_mode: Option<AppMode>,
}
impl LoadCartridgeStep {
pub fn new(cartridge: Cartridge) -> Self {
Self { cartridge, init_error: None }
Self { cartridge, init_error: None, loaded_task_id: None, loaded_app_mode: None }
}
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
@ -36,7 +38,15 @@ impl LoadCartridgeStep {
self.cartridge.assets.clone(),
);
self.init_error = ctx.os.vm().initialize(ctx.vm, &self.cartridge).err();
match ctx.os.sessions().load_vm_cartridge(&self.cartridge) {
Ok(loaded) => {
self.loaded_task_id = Some(loaded.task_id);
self.loaded_app_mode = Some(loaded.app_mode);
}
Err(report) => {
self.init_error = Some(report.into_crash_report("load VM cartridge into session"));
}
}
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
@ -44,11 +54,18 @@ impl LoadCartridgeStep {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
if self.cartridge.app_mode == AppMode::Shell {
let task_id = ctx
.os
.sessions()
.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone());
let Some(task_id) = self.loaded_task_id.take() else {
let report = CrashReport::VmPanic {
message: format!(
"cartridge {} was not loaded into a VM session",
self.cartridge.title
),
pc: None,
};
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
};
if self.loaded_app_mode == Some(AppMode::Shell) {
let id = ctx.os.windows().add_window(
self.cartridge.title.clone(),
WindowOwner::Task(task_id),
@ -59,10 +76,6 @@ impl LoadCartridgeStep {
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
}
let task_id = ctx
.os
.sessions()
.create_vm_game_task(self.cartridge.app_id, self.cartridge.title.clone());
Some(FirmwareState::GameRunning(GameRunningStep::new(task_id)))
}

View File

@ -9,7 +9,8 @@ pub struct ResetStep;
impl ResetStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
ctx.os.vm().reset(ctx.vm);
ctx.os.sessions().clear_vm_sessions();
ctx.os.vm().reset_global_runtime_state();
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {

View File

@ -1,6 +1,7 @@
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::LogSource;
use prometeu_system::process::ProcessKind;
use prometeu_system::task::{TaskId, TaskState};
use prometeu_system::{CrashReport, SystemProfileAction};
@ -59,7 +60,36 @@ impl ShellRunningStep {
}
}
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform);
let process_kind = match ctx.os.lifecycle().process_kind_for_task(self.task_id) {
Ok(process_kind) => process_kind,
Err(error) => {
let report = CrashReport::VmPanic {
message: format!(
"ShellRunningStep cannot resolve process for {:?}: {:?}",
self.task_id, error,
),
pc: None,
};
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
};
let outcome = match process_kind {
ProcessKind::VmShell => {
ctx.hub.update_vm_shell_profile(ctx.os, self.task_id, ctx.signals, ctx.platform)
}
ProcessKind::NativeShell => ctx.hub.update_native_shell_profile(ctx.os, ctx.platform),
ProcessKind::VmGame => {
let report = CrashReport::VmPanic {
message: format!(
"ShellRunningStep expected shell process for {:?}, got VmGame",
self.task_id,
),
pc: None,
};
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
};
if let Some(report) = outcome.crash {
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));

View File

@ -1,10 +1,8 @@
use crate::firmware::boot_target::BootTarget;
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_system::{PrometeuHub, SystemOS};
use prometeu_vm::VirtualMachine;
pub struct PrometeuContext<'a> {
pub vm: &'a mut VirtualMachine,
pub os: &'a mut SystemOS,
pub hub: &'a mut PrometeuHub,
pub boot_target: &'a BootTarget,

View File

@ -40,7 +40,7 @@ pub enum DebugResponse {
Breakpoints { pcs: Vec<usize> },
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandshakeCartridge {
pub app_id: u32,
pub title: String,

View File

@ -4,7 +4,10 @@ mod programs;
mod services;
pub use crash_report::CrashReport;
pub use os::{LifecycleError, LifecycleOperation, SystemOS};
pub use os::{
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
LifecycleOperation, SessionError, SystemOS,
};
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
pub use services::async_work::{
@ -12,6 +15,9 @@ pub use services::async_work::{
AsyncWorkJobKind, AsyncWorkJobOutcome, AsyncWorkLane, AsyncWorkLaneConfig,
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
};
pub use services::foreground::{
ForegroundOwner, ForegroundStack, ForegroundStackError, ResidentGame, ResidentGameState,
};
pub use services::fs;
pub use services::game_library::{
GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root,
@ -23,4 +29,5 @@ pub use services::vm_runtime::{
LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff,
RenderWorkerHandoffWait, RenderWorkerOwnership, VirtualMachineRuntime,
};
pub use services::vm_session;
pub use services::windows;

View File

@ -1,8 +1,14 @@
use crate::CrashReport;
use crate::os::SystemOS;
use crate::os::{LifecycleError, LifecycleOperation};
use crate::process::{ProcessId, ProcessState};
use crate::task::{TaskId, TaskState};
use crate::os::{
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
LifecycleOperation,
};
use crate::process::{Process, ProcessId, ProcessKind, ProcessState};
use crate::services::foreground::{
ForegroundOwner, ForegroundStackError, ResidentGame, ResidentGameState,
};
use crate::task::{TaskId, TaskKind, TaskState};
pub struct LifecycleFacade<'a> {
pub(in crate::os) os: &'a mut SystemOS,
@ -14,28 +20,145 @@ impl<'a> LifecycleFacade<'a> {
}
pub fn process_state_for_task(&self, task_id: TaskId) -> Result<ProcessState, LifecycleError> {
let process_id = process_id_for_task(self.os, task_id)?;
self.os
.process_manager
.get(process_id)
.map(|process| process.state)
.ok_or(LifecycleError::ProcessNotFound(process_id))
process_for_task(self.os, task_id).map(|process| process.state)
}
pub fn process_kind_for_task(&self, task_id: TaskId) -> Result<ProcessKind, LifecycleError> {
process_for_task(self.os, task_id).map(|process| process.kind)
}
pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
match task_kind_for_task(self.os, task_id)? {
TaskKind::Game => self.set_game_foreground_task(task_id),
TaskKind::Shell => self.set_shell_foreground_task(task_id),
}
}
pub fn set_game_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
let process_id = process_id_for_task(self.os, task_id)?;
if task_kind_for_task(self.os, task_id)? != TaskKind::Game {
return Err(LifecycleError::InvalidTransition {
task_id,
from: self.task_state(task_id).unwrap_or(TaskState::Closed),
operation: LifecycleOperation::SetForeground,
});
}
self.os.foreground_stack.set_game_foreground(task_id).map_err(map_foreground_error)?;
self.os.task_manager.set_foreground(task_id);
self.os.process_manager.mark_running(process_id);
Ok(())
}
pub fn set_shell_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
let process_id = process_id_for_task(self.os, task_id)?;
if task_kind_for_task(self.os, task_id)? != TaskKind::Shell {
return Err(LifecycleError::InvalidTransition {
task_id,
from: self.task_state(task_id).unwrap_or(TaskState::Closed),
operation: LifecycleOperation::SetForeground,
});
}
if let Some(game_task_id) = self.os.foreground_stack.resident_game_task()
&& game_task_id != task_id
&& self.task_state(game_task_id) != Some(TaskState::Suspended)
{
let game_process_id = process_id_for_task(self.os, game_task_id)?;
self.os.task_manager.mark_suspended(game_task_id);
self.os.process_manager.mark_suspended(game_process_id);
}
self.os.foreground_stack.set_shell_foreground(task_id).map_err(map_foreground_error)?;
self.os.task_manager.set_foreground(task_id);
self.os.process_manager.mark_running(process_id);
Ok(())
}
pub fn foreground_owner(&self) -> ForegroundOwner {
self.os.foreground_stack.owner()
}
pub fn resident_game(&self) -> Option<ResidentGame> {
self.os.foreground_stack.resident_game()
}
pub fn resident_game_task(&self) -> Option<TaskId> {
self.os.foreground_stack.resident_game_task()
}
pub fn request_home_from_game(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
let _process_id = process_id_for_task(self.os, task_id)?;
let previous = self.os.foreground_stack.resident_game();
self.os
.foreground_stack
.request_home_from_game(task_id, DEFAULT_GAME_PAUSE_BUDGET_TICKS)
.map_err(map_foreground_error)?;
if !matches!(
previous.map(|game| game.state),
Some(ResidentGameState::PauseRequested { .. } | ResidentGameState::PausedSuspended)
) {
self.os
.game_lifecycle_events
.push(GameLifecycleEvent { task_id, kind: GameLifecycleEventKind::Pause });
}
Ok(())
}
pub fn advance_game_pause_budget(&mut self, task_id: TaskId) -> Result<bool, LifecycleError> {
let expired =
self.os.foreground_stack.advance_pause_budget(task_id).map_err(map_foreground_error)?;
if expired {
let process_id = process_id_for_task(self.os, task_id)?;
self.os.task_manager.mark_suspended(task_id);
self.os.process_manager.mark_suspended(process_id);
}
Ok(expired)
}
pub fn pending_game_lifecycle_events(&self) -> &[GameLifecycleEvent] {
&self.os.game_lifecycle_events
}
pub fn take_game_lifecycle_events(&mut self) -> Vec<GameLifecycleEvent> {
std::mem::take(&mut self.os.game_lifecycle_events)
}
pub fn return_to_hub(&mut self) {
self.os.foreground_stack.return_to_hub();
self.os.task_manager.clear_foreground();
}
pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
let process_id = process_id_for_task(self.os, task_id)?;
self.os.task_manager.mark_suspended(task_id);
self.os.process_manager.mark_suspended(process_id);
match self.os.foreground_stack.owner() {
ForegroundOwner::Game(owner) if owner == task_id => {
self.os
.foreground_stack
.request_home_from_game(task_id, DEFAULT_GAME_PAUSE_BUDGET_TICKS)
.map_err(map_foreground_error)?;
self.os
.foreground_stack
.advance_pause_budget(task_id)
.map_err(map_foreground_error)?;
}
ForegroundOwner::Shell(owner) if owner == task_id => {
self.os.foreground_stack.return_to_hub();
}
_ => {}
}
Ok(())
}
@ -54,8 +177,32 @@ impl<'a> LifecycleFacade<'a> {
let process_id = process_id_for_task(self.os, task_id)?;
match task_kind_for_task(self.os, task_id)? {
TaskKind::Game => {
self.os
.foreground_stack
.request_resume_game(task_id)
.map_err(map_foreground_error)?;
self.os.task_manager.set_foreground(task_id);
self.os.process_manager.mark_running(process_id);
self.os.game_lifecycle_events.push(GameLifecycleEvent {
task_id,
kind: GameLifecycleEventKind::ResumeForeground,
});
self.os
.foreground_stack
.complete_resume_game(task_id)
.map_err(map_foreground_error)?;
}
TaskKind::Shell => {
self.os
.foreground_stack
.set_shell_foreground(task_id)
.map_err(map_foreground_error)?;
self.os.task_manager.set_foreground(task_id);
self.os.process_manager.mark_running(process_id);
}
}
Ok(())
}
@ -65,6 +212,12 @@ impl<'a> LifecycleFacade<'a> {
self.os.task_manager.close_task(task_id);
self.os.process_manager.mark_stopped(process_id);
if self.os.foreground_stack.resident_game_task() == Some(task_id) {
self.os.foreground_stack.clear_resident_game(task_id);
} else if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Shell(owner) if owner == task_id)
{
self.os.foreground_stack.return_to_hub();
}
Ok(())
}
@ -78,11 +231,22 @@ impl<'a> LifecycleFacade<'a> {
self.os.task_manager.mark_crashed(task_id);
self.os.process_manager.mark_crashed(process_id);
if self.os.foreground_stack.resident_game_task() == Some(task_id) {
self.os.foreground_stack.clear_resident_game(task_id);
} else if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Shell(owner) if owner == task_id)
{
self.os.foreground_stack.return_to_hub();
}
Ok(())
}
}
fn task_kind_for_task(os: &SystemOS, task_id: TaskId) -> Result<TaskKind, LifecycleError> {
let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
Ok(task.kind)
}
fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result<ProcessId, LifecycleError> {
let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
@ -92,3 +256,22 @@ fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result<ProcessId, Life
Ok(task.process_id)
}
fn process_for_task(os: &SystemOS, task_id: TaskId) -> Result<&Process, LifecycleError> {
let process_id = process_id_for_task(os, task_id)?;
os.process_manager.get(process_id).ok_or(LifecycleError::ProcessNotFound(process_id))
}
fn map_foreground_error(error: ForegroundStackError) -> LifecycleError {
match error {
ForegroundStackError::ResidentGameAlreadyExists { existing, requested } => {
LifecycleError::ResidentGameAlreadyExists { existing, requested }
}
ForegroundStackError::ForegroundShellAlreadyExists { existing, requested } => {
LifecycleError::ForegroundShellAlreadyExists { existing, requested }
}
ForegroundStackError::TaskIsNotResidentGame(task_id) => {
LifecycleError::TaskIsNotResidentGame(task_id)
}
}
}

View File

@ -6,6 +6,6 @@ mod window;
pub use fs::FsFacade;
pub use lifecycle::LifecycleFacade;
pub use sessions::SessionsFacade;
pub use sessions::{SessionError, SessionsFacade};
pub use vm::VmFacade;
pub use window::WindowFacade;

View File

@ -1,44 +1,209 @@
use crate::CrashReport;
use crate::os::LifecycleError;
use crate::os::SystemOS;
use crate::process::ProcessId;
use crate::task::TaskId;
use crate::vm_session::{VmSession, VmSessionError};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LoadedVmCartridge {
pub task_id: TaskId,
pub app_mode: AppMode,
pub reused_existing_session: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionError {
Lifecycle(LifecycleError),
VmSession(VmSessionError),
TaskNotFound(TaskId),
ProcessNotFound(ProcessId),
MissingVmSession(TaskId),
Initialization(CrashReport),
}
impl SessionError {
pub fn into_crash_report(self, operation: &str) -> CrashReport {
match self {
Self::Initialization(report) => report,
error => {
CrashReport::VmPanic { message: format!("{operation} failed: {error:?}"), pc: None }
}
}
}
}
impl From<LifecycleError> for SessionError {
fn from(error: LifecycleError) -> Self {
Self::Lifecycle(error)
}
}
impl From<VmSessionError> for SessionError {
fn from(error: VmSessionError) -> Self {
Self::VmSession(error)
}
}
pub struct SessionsFacade<'a> {
pub(in crate::os) os: &'a mut SystemOS,
}
impl<'a> SessionsFacade<'a> {
#[cfg(test)]
pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
self.try_create_vm_game_task(app_id, title)
.expect("test/setup VM game creation should succeed")
}
pub fn try_create_vm_game_task(
&mut self,
app_id: u32,
title: impl Into<String>,
) -> Result<TaskId, SessionError> {
if let Some(session) = self.os.vm_sessions.resident_game_for_app(app_id) {
return Ok(session.task_id);
}
let title = title.into();
let process_id = self.os.process_manager.spawn_vm_game(app_id, title.clone());
let task_id = self.os.task_manager.create_game_task(process_id, app_id, title);
self.os
.lifecycle()
.set_foreground_task(task_id)
.expect("newly created game task should have an associated process");
self.os.lifecycle().set_game_foreground_task(task_id)?;
self.try_create_vm_session_for_task(task_id)?;
task_id
Ok(task_id)
}
#[cfg(test)]
pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
self.try_create_vm_shell_task(app_id, title)
.expect("test/setup VM shell creation should succeed")
}
pub fn try_create_vm_shell_task(
&mut self,
app_id: u32,
title: impl Into<String>,
) -> Result<TaskId, SessionError> {
let title = title.into();
let process_id = self.os.process_manager.spawn_vm_shell(app_id, title.clone());
let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title);
self.os
.lifecycle()
.set_foreground_task(task_id)
.expect("newly created shell task should have an associated process");
self.os.lifecycle().set_shell_foreground_task(task_id)?;
self.try_create_vm_session_for_task(task_id)?;
task_id
Ok(task_id)
}
#[cfg(test)]
pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
self.try_create_native_shell_task(app_id, title)
.expect("test/setup native shell creation should succeed")
}
pub fn try_create_native_shell_task(
&mut self,
app_id: u32,
title: impl Into<String>,
) -> Result<TaskId, SessionError> {
let title = title.into();
let process_id = self.os.process_manager.spawn_native_shell(app_id, title.clone());
let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title);
self.os
.lifecycle()
.set_foreground_task(task_id)
.expect("newly created native shell task should have an associated process");
self.os.lifecycle().set_shell_foreground_task(task_id)?;
task_id
Ok(task_id)
}
pub fn load_vm_cartridge(
&mut self,
cartridge: &Cartridge,
) -> Result<LoadedVmCartridge, SessionError> {
let existing_game_task = if cartridge.app_mode == AppMode::Game {
self.os
.vm_sessions
.resident_game_for_app(cartridge.app_id)
.map(|session| session.task_id)
} else {
None
};
let (task_id, reused_existing_session) = if let Some(task_id) = existing_game_task {
(task_id, true)
} else if cartridge.app_mode == AppMode::Shell {
(self.try_create_vm_shell_task(cartridge.app_id, cartridge.title.clone())?, false)
} else {
let task_id =
self.try_create_vm_game_task(cartridge.app_id, cartridge.title.clone())?;
(task_id, false)
};
if !reused_existing_session {
self.try_initialize_session_cartridge(task_id, cartridge)?;
}
Ok(LoadedVmCartridge { task_id, app_mode: cartridge.app_mode, reused_existing_session })
}
pub fn vm_session_for_task(&self, task_id: TaskId) -> Option<&VmSession> {
self.os.vm_sessions.for_task(task_id)
}
pub fn vm_session_for_task_mut(&mut self, task_id: TaskId) -> Option<&mut VmSession> {
self.os.vm_sessions.for_task_mut(task_id)
}
pub fn resident_game_session_for_app(&self, app_id: u32) -> Option<&VmSession> {
self.os.vm_sessions.resident_game_for_app(app_id)
}
pub fn vm_session_count(&self) -> usize {
self.os.vm_sessions.len()
}
pub fn clear_vm_sessions(&mut self) {
self.os.vm_sessions.clear();
}
pub fn try_create_vm_session_for_task(&mut self, task_id: TaskId) -> Result<(), SessionError> {
let task =
self.os.task_manager.get(task_id).ok_or(SessionError::TaskNotFound(task_id))?.clone();
let process = self
.os
.process_manager
.get(task.process_id)
.ok_or(SessionError::ProcessNotFound(task.process_id))?
.clone();
let cap_config = self.os.vm_runtime.certifier.config;
let logs_count = Arc::clone(&self.os.log_service.logs_count);
self.os
.vm_sessions
.create_for_task(&task, &process, "", Some(cap_config), logs_count)
.map(|_| ())
.map_err(SessionError::from)
}
pub fn try_initialize_session_cartridge(
&mut self,
task_id: TaskId,
cartridge: &Cartridge,
) -> Result<(), SessionError> {
let session = self
.os
.vm_sessions
.for_task_mut(task_id)
.ok_or(SessionError::MissingVmSession(task_id))?;
session.app_id = cartridge.app_id;
session.title = cartridge.title.clone();
session.app_version = cartridge.app_version.clone();
session.app_mode = cartridge.app_mode;
session.clear_cartridge_service_state();
session
.runtime
.initialize_vm(&mut self.os.log_service, &mut session.vm, cartridge)
.map_err(SessionError::Initialization)
}
}

View File

@ -1,14 +1,15 @@
use crate::CrashReport;
use crate::os::SystemOS;
use crate::os::{GameLifecycleEvent, SystemOS};
use crate::task::TaskId;
use crate::vm_session::VmSession;
use crate::{RenderWorkerConfig, RenderWorkerController};
use prometeu_bytecode::Value;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
use prometeu_hal::{
FrameId, InputSignals, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink,
RuntimePlatform,
FrameId, InputSignals, RenderOwnership, RenderWorkerBackend, RenderWorkerError,
RenderWorkerFrameSink, RuntimePlatform,
};
use prometeu_vm::VirtualMachine;
use std::sync::atomic::Ordering;
pub struct VmFacade<'a> {
@ -16,88 +17,216 @@ pub struct VmFacade<'a> {
}
impl<'a> VmFacade<'a> {
pub fn initialize(
pub fn tick_session(
&mut self,
vm: &mut VirtualMachine,
cartridge: &Cartridge,
) -> Result<(), CrashReport> {
self.os.clear_cartridge_service_state();
self.os.vm_runtime.initialize_vm(&mut self.os.log_service, vm, cartridge)
}
pub fn tick(
&mut self,
vm: &mut VirtualMachine,
task_id: TaskId,
signals: &InputSignals,
platform: &mut dyn RuntimePlatform,
) -> Option<CrashReport> {
self.os.vm_runtime.tick(
self.deliver_pending_game_lifecycle_events_to_session(task_id);
let Some(session) = self.os.vm_sessions.for_task_mut(task_id) else {
return Some(CrashReport::VmPanic {
message: format!("task {:?} has no VM session", task_id),
pc: None,
});
};
session.runtime.tick(
&mut self.os.log_service,
&mut self.os.fs,
&mut self.os.fs_state,
&mut session.fs_state,
&mut self.os.memcard,
&mut self.os.open_files,
&mut self.os.next_handle,
vm,
&mut session.open_files,
&mut session.next_handle,
&mut session.vm,
signals,
platform,
)
}
pub fn reset(&mut self, vm: &mut VirtualMachine) {
self.os.vm_runtime.reset(vm);
fn deliver_pending_game_lifecycle_events_to_session(&mut self, task_id: TaskId) {
let Some(session) = self.os.vm_sessions.for_task(task_id) else {
return;
};
if session.runtime.current_cartridge_app_mode != AppMode::Game {
return;
}
pub fn debug_step_instruction(
let mut retained = Vec::new();
let mut delivered = Vec::new();
for event in self.os.game_lifecycle_events.drain(..) {
if event.task_id == task_id {
delivered.push(event);
} else {
retained.push(event);
}
}
self.os.game_lifecycle_events = retained;
if let Some(session) = self.os.vm_sessions.for_task_mut(task_id) {
session.runtime.deliver_game_lifecycle_events(delivered);
}
}
pub fn reset_global_runtime_state(&mut self) {
self.os.clear_cartridge_service_state();
self.os.vm_runtime.clear_cartridge_state();
}
pub fn debug_step_active_session(
&mut self,
vm: &mut VirtualMachine,
platform: &mut dyn RuntimePlatform,
) -> Option<CrashReport> {
self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, platform)
let Some(task_id) = self.active_session_task_id() else {
return Some(CrashReport::VmPanic {
message: "debug step requested without an active VM session".to_string(),
pc: None,
});
};
let Some(session) = self.os.vm_sessions.for_task_mut(task_id) else {
return Some(CrashReport::VmPanic {
message: format!("active debug task {:?} has no VM session", task_id),
pc: None,
});
};
session.runtime.debug_step_instruction(&mut self.os.log_service, &mut session.vm, platform)
}
pub fn active_pc(&self) -> Option<usize> {
self.active_session().map(|session| session.vm.pc())
}
pub fn active_operand_stack_top(&self, n: usize) -> Vec<Value> {
self.active_session().map(|session| session.vm.operand_stack_top(n)).unwrap_or_default()
}
pub fn insert_active_breakpoint(&mut self, pc: usize) -> bool {
if let Some(session) = self.active_session_mut() {
session.vm.insert_breakpoint(pc);
return true;
}
false
}
pub fn remove_active_breakpoint(&mut self, pc: usize) -> bool {
if let Some(session) = self.active_session_mut() {
session.vm.remove_breakpoint(pc);
return true;
}
false
}
pub fn active_breakpoints_list(&self) -> Vec<usize> {
self.active_session().map(|session| session.vm.breakpoints_list()).unwrap_or_default()
}
pub fn paused(&self) -> bool {
self.os.vm_runtime.paused
self.active_session()
.map(|session| session.runtime.paused)
.unwrap_or(self.os.vm_runtime.paused)
}
pub fn set_paused(&mut self, paused: bool) {
if let Some(session) = self.active_session_mut() {
session.runtime.paused = paused;
return;
}
self.os.vm_runtime.paused = paused;
}
pub fn set_inspection_active(&mut self, active: bool) {
if let Some(session) = self.active_session_mut() {
session.runtime.inspection_active = active;
return;
}
self.os.vm_runtime.inspection_active = active;
}
pub fn request_debug_step(&mut self) {
if let Some(session) = self.active_session_mut() {
session.runtime.debug_step_request = true;
return;
}
self.os.vm_runtime.debug_step_request = true;
}
pub fn debug_step_requested(&self) -> bool {
self.os.vm_runtime.debug_step_request
self.active_session()
.map(|session| session.runtime.debug_step_request)
.unwrap_or(self.os.vm_runtime.debug_step_request)
}
pub fn logical_frame_active(&self) -> bool {
self.os.vm_runtime.logical_frame_active
self.active_session()
.map(|session| session.runtime.logical_frame_active)
.unwrap_or(self.os.vm_runtime.logical_frame_active)
}
pub fn delivered_game_lifecycle_events(&self) -> &[GameLifecycleEvent] {
self.active_session()
.map(|session| session.runtime.delivered_game_lifecycle_events())
.unwrap_or_else(|| self.os.vm_runtime.delivered_game_lifecycle_events())
}
pub fn take_delivered_game_lifecycle_events(&mut self) -> Vec<GameLifecycleEvent> {
if let Some(session) = self.active_session_mut() {
return session.runtime.take_delivered_game_lifecycle_events();
}
self.os.vm_runtime.take_delivered_game_lifecycle_events()
}
pub fn tick_index(&self) -> u64 {
self.os.vm_runtime.tick_index
self.active_session()
.map(|session| session.runtime.tick_index)
.unwrap_or(self.os.vm_runtime.tick_index)
}
pub fn logical_frame_index(&self) -> u64 {
self.os.vm_runtime.logical_frame_index
self.active_session()
.map(|session| session.runtime.logical_frame_index)
.unwrap_or(self.os.vm_runtime.logical_frame_index)
}
pub fn last_frame_cpu_time_us(&self) -> u64 {
self.os.vm_runtime.last_frame_cpu_time_us
self.active_session()
.map(|session| session.runtime.last_frame_cpu_time_us)
.unwrap_or(self.os.vm_runtime.last_frame_cpu_time_us)
}
pub fn completed_logical_frames(&self) -> u64 {
self.os.vm_runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed).into()
self.active_session()
.map(|session| {
session
.runtime
.atomic_telemetry
.completed_logical_frames
.load(Ordering::Relaxed)
.into()
})
.unwrap_or_else(|| {
self.os
.vm_runtime
.atomic_telemetry
.completed_logical_frames
.load(Ordering::Relaxed)
.into()
})
}
pub fn telemetry_snapshot(&self) -> TelemetryFrame {
self.os.vm_runtime.atomic_telemetry.snapshot()
self.active_session()
.map(|session| session.runtime.atomic_telemetry.snapshot())
.unwrap_or_else(|| self.os.vm_runtime.atomic_telemetry.snapshot())
}
pub fn start_render_worker<B, S>(&mut self, config: RenderWorkerConfig, backend: B, sink: S)
@ -120,27 +249,72 @@ impl<'a> VmFacade<'a> {
self.os.vm_runtime.record_repeated_render_worker_frame(frame_id);
}
pub fn active_render_ownership(&self) -> RenderOwnership {
self.os.vm_runtime.render_manager.active_ownership()
}
pub fn transition_render_owner(&mut self, app_mode: AppMode, app_id: u32) {
self.os.vm_runtime.render_manager.transition_render_owner(app_mode, app_id);
self.os.vm_runtime.sync_render_worker_ownership();
self.os.vm_runtime.render_manager.sync_telemetry(&self.os.vm_runtime.atomic_telemetry);
}
pub fn cert_config(&self) -> &CertificationConfig {
&self.os.vm_runtime.certifier.config
}
pub fn last_crash_report(&self) -> Option<&CrashReport> {
self.os.vm_runtime.last_crash_report.as_ref()
self.active_session()
.and_then(|session| session.runtime.last_crash_report.as_ref())
.or(self.os.vm_runtime.last_crash_report.as_ref())
}
pub fn current_app_id(&self) -> u32 {
self.os.vm_runtime.current_app_id
self.active_session()
.map(|session| session.runtime.current_app_id)
.unwrap_or(self.os.vm_runtime.current_app_id)
}
pub fn current_cartridge_title(&self) -> String {
self.os.vm_runtime.current_cartridge_title.clone()
self.active_session()
.map(|session| session.runtime.current_cartridge_title.clone())
.unwrap_or_else(|| self.os.vm_runtime.current_cartridge_title.clone())
}
pub fn current_cartridge_app_version(&self) -> String {
self.os.vm_runtime.current_cartridge_app_version.clone()
self.active_session()
.map(|session| session.runtime.current_cartridge_app_version.clone())
.unwrap_or_else(|| self.os.vm_runtime.current_cartridge_app_version.clone())
}
pub fn current_cartridge_app_mode(&self) -> AppMode {
self.os.vm_runtime.current_cartridge_app_mode
self.active_session()
.map(|session| session.runtime.current_cartridge_app_mode)
.unwrap_or(self.os.vm_runtime.current_cartridge_app_mode)
}
fn active_session(&self) -> Option<&VmSession> {
self.os
.task_manager
.foreground_task()
.and_then(|task_id| self.os.vm_sessions.for_task(task_id))
.or_else(|| {
self.os
.foreground_stack
.resident_game_task()
.and_then(|task_id| self.os.vm_sessions.for_task(task_id))
})
}
fn active_session_mut(&mut self) -> Option<&mut VmSession> {
let task_id = self.active_session_task_id()?;
self.os.vm_sessions.for_task_mut(task_id)
}
fn active_session_task_id(&self) -> Option<TaskId> {
self.os
.task_manager
.foreground_task()
.or_else(|| self.os.foreground_stack.resident_game_task())
}
}

View File

@ -1,6 +1,20 @@
use crate::process::ProcessId;
use crate::task::{TaskId, TaskState};
pub const DEFAULT_GAME_PAUSE_BUDGET_TICKS: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameLifecycleEventKind {
Pause,
ResumeForeground,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GameLifecycleEvent {
pub task_id: TaskId,
pub kind: GameLifecycleEventKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleOperation {
SetForeground,
@ -15,4 +29,7 @@ pub enum LifecycleError {
TaskNotFound(TaskId),
ProcessNotFound(ProcessId),
InvalidTransition { task_id: TaskId, from: TaskState, operation: LifecycleOperation },
ResidentGameAlreadyExists { existing: TaskId, requested: TaskId },
ForegroundShellAlreadyExists { existing: TaskId, requested: TaskId },
TaskIsNotResidentGame(TaskId),
}

View File

@ -2,6 +2,11 @@ mod facades;
mod lifecycle;
mod system_os;
pub use facades::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade};
pub use lifecycle::{LifecycleError, LifecycleOperation};
pub use facades::{
FsFacade, LifecycleFacade, SessionError, SessionsFacade, VmFacade, WindowFacade,
};
pub use lifecycle::{
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
LifecycleOperation,
};
pub use system_os::SystemOS;

View File

@ -1,8 +1,12 @@
use crate::VirtualMachineRuntime;
use crate::fs::{FsState, VirtualFS};
use crate::os::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade};
use crate::os::{
FsFacade, GameLifecycleEvent, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade,
};
use crate::process::ProcessManager;
use crate::services::foreground::ForegroundStack;
use crate::services::memcard::MemcardService;
use crate::services::vm_session::VmSessionRegistry;
use crate::services::windows::WindowManager;
use crate::task::TaskManager;
use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource};
@ -14,6 +18,9 @@ pub struct SystemOS {
pub(super) vm_runtime: VirtualMachineRuntime,
pub(super) process_manager: ProcessManager,
pub(super) task_manager: TaskManager,
pub(super) foreground_stack: ForegroundStack,
pub(super) vm_sessions: VmSessionRegistry,
pub(super) game_lifecycle_events: Vec<GameLifecycleEvent>,
pub(super) window_manager: WindowManager,
pub(super) log_service: LogService,
pub(super) fs: VirtualFS,
@ -33,6 +40,9 @@ impl SystemOS {
),
process_manager: ProcessManager::new(),
task_manager: TaskManager::new(),
foreground_stack: ForegroundStack::new(),
vm_sessions: VmSessionRegistry::new(),
game_lifecycle_events: Vec::new(),
window_manager: WindowManager::new(),
log_service,
fs: VirtualFS::new(),
@ -133,15 +143,33 @@ impl SystemOS {
#[cfg(test)]
mod tests {
use super::*;
use crate::os::{LifecycleError, LifecycleOperation};
use crate::process::ProcessState;
use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError};
use crate::process::{ProcessId, ProcessKind, ProcessState};
use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState};
use crate::task::{TaskId, TaskState};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
use prometeu_hal::syscalls::caps;
fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState {
let task = os.task_manager.get(task_id).expect("task should exist");
os.process_manager.get(task.process_id).expect("process should exist").state
}
fn session_test_cartridge(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge {
Cartridge {
app_id,
title: title.to_string(),
app_version: "1.0.0".to_string(),
app_mode,
capabilities: caps::NONE,
program: vec![],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
}
}
#[test]
fn set_foreground_task_marks_task_and_process_running() {
let mut os = SystemOS::new(None);
@ -157,6 +185,358 @@ mod tests {
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running);
}
#[test]
fn foreground_stack_starts_at_hub() {
let os = SystemOS::new(None);
assert_eq!(os.foreground_stack.owner(), ForegroundOwner::Hub);
assert_eq!(os.foreground_stack.resident_game(), None);
}
#[test]
fn game_session_registers_single_resident_game() {
let mut os = SystemOS::new(None);
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id));
assert_eq!(
os.lifecycle().resident_game(),
Some(ResidentGame { task_id, state: ResidentGameState::Foreground })
);
}
#[test]
fn second_resident_game_is_rejected() {
let mut os = SystemOS::new(None);
let first = os.sessions().create_vm_game_task(42, "Sector Crawl");
let process_id = os.process_manager.spawn_vm_game(43, "Second");
let second = os.task_manager.create_game_task(process_id, 43, "Second");
assert_eq!(
os.lifecycle().set_game_foreground_task(second),
Err(LifecycleError::ResidentGameAlreadyExists { existing: first, requested: second })
);
}
#[test]
fn vm_game_task_creates_vm_session() {
let mut os = SystemOS::new(None);
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
let sessions = os.sessions();
let session = sessions.vm_session_for_task(task_id).expect("VM session should exist");
assert_eq!(session.task_id, task_id);
assert_eq!(session.app_id, 42);
assert_eq!(session.title, "Sector Crawl");
assert_eq!(session.app_mode, AppMode::Game);
assert_eq!(sessions.vm_session_count(), 1);
}
#[test]
fn vm_shell_task_creates_vm_session() {
let mut os = SystemOS::new(None);
let task_id = os.sessions().create_vm_shell_task(7, "Settings");
let sessions = os.sessions();
let session = sessions.vm_session_for_task(task_id).expect("VM session should exist");
assert_eq!(session.task_id, task_id);
assert_eq!(session.app_id, 7);
assert_eq!(session.title, "Settings");
assert_eq!(session.app_mode, AppMode::Shell);
assert_eq!(sessions.vm_session_count(), 1);
}
#[test]
fn native_shell_task_does_not_create_vm_session() {
let mut os = SystemOS::new(None);
let task_id = os.sessions().create_native_shell_task(7, "Settings");
let sessions = os.sessions();
assert!(sessions.vm_session_for_task(task_id).is_none());
assert_eq!(sessions.vm_session_count(), 0);
}
#[test]
fn same_resident_game_app_reuses_existing_vm_session() {
let mut os = SystemOS::new(None);
let first = os.sessions().create_vm_game_task(42, "Sector Crawl");
let second = os.sessions().create_vm_game_task(42, "Sector Crawl");
let sessions = os.sessions();
assert_eq!(second, first);
assert_eq!(
sessions.resident_game_session_for_app(42).map(|session| session.task_id),
Some(first)
);
assert_eq!(sessions.vm_session_count(), 1);
}
#[test]
fn vm_sessions_hold_independent_runtime_state() {
let mut os = SystemOS::new(None);
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
os.lifecycle().request_home_from_game(game).expect("home request should succeed");
os.lifecycle().advance_game_pause_budget(game).expect("budget should expire");
let shell = os.sessions().create_vm_shell_task(7, "Settings");
{
let mut sessions = os.sessions();
let game_session =
sessions.vm_session_for_task_mut(game).expect("game VM session should exist");
game_session.runtime.tick_index = 100;
game_session.open_files.insert(1, "/game.dat".to_string());
}
{
let mut sessions = os.sessions();
let shell_session =
sessions.vm_session_for_task_mut(shell).expect("shell VM session should exist");
shell_session.runtime.tick_index = 5;
shell_session.open_files.insert(1, "/shell.dat".to_string());
}
let sessions = os.sessions();
let game_session =
sessions.vm_session_for_task(game).expect("game VM session should exist");
let shell_session =
sessions.vm_session_for_task(shell).expect("shell VM session should exist");
assert_eq!(game_session.runtime.tick_index, 100);
assert_eq!(shell_session.runtime.tick_index, 5);
assert_eq!(game_session.open_files.get(&1).map(String::as_str), Some("/game.dat"));
assert_eq!(shell_session.open_files.get(&1).map(String::as_str), Some("/shell.dat"));
}
#[test]
fn vm_debug_operations_target_foreground_shell_before_resident_game() {
let mut os = SystemOS::new(None);
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
os.lifecycle().request_home_from_game(game).expect("home request should succeed");
os.lifecycle().advance_game_pause_budget(game).expect("budget should expire");
let shell = os.sessions().create_vm_shell_task(7, "Settings");
{
let mut sessions = os.sessions();
let game_session =
sessions.vm_session_for_task_mut(game).expect("game VM session should exist");
game_session.vm.insert_breakpoint(10);
}
assert!(os.vm().insert_active_breakpoint(20));
assert_eq!(os.vm().active_breakpoints_list(), vec![20]);
let sessions = os.sessions();
assert_eq!(
sessions
.vm_session_for_task(game)
.expect("game VM session should exist")
.vm
.breakpoints_list(),
vec![10]
);
assert_eq!(
sessions
.vm_session_for_task(shell)
.expect("shell VM session should exist")
.vm
.breakpoints_list(),
vec![20]
);
}
#[test]
fn home_request_notifies_game_before_budget_suspends_it() {
let mut os = SystemOS::new(None);
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
os.lifecycle().request_home_from_game(task_id).expect("home request should succeed");
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id));
assert_eq!(
os.lifecycle().resident_game(),
Some(ResidentGame {
task_id,
state: ResidentGameState::PauseRequested { remaining_ticks: 1 },
})
);
assert_eq!(
os.lifecycle().pending_game_lifecycle_events()[0].kind,
GameLifecycleEventKind::Pause
);
assert_eq!(
os.task_manager.get(task_id).expect("task should exist").state,
TaskState::Foreground
);
assert!(os.lifecycle().advance_game_pause_budget(task_id).expect("budget should advance"));
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
assert_eq!(
os.lifecycle().resident_game(),
Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended })
);
assert_eq!(
os.task_manager.get(task_id).expect("task should exist").state,
TaskState::Suspended
);
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended);
}
#[test]
fn shell_foreground_keeps_game_resident_and_marks_it_suspended() {
let mut os = SystemOS::new(None);
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
os.lifecycle().request_home_from_game(game).expect("home request should succeed");
os.lifecycle().advance_game_pause_budget(game).expect("budget should expire");
let shell = os.sessions().create_vm_shell_task(7, "Settings");
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell));
assert_eq!(
os.lifecycle().resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
);
}
#[test]
fn vm_shell_foreground_suspends_foreground_resident_game() {
let mut os = SystemOS::new(None);
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
let shell = os.sessions().create_vm_shell_task(7, "Settings");
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell));
assert_eq!(
os.lifecycle().resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
);
assert_eq!(
os.task_manager.get(game).expect("game should exist").state,
TaskState::Suspended
);
assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended);
}
#[test]
fn native_shell_foreground_suspends_foreground_resident_game() {
let mut os = SystemOS::new(None);
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
let shell = os.sessions().create_native_shell_task(7, "Settings");
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell));
assert_eq!(
os.lifecycle().resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
);
assert_eq!(
os.task_manager.get(game).expect("game should exist").state,
TaskState::Suspended
);
assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended);
}
#[test]
fn lifecycle_exposes_process_kind_for_shell_tasks() {
let mut os = SystemOS::new(None);
let vm_shell = os.sessions().create_vm_shell_task(7, "Settings");
os.lifecycle().close_task(vm_shell).expect("vm shell should close");
let native_shell = os.sessions().create_native_shell_task(8, "Files");
assert_eq!(os.lifecycle().process_kind_for_task(vm_shell), Ok(ProcessKind::VmShell));
assert_eq!(
os.lifecycle().process_kind_for_task(native_shell),
Ok(ProcessKind::NativeShell)
);
}
#[test]
fn second_foreground_shell_is_rejected() {
let mut os = SystemOS::new(None);
let first = os.sessions().create_vm_shell_task(7, "Settings");
let process_id = os.process_manager.spawn_vm_shell(8, "Files");
let second = os.task_manager.create_shell_task(process_id, 8, "Files");
assert_eq!(
os.lifecycle().set_shell_foreground_task(second),
Err(LifecycleError::ForegroundShellAlreadyExists {
existing: first,
requested: second,
})
);
}
#[test]
fn vm_shell_load_returns_typed_error_when_foreground_shell_exists() {
let mut os = SystemOS::new(None);
let first = os.sessions().create_vm_shell_task(7, "Settings");
let cartridge = session_test_cartridge(AppMode::Shell, 8, "Files");
let error = os.sessions().load_vm_cartridge(&cartridge).expect_err("load should fail");
match error {
SessionError::Lifecycle(LifecycleError::ForegroundShellAlreadyExists {
existing,
requested,
}) => {
assert_eq!(existing, first);
assert_ne!(requested, first);
}
other => panic!("expected foreground shell lifecycle error, got {:?}", other),
}
}
#[test]
fn vm_session_creation_returns_typed_error_for_missing_task() {
let mut os = SystemOS::new(None);
assert_eq!(
os.sessions().try_create_vm_session_for_task(TaskId(999)),
Err(SessionError::TaskNotFound(TaskId(999)))
);
}
#[test]
fn vm_session_creation_returns_typed_error_for_missing_process() {
let mut os = SystemOS::new(None);
let task_id = os.task_manager.create_game_task(ProcessId(999), 42, "Broken");
assert_eq!(
os.sessions().try_create_vm_session_for_task(task_id),
Err(SessionError::ProcessNotFound(ProcessId(999)))
);
}
#[test]
fn session_cartridge_initialization_returns_typed_error_for_missing_session() {
let mut os = SystemOS::new(None);
let process_id = os.process_manager.spawn_vm_game(42, "Sector Crawl");
let task_id = os.task_manager.create_game_task(process_id, 42, "Sector Crawl");
let cartridge = session_test_cartridge(AppMode::Game, 42, "Sector Crawl");
assert_eq!(
os.sessions().try_initialize_session_cartridge(task_id, &cartridge),
Err(SessionError::MissingVmSession(task_id))
);
}
#[test]
fn closing_foreground_shell_returns_to_hub() {
let mut os = SystemOS::new(None);
let shell = os.sessions().create_vm_shell_task(7, "Settings");
os.lifecycle().close_task(shell).expect("close should succeed");
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
assert_eq!(
os.task_manager.get(shell).expect("shell should remain").state,
TaskState::Closed
);
}
#[test]
fn suspend_task_marks_task_and_process_suspended() {
let mut os = SystemOS::new(None);
@ -184,6 +564,10 @@ mod tests {
TaskState::Foreground
);
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running);
assert_eq!(
os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind),
Some(GameLifecycleEventKind::ResumeForeground)
);
}
#[test]

View File

@ -1,3 +1,4 @@
use crate::task::TaskId;
use crate::{CrashReport, GameLibrary, SystemOS};
use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect;
@ -5,7 +6,6 @@ use prometeu_hal::{
FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform,
ShellUiFramePacket,
};
use prometeu_vm::VirtualMachine;
use std::path::PathBuf;
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 };
@ -174,7 +174,23 @@ impl PrometeuHub {
pub fn update_shell_profile(
&mut self,
os: &mut SystemOS,
vm: &mut VirtualMachine,
platform: &mut dyn RuntimePlatform,
) -> SystemProfileUpdate {
let mut action = self.gui_update(os, platform);
if os.windows().focused_window().is_some() && platform.input().pad().start().down {
action = Some(SystemProfileAction::CloseShell);
}
self.render(os, platform);
SystemProfileUpdate { crash: None, action }
}
pub fn update_vm_shell_profile(
&mut self,
os: &mut SystemOS,
task_id: TaskId,
signals: &InputSignals,
platform: &mut dyn RuntimePlatform,
) -> SystemProfileUpdate {
@ -185,7 +201,7 @@ impl PrometeuHub {
if platform.input().pad().start().down {
action = Some(SystemProfileAction::CloseShell);
} else if action != Some(SystemProfileAction::CloseShell) {
crash = os.vm().tick(vm, signals, platform);
crash = os.vm().tick_session(task_id, signals, platform);
}
}
@ -193,6 +209,22 @@ impl PrometeuHub {
SystemProfileUpdate { crash, action }
}
pub fn update_native_shell_profile(
&mut self,
os: &mut SystemOS,
platform: &mut dyn RuntimePlatform,
) -> SystemProfileUpdate {
let mut action = self.gui_update(os, platform);
if os.windows().focused_window().is_some() && platform.input().pad().start().down {
action = Some(SystemProfileAction::CloseShell);
}
self.render(os, platform);
SystemProfileUpdate { crash: None, action }
}
}
fn action_for_click(
@ -398,6 +430,34 @@ fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str
#[cfg(test)]
mod tests {
use super::*;
use crate::task::TaskId;
use crate::windows::WindowOwner;
use prometeu_drivers::TestPlatform;
use prometeu_hal::{InputSignals, RuntimePlatform};
fn focused_native_shell_fixture(
signals: &InputSignals,
) -> (PrometeuHub, SystemOS, TestPlatform) {
let hub = PrometeuHub::new();
let mut os = SystemOS::new(None);
let mut platform = TestPlatform::new();
{
let mut windows = os.windows();
let id = windows.add_window(
"ShellA".to_string(),
WindowOwner::Task(TaskId(7)),
SHELL_FRAME,
Color::GREEN,
);
windows.set_focus(id);
}
platform.input_mut().pad_mut().begin_frame(signals);
platform.input_mut().touch_mut().begin_frame(signals);
(hub, os, platform)
}
#[test]
fn shell_a_button_click_emits_launch_action() {
@ -518,4 +578,35 @@ mod tests {
CLOSE_BUTTON.y + CLOSE_BUTTON.h - 1
));
}
#[test]
fn native_shell_profile_start_close_does_not_tick_vm() {
let signals = InputSignals { start_signal: true, ..Default::default() };
let (mut hub, mut os, mut platform) = focused_native_shell_fixture(&signals);
let tick_index_before_update = os.vm().tick_index();
let outcome = hub.update_native_shell_profile(&mut os, &mut platform);
assert_eq!(outcome.action, Some(SystemProfileAction::CloseShell));
assert!(outcome.crash.is_none());
assert_eq!(os.vm().tick_index(), tick_index_before_update);
}
#[test]
fn native_shell_profile_close_button_does_not_tick_vm() {
let signals = InputSignals {
f_signal: true,
x_pos: CLOSE_BUTTON.x,
y_pos: CLOSE_BUTTON.y,
..Default::default()
};
let (mut hub, mut os, mut platform) = focused_native_shell_fixture(&signals);
let tick_index_before_update = os.vm().tick_index();
let outcome = hub.update_native_shell_profile(&mut os, &mut platform);
assert_eq!(outcome.action, Some(SystemProfileAction::CloseShell));
assert!(outcome.crash.is_none());
assert_eq!(os.vm().tick_index(), tick_index_before_update);
}
}

View File

@ -0,0 +1,330 @@
use crate::task::TaskId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForegroundOwner {
Hub,
Game(TaskId),
Shell(TaskId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResidentGameState {
Foreground,
PauseRequested { remaining_ticks: u8 },
ResumeRequested,
PausedSuspended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResidentGame {
pub task_id: TaskId,
pub state: ResidentGameState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForegroundStackError {
ResidentGameAlreadyExists { existing: TaskId, requested: TaskId },
ForegroundShellAlreadyExists { existing: TaskId, requested: TaskId },
TaskIsNotResidentGame(TaskId),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForegroundStack {
owner: ForegroundOwner,
resident_game: Option<ResidentGame>,
}
impl Default for ForegroundStack {
fn default() -> Self {
Self { owner: ForegroundOwner::Hub, resident_game: None }
}
}
impl ForegroundStack {
pub fn new() -> Self {
Self::default()
}
pub fn owner(&self) -> ForegroundOwner {
self.owner
}
pub fn resident_game(&self) -> Option<ResidentGame> {
self.resident_game
}
pub fn resident_game_task(&self) -> Option<TaskId> {
self.resident_game.map(|game| game.task_id)
}
pub fn set_game_foreground(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> {
if let Some(game) = self.resident_game
&& game.task_id != task_id
{
return Err(ForegroundStackError::ResidentGameAlreadyExists {
existing: game.task_id,
requested: task_id,
});
}
self.resident_game = Some(ResidentGame { task_id, state: ResidentGameState::Foreground });
self.owner = ForegroundOwner::Game(task_id);
Ok(())
}
pub fn request_home_from_game(
&mut self,
task_id: TaskId,
pause_budget_ticks: u8,
) -> Result<(), ForegroundStackError> {
match self.resident_game {
Some(game) if game.task_id == task_id => {
if !matches!(
game.state,
ResidentGameState::PauseRequested { .. } | ResidentGameState::PausedSuspended
) {
self.resident_game = Some(ResidentGame {
task_id,
state: ResidentGameState::PauseRequested {
remaining_ticks: pause_budget_ticks.max(1),
},
});
}
Ok(())
}
_ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)),
}
}
pub fn advance_pause_budget(&mut self, task_id: TaskId) -> Result<bool, ForegroundStackError> {
match self.resident_game {
Some(game)
if game.task_id == task_id
&& matches!(
game.state,
ResidentGameState::PauseRequested { remaining_ticks: 1 }
) =>
{
self.resident_game =
Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended });
self.owner = ForegroundOwner::Hub;
Ok(true)
}
Some(game)
if game.task_id == task_id
&& matches!(game.state, ResidentGameState::PauseRequested { .. }) =>
{
let ResidentGameState::PauseRequested { remaining_ticks } = game.state else {
unreachable!();
};
self.resident_game = Some(ResidentGame {
task_id,
state: ResidentGameState::PauseRequested {
remaining_ticks: remaining_ticks.saturating_sub(1).max(1),
},
});
Ok(false)
}
Some(game) if game.task_id == task_id => Ok(false),
_ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)),
}
}
pub fn request_resume_game(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> {
match self.resident_game {
Some(game) if game.task_id == task_id => {
self.resident_game =
Some(ResidentGame { task_id, state: ResidentGameState::ResumeRequested });
Ok(())
}
_ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)),
}
}
pub fn complete_resume_game(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> {
match self.resident_game {
Some(game) if game.task_id == task_id => {
self.resident_game =
Some(ResidentGame { task_id, state: ResidentGameState::Foreground });
self.owner = ForegroundOwner::Game(task_id);
Ok(())
}
_ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)),
}
}
pub fn set_shell_foreground(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> {
if let ForegroundOwner::Shell(existing) = self.owner
&& existing != task_id
{
return Err(ForegroundStackError::ForegroundShellAlreadyExists {
existing,
requested: task_id,
});
}
if let Some(game) = self.resident_game {
self.resident_game = Some(ResidentGame {
task_id: game.task_id,
state: ResidentGameState::PausedSuspended,
});
}
self.owner = ForegroundOwner::Shell(task_id);
Ok(())
}
pub fn return_to_hub(&mut self) {
self.owner = ForegroundOwner::Hub;
}
pub fn clear_resident_game(&mut self, task_id: TaskId) {
if self.resident_game_task() == Some(task_id) {
self.resident_game = None;
}
if self.owner == ForegroundOwner::Game(task_id) {
self.owner = ForegroundOwner::Hub;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn foreground_stack_starts_at_hub() {
let stack = ForegroundStack::new();
assert_eq!(stack.owner(), ForegroundOwner::Hub);
assert_eq!(stack.resident_game(), None);
}
#[test]
fn setting_game_foreground_registers_resident_game() {
let mut stack = ForegroundStack::new();
let game = TaskId(7);
stack.set_game_foreground(game).expect("game should become foreground");
assert_eq!(stack.owner(), ForegroundOwner::Game(game));
assert_eq!(
stack.resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::Foreground })
);
}
#[test]
fn second_resident_game_is_rejected() {
let mut stack = ForegroundStack::new();
stack.set_game_foreground(TaskId(1)).expect("first game should be accepted");
assert_eq!(
stack.set_game_foreground(TaskId(2)),
Err(ForegroundStackError::ResidentGameAlreadyExists {
existing: TaskId(1),
requested: TaskId(2),
})
);
}
#[test]
fn home_request_moves_resident_game_to_pause_requested() {
let mut stack = ForegroundStack::new();
let game = TaskId(3);
stack.set_game_foreground(game).expect("game should be foreground");
stack.request_home_from_game(game, 2).expect("home request should pause game");
assert_eq!(stack.owner(), ForegroundOwner::Game(game));
assert_eq!(
stack.resident_game(),
Some(ResidentGame {
task_id: game,
state: ResidentGameState::PauseRequested { remaining_ticks: 2 },
})
);
}
#[test]
fn pause_budget_expiry_suspends_game_and_returns_to_hub() {
let mut stack = ForegroundStack::new();
let game = TaskId(3);
stack.set_game_foreground(game).expect("game should be foreground");
stack.request_home_from_game(game, 1).expect("home request should pause game");
assert!(stack.advance_pause_budget(game).expect("budget should advance"));
assert_eq!(stack.owner(), ForegroundOwner::Hub);
assert_eq!(
stack.resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
);
}
#[test]
fn pause_budget_decrements_before_expiry() {
let mut stack = ForegroundStack::new();
let game = TaskId(3);
stack.set_game_foreground(game).expect("game should be foreground");
stack.request_home_from_game(game, 2).expect("home request should pause game");
assert!(!stack.advance_pause_budget(game).expect("budget should advance"));
assert_eq!(stack.owner(), ForegroundOwner::Game(game));
assert_eq!(
stack.resident_game(),
Some(ResidentGame {
task_id: game,
state: ResidentGameState::PauseRequested { remaining_ticks: 1 },
})
);
assert!(stack.advance_pause_budget(game).expect("budget should expire"));
assert_eq!(stack.owner(), ForegroundOwner::Hub);
assert_eq!(
stack.resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
);
}
#[test]
fn resume_request_precedes_game_foreground_restore() {
let mut stack = ForegroundStack::new();
let game = TaskId(3);
stack.set_game_foreground(game).expect("game should be foreground");
stack.request_home_from_game(game, 1).expect("home request should pause game");
stack.advance_pause_budget(game).expect("budget should expire");
stack.request_resume_game(game).expect("resume should be requested");
assert_eq!(stack.owner(), ForegroundOwner::Hub);
assert_eq!(
stack.resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::ResumeRequested })
);
stack.complete_resume_game(game).expect("resume should complete");
assert_eq!(stack.owner(), ForegroundOwner::Game(game));
assert_eq!(
stack.resident_game(),
Some(ResidentGame { task_id: game, state: ResidentGameState::Foreground })
);
}
#[test]
fn shell_foreground_keeps_resident_game() {
let mut stack = ForegroundStack::new();
let game = TaskId(3);
let shell = TaskId(4);
stack.set_game_foreground(game).expect("game should be foreground");
stack.request_home_from_game(game, 1).expect("home request should suspend game");
stack.advance_pause_budget(game).expect("budget should expire");
stack.set_shell_foreground(shell).expect("shell should become foreground");
assert_eq!(stack.owner(), ForegroundOwner::Shell(shell));
assert_eq!(stack.resident_game_task(), Some(game));
}
}

View File

@ -1,8 +1,10 @@
pub mod async_work;
pub mod foreground;
pub mod fs;
pub mod game_library;
pub mod memcard;
pub mod process;
pub mod task;
pub mod vm_runtime;
pub mod vm_session;
pub mod windows;

View File

@ -32,6 +32,7 @@ impl VirtualMachineRuntime {
gfx2d_commands: Vec::new(),
gfxui_commands: Vec::new(),
logs_written_this_frame: HashMap::new(),
game_lifecycle_events_delivered: Vec::new(),
atomic_telemetry,
last_crash_report: None,
certifier: Certifier::new(cap_config.unwrap_or_default()),
@ -135,6 +136,7 @@ impl VirtualMachineRuntime {
self.gfx2d_commands.clear();
self.gfxui_commands.clear();
self.logs_written_this_frame.clear();
self.game_lifecycle_events_delivered.clear();
self.last_crash_report = None;
@ -191,6 +193,21 @@ impl VirtualMachineRuntime {
}
}
pub fn deliver_game_lifecycle_events<I>(&mut self, events: I)
where
I: IntoIterator<Item = GameLifecycleEvent>,
{
self.game_lifecycle_events_delivered.extend(events);
}
pub fn delivered_game_lifecycle_events(&self) -> &[GameLifecycleEvent] {
&self.game_lifecycle_events_delivered
}
pub fn take_delivered_game_lifecycle_events(&mut self) -> Vec<GameLifecycleEvent> {
std::mem::take(&mut self.game_lifecycle_events_delivered)
}
pub fn initialize_vm(
&mut self,
log_service: &mut LogService,
@ -224,3 +241,26 @@ impl VirtualMachineRuntime {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::os::{GameLifecycleEvent, GameLifecycleEventKind};
use crate::task::TaskId;
#[test]
fn vm_runtime_records_and_drains_delivered_game_lifecycle_events() {
let mut runtime = VirtualMachineRuntime::new(None);
let pause = GameLifecycleEvent { task_id: TaskId(7), kind: GameLifecycleEventKind::Pause };
let resume = GameLifecycleEvent {
task_id: TaskId(7),
kind: GameLifecycleEventKind::ResumeForeground,
};
runtime.deliver_game_lifecycle_events([pause, resume]);
assert_eq!(runtime.delivered_game_lifecycle_events(), &[pause, resume]);
assert_eq!(runtime.take_delivered_game_lifecycle_events(), vec![pause, resume]);
assert!(runtime.delivered_game_lifecycle_events().is_empty());
}
}

View File

@ -13,6 +13,7 @@ mod tests;
mod tick;
use crate::CrashReport;
use crate::os::GameLifecycleEvent;
pub use frame_scheduler::FrameScheduler;
use prometeu_bytecode::string_materialization_count;
use prometeu_hal::app_mode::AppMode;
@ -49,6 +50,7 @@ pub struct VirtualMachineRuntime {
pub gfx2d_commands: Vec<Gfx2dCommand>,
pub gfxui_commands: Vec<GfxUiCommand>,
pub logs_written_this_frame: HashMap<u32, u32>,
pub game_lifecycle_events_delivered: Vec<GameLifecycleEvent>,
pub atomic_telemetry: Arc<AtomicTelemetry>,
pub last_crash_report: Option<CrashReport>,
pub certifier: Certifier,

View File

@ -0,0 +1,281 @@
use crate::VirtualMachineRuntime;
use crate::fs::FsState;
use crate::process::{Process, ProcessId, ProcessKind};
use crate::task::{Task, TaskId, TaskKind};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_vm::VirtualMachine;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VmSessionId(pub TaskId);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmSessionError {
SessionAlreadyExists(TaskId),
TaskProcessMismatch { task_id: TaskId, task_process: ProcessId, process_id: ProcessId },
TaskKindMismatch { task_id: TaskId, task_kind: TaskKind, process_kind: ProcessKind },
ProcessIsNotVmBacked(ProcessId),
}
pub struct VmSession {
pub id: VmSessionId,
pub task_id: TaskId,
pub process_id: ProcessId,
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub vm: VirtualMachine,
pub runtime: VirtualMachineRuntime,
pub fs_state: FsState,
pub open_files: HashMap<u32, String>,
pub next_handle: u32,
}
impl VmSession {
fn new(
task: &Task,
process: &Process,
app_mode: AppMode,
app_version: impl Into<String>,
cap_config: Option<CertificationConfig>,
logs_count: Arc<AtomicU32>,
) -> Self {
Self {
id: VmSessionId(task.id),
task_id: task.id,
process_id: process.id,
app_id: task.app_id,
title: task.title.clone(),
app_version: app_version.into(),
app_mode,
vm: VirtualMachine::default(),
runtime: VirtualMachineRuntime::new_with_log_counter(cap_config, logs_count),
fs_state: FsState::Unmounted,
open_files: HashMap::new(),
next_handle: 1,
}
}
pub fn clear_cartridge_service_state(&mut self) {
self.open_files.clear();
self.next_handle = 1;
}
}
#[derive(Default)]
pub struct VmSessionRegistry {
sessions: HashMap<VmSessionId, VmSession>,
}
impl VmSessionRegistry {
pub fn new() -> Self {
Self { sessions: HashMap::new() }
}
pub fn create_for_task(
&mut self,
task: &Task,
process: &Process,
app_version: impl Into<String>,
cap_config: Option<CertificationConfig>,
logs_count: Arc<AtomicU32>,
) -> Result<VmSessionId, VmSessionError> {
if task.process_id != process.id {
return Err(VmSessionError::TaskProcessMismatch {
task_id: task.id,
task_process: task.process_id,
process_id: process.id,
});
}
let app_mode = app_mode_for_task_and_process(task, process)?;
let id = VmSessionId(task.id);
if self.sessions.contains_key(&id) {
return Err(VmSessionError::SessionAlreadyExists(task.id));
}
self.sessions.insert(
id,
VmSession::new(task, process, app_mode, app_version, cap_config, logs_count),
);
Ok(id)
}
pub fn get(&self, id: VmSessionId) -> Option<&VmSession> {
self.sessions.get(&id)
}
pub fn get_mut(&mut self, id: VmSessionId) -> Option<&mut VmSession> {
self.sessions.get_mut(&id)
}
pub fn for_task(&self, task_id: TaskId) -> Option<&VmSession> {
self.get(VmSessionId(task_id))
}
pub fn for_task_mut(&mut self, task_id: TaskId) -> Option<&mut VmSession> {
self.get_mut(VmSessionId(task_id))
}
pub fn resident_game(&self) -> Option<&VmSession> {
self.sessions.values().find(|session| session.app_mode == AppMode::Game)
}
pub fn resident_game_for_app(&self, app_id: u32) -> Option<&VmSession> {
self.sessions
.values()
.find(|session| session.app_mode == AppMode::Game && session.app_id == app_id)
}
pub fn len(&self) -> usize {
self.sessions.len()
}
pub fn is_empty(&self) -> bool {
self.sessions.is_empty()
}
pub fn clear(&mut self) {
self.sessions.clear();
}
}
fn app_mode_for_task_and_process(
task: &Task,
process: &Process,
) -> Result<AppMode, VmSessionError> {
match (task.kind, process.kind) {
(TaskKind::Game, ProcessKind::VmGame) => Ok(AppMode::Game),
(TaskKind::Shell, ProcessKind::VmShell) => Ok(AppMode::Shell),
(_, ProcessKind::NativeShell) => Err(VmSessionError::ProcessIsNotVmBacked(process.id)),
_ => Err(VmSessionError::TaskKindMismatch {
task_id: task.id,
task_kind: task.kind,
process_kind: process.kind,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::process::Process;
use crate::task::Task;
#[test]
fn creates_vm_game_session_for_vm_game_task() {
let process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame);
let task = Task::new(TaskId(7), process.id, 42, "Stress", TaskKind::Game);
let mut registry = VmSessionRegistry::new();
let id = registry
.create_for_task(&task, &process, "", None, Arc::new(AtomicU32::new(0)))
.expect("session should create");
let session = registry.get(id).expect("session should exist");
assert_eq!(session.id, VmSessionId(task.id));
assert_eq!(session.task_id, task.id);
assert_eq!(session.process_id, process.id);
assert_eq!(session.app_id, 42);
assert_eq!(session.title, "Stress");
assert_eq!(session.app_mode, AppMode::Game);
assert_eq!(session.runtime.current_app_id, 0);
assert_eq!(session.fs_state, FsState::Unmounted);
assert_eq!(session.next_handle, 1);
}
#[test]
fn creates_vm_shell_session_for_vm_shell_task() {
let process = Process::new(ProcessId(1), 7, "Shell", ProcessKind::VmShell);
let task = Task::new(TaskId(3), process.id, 7, "Shell", TaskKind::Shell);
let mut registry = VmSessionRegistry::new();
let id = registry
.create_for_task(&task, &process, "1.0", None, Arc::new(AtomicU32::new(0)))
.expect("session should create");
let session = registry.get(id).expect("session should exist");
assert_eq!(session.app_mode, AppMode::Shell);
assert_eq!(session.app_version, "1.0");
}
#[test]
fn rejects_native_shell_process() {
let process = Process::new(ProcessId(1), 7, "Shell", ProcessKind::NativeShell);
let task = Task::new(TaskId(3), process.id, 7, "Shell", TaskKind::Shell);
let mut registry = VmSessionRegistry::new();
assert_eq!(
registry.create_for_task(&task, &process, "", None, Arc::new(AtomicU32::new(0))),
Err(VmSessionError::ProcessIsNotVmBacked(process.id))
);
}
#[test]
fn finds_resident_game_by_app_id() {
let process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame);
let task = Task::new(TaskId(7), process.id, 42, "Stress", TaskKind::Game);
let mut registry = VmSessionRegistry::new();
registry
.create_for_task(&task, &process, "", None, Arc::new(AtomicU32::new(0)))
.expect("session should create");
assert_eq!(
registry.resident_game_for_app(42).map(|session| session.task_id),
Some(task.id)
);
assert_eq!(registry.resident_game_for_app(43).map(|session| session.task_id), None);
}
#[test]
fn session_runtime_state_is_independent_per_session() {
let game_process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame);
let game_task = Task::new(TaskId(7), game_process.id, 42, "Stress", TaskKind::Game);
let shell_process = Process::new(ProcessId(2), 9, "Shell", ProcessKind::VmShell);
let shell_task = Task::new(TaskId(8), shell_process.id, 9, "Shell", TaskKind::Shell);
let mut registry = VmSessionRegistry::new();
registry
.create_for_task(&game_task, &game_process, "", None, Arc::new(AtomicU32::new(0)))
.expect("game session should create");
registry
.create_for_task(&shell_task, &shell_process, "", None, Arc::new(AtomicU32::new(0)))
.expect("shell session should create");
registry
.for_task_mut(game_task.id)
.expect("game session should exist")
.runtime
.tick_index = 11;
registry
.for_task_mut(shell_task.id)
.expect("shell session should exist")
.runtime
.tick_index = 3;
registry
.for_task_mut(game_task.id)
.expect("game session should exist")
.open_files
.insert(1, "/game.save".to_string());
assert_eq!(
registry.for_task(game_task.id).map(|session| session.runtime.tick_index),
Some(11)
);
assert_eq!(
registry.for_task(shell_task.id).map(|session| session.runtime.tick_index),
Some(3)
);
assert!(
registry
.for_task(shell_task.id)
.expect("shell session should exist")
.open_files
.is_empty()
);
}
}

View File

@ -28,6 +28,8 @@ pub struct HostDebugger {
last_telemetry_frame: u64,
/// Last fault summary sent to the debugger client.
last_fault_summary: Option<String>,
/// Debug cartridge metadata captured before the VM session exists.
debug_cartridge: Option<HandshakeCartridge>,
}
impl Default for HostDebugger {
@ -46,19 +48,25 @@ impl HostDebugger {
last_log_seq: 0,
last_telemetry_frame: 0,
last_fault_summary: None,
debug_cartridge: None,
}
}
/// Configures the debugger based on the boot target.
/// If debug mode is enabled, it binds to the specified TCP port.
pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) {
pub fn setup_boot_target(&mut self, boot_target: &BootTarget, _firmware: &mut Firmware) {
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
self.waiting_for_start = true;
// Pre-load cartridge metadata so the Handshake message can contain
// valid information about the App being debugged.
// valid information before the VM session is created.
if let Ok(cartridge) = CartridgeLoader::load(path) {
let _ = firmware.os.vm().initialize(&mut firmware.vm, &cartridge);
self.debug_cartridge = Some(HandshakeCartridge {
app_id: cartridge.app_id,
title: cartridge.title,
app_version: cartridge.app_version,
app_mode: cartridge.app_mode,
});
}
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
@ -116,12 +124,7 @@ impl HostDebugger {
let handshake = DebugResponse::Handshake {
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
runtime_version: "0.1".to_string(),
cartridge: HandshakeCartridge {
app_id: firmware.os.vm().current_app_id(),
title: firmware.os.vm().current_cartridge_title(),
app_version: firmware.os.vm().current_cartridge_app_version(),
app_mode: firmware.os.vm().current_cartridge_app_mode(),
},
cartridge: self.handshake_cartridge(firmware),
};
self.send_response(handshake);
} else {
@ -201,7 +204,7 @@ impl HostDebugger {
DebugCommand::Step => {
// Execute exactly one instruction and keep paused.
firmware.os.vm().set_paused(true);
let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, platform);
let _ = firmware.os.vm().debug_step_active_session(platform);
}
DebugCommand::StepFrame => {
// Execute until the end of the current logical frame.
@ -210,10 +213,10 @@ impl HostDebugger {
}
DebugCommand::GetState => {
// Return detailed VM register and stack state.
let stack_top = firmware.vm.operand_stack_top(10);
let stack_top = firmware.os.vm().active_operand_stack_top(10);
let resp = DebugResponse::GetState {
pc: firmware.vm.pc(),
pc: firmware.os.vm().active_pc().unwrap_or(0),
stack_top,
frame_index: firmware.os.vm().logical_frame_index(),
app_id: firmware.os.vm().current_app_id(),
@ -221,18 +224,37 @@ impl HostDebugger {
self.send_response(resp);
}
DebugCommand::SetBreakpoint { pc } => {
firmware.vm.insert_breakpoint(pc);
let _ = firmware.os.vm().insert_active_breakpoint(pc);
}
DebugCommand::ListBreakpoints => {
let pcs = firmware.vm.breakpoints_list();
let pcs = firmware.os.vm().active_breakpoints_list();
self.send_response(DebugResponse::Breakpoints { pcs });
}
DebugCommand::ClearBreakpoint { pc } => {
firmware.vm.remove_breakpoint(pc);
let _ = firmware.os.vm().remove_active_breakpoint(pc);
}
}
}
fn handshake_cartridge(&self, firmware: &mut Firmware) -> HandshakeCartridge {
let app_id = firmware.os.vm().current_app_id();
if app_id != 0 {
return HandshakeCartridge {
app_id,
title: firmware.os.vm().current_cartridge_title(),
app_version: firmware.os.vm().current_cartridge_app_version(),
app_mode: firmware.os.vm().current_cartridge_app_mode(),
};
}
self.debug_cartridge.clone().unwrap_or_else(|| HandshakeCartridge {
app_id,
title: firmware.os.vm().current_cartridge_title(),
app_version: firmware.os.vm().current_cartridge_app_version(),
app_mode: firmware.os.vm().current_cartridge_app_mode(),
})
}
pub(crate) fn cert_event_from_snapshot(
tag: u16,
telemetry: TelemetryFrame,
@ -315,7 +337,7 @@ impl HostDebugger {
// Map specific internal log tags to protocol events.
if event.tag == 0xDEB1 {
self.send_event(DebugEvent::BreakpointHit {
pc: firmware.vm.pc(),
pc: firmware.os.vm().active_pc().unwrap_or(0),
frame_index: firmware.os.vm().logical_frame_index(),
});
}
@ -409,6 +431,52 @@ mod tests {
assert!(firmware.os.vm().debug_step_requested());
}
#[test]
fn breakpoint_commands_mutate_active_vm_session() {
let mut debugger = HostDebugger::new();
let mut firmware = Firmware::new(None);
let mut platform = TestPlatform::new();
let task_id = firmware
.os
.sessions()
.try_create_vm_game_task(42, "Sector Crawl")
.expect("debugger test should create a game session");
debugger.handle_command(
DebugCommand::SetBreakpoint { pc: 123 },
&mut firmware,
&mut platform,
);
assert_eq!(
firmware
.os
.sessions()
.vm_session_for_task(task_id)
.expect("game VM session should exist")
.vm
.breakpoints_list(),
vec![123]
);
debugger.handle_command(
DebugCommand::ClearBreakpoint { pc: 123 },
&mut firmware,
&mut platform,
);
assert!(
firmware
.os
.sessions()
.vm_session_for_task(task_id)
.expect("game VM session should exist")
.vm
.breakpoints_list()
.is_empty()
);
}
#[test]
fn handle_command_start_leaves_waiting_for_start_mode() {
let mut debugger = HostDebugger::new();

View File

@ -43,9 +43,27 @@ impl FbViewport {
pub struct HostInputHandler {
pub signals: InputSignals,
system_controls: HostSystemControls,
display_size: (usize, usize),
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HostSystemControls {
home_requested: bool,
}
impl HostSystemControls {
pub fn request_home(&mut self) {
self.home_requested = true;
}
pub fn take_home_requested(&mut self) -> bool {
let requested = self.home_requested;
self.home_requested = false;
requested
}
}
impl Default for HostInputHandler {
fn default() -> Self {
Self::new((480, 270))
@ -54,16 +72,27 @@ impl Default for HostInputHandler {
impl HostInputHandler {
pub fn new(display_size: (usize, usize)) -> Self {
Self { signals: InputSignals::default(), display_size }
Self {
signals: InputSignals::default(),
system_controls: HostSystemControls::default(),
display_size,
}
}
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
match event {
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
let is_down = event.state == ElementState::Pressed;
pub fn take_home_requested(&mut self) -> bool {
self.system_controls.take_home_requested()
}
pub fn clear_guest_signals(&mut self) {
self.signals = InputSignals::default();
}
pub fn handle_key_code(&mut self, code: KeyCode, state: ElementState) {
let is_down = state == ElementState::Pressed;
match code {
KeyCode::Escape | KeyCode::Home if is_down => self.system_controls.request_home(),
KeyCode::ArrowUp => self.signals.up_signal = is_down,
KeyCode::ArrowDown => self.signals.down_signal = is_down,
KeyCode::ArrowLeft => self.signals.left_signal = is_down,
@ -77,13 +106,18 @@ impl HostInputHandler {
KeyCode::KeyE => self.signals.r_signal = is_down,
KeyCode::KeyZ => self.signals.start_signal = is_down,
KeyCode::ShiftLeft | KeyCode::ShiftRight => {
self.signals.select_signal = is_down
}
KeyCode::ShiftLeft | KeyCode::ShiftRight => self.signals.select_signal = is_down,
_ => {}
}
}
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
match event {
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
self.handle_key_code(code, event.state);
}
}
WindowEvent::CursorMoved { position, .. } => {
@ -134,3 +168,81 @@ impl HostInputHandler {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_no_pad_signal(signals: InputSignals) {
assert!(!signals.up_signal);
assert!(!signals.down_signal);
assert!(!signals.left_signal);
assert!(!signals.right_signal);
assert!(!signals.a_signal);
assert!(!signals.b_signal);
assert!(!signals.x_signal);
assert!(!signals.y_signal);
assert!(!signals.l_signal);
assert!(!signals.r_signal);
assert!(!signals.start_signal);
assert!(!signals.select_signal);
assert!(!signals.f_signal);
}
#[test]
fn escape_requests_home_without_mutating_guest_pad() {
let mut input = HostInputHandler::default();
input.handle_key_code(KeyCode::Escape, ElementState::Pressed);
assert!(input.take_home_requested());
assert!(!input.take_home_requested());
assert_no_pad_signal(input.signals);
}
#[test]
fn physical_home_requests_home_without_mutating_guest_pad() {
let mut input = HostInputHandler::default();
input.handle_key_code(KeyCode::Home, ElementState::Pressed);
assert!(input.take_home_requested());
assert_no_pad_signal(input.signals);
}
#[test]
fn home_release_does_not_emit_a_new_home_request() {
let mut input = HostInputHandler::default();
input.handle_key_code(KeyCode::Home, ElementState::Released);
assert!(!input.take_home_requested());
assert_no_pad_signal(input.signals);
}
#[test]
fn existing_keyboard_pad_mapping_is_preserved() {
let mut input = HostInputHandler::default();
input.handle_key_code(KeyCode::KeyZ, ElementState::Pressed);
input.handle_key_code(KeyCode::ShiftLeft, ElementState::Pressed);
input.handle_key_code(KeyCode::ArrowUp, ElementState::Pressed);
assert!(input.signals.start_signal);
assert!(input.signals.select_signal);
assert!(input.signals.up_signal);
assert!(!input.take_home_requested());
}
#[test]
fn clearing_guest_signals_preserves_system_home_control() {
let mut input = HostInputHandler::default();
input.handle_key_code(KeyCode::ArrowUp, ElementState::Pressed);
input.handle_key_code(KeyCode::Escape, ElementState::Pressed);
input.clear_guest_signals();
assert_no_pad_signal(input.signals);
assert!(input.take_home_requested());
}
}

View File

@ -10,8 +10,8 @@ use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks};
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
use prometeu_hal::RuntimePlatform;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{OwnedRgba8888Frame, RenderOwnership, RuntimePlatform};
use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root};
use std::sync::Arc;
use std::time::{Duration, Instant};
@ -88,8 +88,12 @@ fn desired_control_flow(
}
}
fn should_present_worker_frame(state: &FirmwareState) -> bool {
matches!(state, FirmwareState::GameRunning(_))
fn should_present_worker_frame(
state: &FirmwareState,
frame: &OwnedRgba8888Frame,
active_ownership: RenderOwnership,
) -> bool {
matches!(state, FirmwareState::GameRunning(_)) && frame.ownership == active_ownership
}
/// The Desktop implementation of the PROMETEU Runtime.
@ -312,8 +316,13 @@ impl ApplicationHandler for HostRunner {
{
let frame = pixels.frame_mut();
if should_present_worker_frame(&self.firmware.state)
&& let Some(worker_frame) = self.render_frame_store.latest_frame()
let active_ownership = self.firmware.os.vm().active_render_ownership();
if let Some(worker_frame) = self.render_frame_store.latest_frame()
&& should_present_worker_frame(
&self.firmware.state,
&worker_frame,
active_ownership,
)
{
self.firmware
.os
@ -372,6 +381,13 @@ impl ApplicationHandler for HostRunner {
self.firmware.os.fs().mount(Box::new(backend));
}
if self.input.take_home_requested() {
self.input.clear_guest_signals();
self.firmware.request_home_from_host();
self.invalidate_host_surface();
self.request_redraw_if_needed();
}
// 3. Timing Management (The heart of determinism).
// We measure the elapsed time since the last iteration and add it to an
// accumulator. We then execute exactly as many 60Hz slices as the
@ -399,7 +415,10 @@ impl ApplicationHandler for HostRunner {
// Sync pause state with audio.
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
// being cleared by the OS if a new logical frame starts in this tick.
let is_paused = self.firmware.os.vm().paused() || self.debugger.waiting_for_start;
let lifecycle_audio_paused = self.firmware.game_lifecycle_audio_paused();
let is_paused = lifecycle_audio_paused
|| self.firmware.os.vm().paused()
|| self.debugger.waiting_for_start;
if is_paused != self.last_paused_state {
self.last_paused_state = is_paused;
let cmd =
@ -414,8 +433,9 @@ impl ApplicationHandler for HostRunner {
self.stats.record_frame();
}
if should_present_worker_frame(&self.firmware.state)
&& let Some(worker_frame) = self.render_frame_store.latest_frame()
let active_ownership = self.firmware.os.vm().active_render_ownership();
if let Some(worker_frame) = self.render_frame_store.latest_frame()
&& should_present_worker_frame(&self.firmware.state, &worker_frame, active_ownership)
{
self.presentation.note_published_frame(worker_frame.frame_id.get());
} else {
@ -536,14 +556,39 @@ mod tests {
}
#[test]
fn worker_frame_presentation_is_game_only() {
assert!(should_present_worker_frame(&FirmwareState::GameRunning(GameRunningStep::new(
TaskId(1),
))));
assert!(!should_present_worker_frame(&FirmwareState::ShellRunning(ShellRunningStep::new(
TaskId(2)
),)));
assert!(!should_present_worker_frame(&FirmwareState::HubHome(HubHomeStep)));
fn worker_frame_presentation_requires_game_state_and_current_ownership() {
let active = RenderOwnership::new(3, AppMode::Game, 1);
let current = OwnedRgba8888Frame::packed(FrameId::new(9), active, 1, 1, vec![0xFF00FFFF])
.expect("test frame");
let stale = OwnedRgba8888Frame::packed(
FrameId::new(8),
RenderOwnership::new(2, AppMode::Game, 1),
1,
1,
vec![0xFF00FFFF],
)
.expect("test frame");
assert!(should_present_worker_frame(
&FirmwareState::GameRunning(GameRunningStep::new(TaskId(1))),
&current,
active,
));
assert!(!should_present_worker_frame(
&FirmwareState::GameRunning(GameRunningStep::new(TaskId(1),)),
&stale,
active,
));
assert!(!should_present_worker_frame(
&FirmwareState::ShellRunning(ShellRunningStep::new(TaskId(2))),
&current,
active,
));
assert!(!should_present_worker_frame(
&FirmwareState::HubHome(HubHomeStep),
&current,
active,
));
}
#[test]

View File

@ -1,8 +1,8 @@
{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":37,"PLN":136,"LSN":51,"CLSN":1}}
{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":157,"LSN":53,"CLSN":1}}
{"type":"discussion","id":"DSC-0043","status":"open","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-03","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
{"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]}
{"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]}
{"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]}
{"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
@ -14,7 +14,7 @@
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
{"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]}
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0004","status":"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-0004","status":"done","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0051","file":"discussion/lessons/DSC-0004-system-run-cart/LSN-0051-launch-authority-belongs-to-systemos.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03"}]}
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}

View File

@ -0,0 +1,118 @@
---
id: LSN-0051
ticket: system-run-cart
title: Launch Authority Belongs to SystemOS
created: 2026-07-03
tags: [runtime, systemos, cartridge, launch, abi, home]
---
## Context
The `system.run_cart` workflow resolved two related questions: whether a guest
should have an app-callable cartridge launch syscall, and how Home should start
games from a local library.
The final model removes `system.run_cart` from the userland ABI while preserving
direct host-controlled cartridge boot. Home launch is restored as a
system-owned path: the host may provide `--games-root <dir>`, SystemOS/Home
discovers valid Game directory cartridges, and selecting an entry routes through
firmware into `LoadCartridge`.
The important distinction is authority. A launch can be a valid product action
without being a guest capability.
## Key Decisions
### Remove Guest Cartridge Launch
**What:** Prometeu removed `system.run_cart` from the public userland syscall
surface, generated guest metadata, and runtime dispatch.
**Why:** A guest syscall made cartridge launch look like app-owned navigation.
That gave userland implied authority over target resolution, lifecycle cleanup,
failure policy, and system navigation.
**Trade-offs:** Obsolete guest code cannot keep a compatibility stub. The
runtime instead keeps direct boot as a host/system entrypoint and lets stale
guest attempts fail through the invalid syscall path.
### Preserve Direct Boot Outside The Guest ABI
**What:** Host, CLI, debugger, tests, and single-game flows continue to boot a
selected cartridge directly.
**Why:** Direct boot is still essential for development, automation, and future
single-game distribution. Those flows start with a host-selected target before
VM execution, so they do not need a guest syscall.
**Trade-offs:** The loader remains reusable, but it is not lifecycle
orchestration authority. Higher-level transitions must be owned by firmware or
SystemOS.
### Model Home Launch As A System Action
**What:** `--games-root <dir>` configures a local games library for Home. V1
discovery scans immediate child directories, keeps only valid Game cartridges,
and exposes enough internal metadata to launch a selected entry.
**Why:** Home needs a product path for "choose a game and start it", but that
action belongs to the system profile. SystemOS can present the library and ask
firmware to enter `LoadCartridge` without reintroducing guest-controlled launch.
**Trade-offs:** V1 intentionally excludes recursive discovery, `.pmc`, non-game
apps, rich catalog metadata, return-to-Home, and game-to-game switching. Those
need separate lifecycle and orchestration decisions.
## Patterns and Algorithms
### Authority-Based API Classification
Before exposing an operation to guest/userland code, classify what authority it
implies. If the operation selects another executable target, changes process
lifecycle, or controls navigation between system-owned modes, it is probably a
SystemOS or firmware action rather than an app syscall.
### Separate Selection From Loading
Home owns user selection and presentation. SystemOS owns the launch request.
Firmware owns the transition into `LoadCartridge`. The cartridge loader remains
the mechanism that validates and materializes a cartridge, not the authority
that decides when navigation is allowed.
### Expandable Internal Catalog Records
Home can render a small v1 list while the internal entry retains manifest data,
title, app id, app version, cartridge path, and discovery metadata. This keeps
the UI simple without reducing the model to a display-only DTO.
## Pitfalls
### Compatibility Stubs Preserve False Contracts
A syscall that returns success without doing real lifecycle work is worse than
an absent syscall. It teaches callers that the platform supports app-driven
launch while hiding that no complete transition happened.
### A Product Flow Is Not Automatically A Guest API
"Home can launch a game" and "a guest can launch a cartridge" are different
claims. Product behavior should be mapped to the component with the correct
authority instead of mirrored into the nearest callable ABI.
### Generic Library Roots Prematurely Expand Scope
The first launcher path is a games library, not a marketplace or full app
catalog. Shell apps, System apps, packages, icons, search, and recursive layouts
carry separate contracts and should not be smuggled into the v1 games-root path.
## Takeaways
- Cartridge launch is system navigation, not a userland syscall.
- Direct boot can remain host-controlled without appearing in guest metadata.
- Home launch should emit a system action and let firmware enter
`LoadCartridge`.
- Loader reuse does not imply loader ownership of lifecycle policy.
- Keep v1 discovery narrow: immediate child directory cartridges with
`app_mode: Game`.
- Preserve internal catalog metadata even when the Home UI renders only a small
subset.

View File

@ -0,0 +1,145 @@
---
id: LSN-0052
ticket: foreground-stack-game-pause-shell-vm-backed
title: Foreground Ownership and VM Session Ownership Are Separate
created: 2026-07-05
tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture]
---
# LSN-0052: Foreground Ownership and VM Session Ownership Are Separate
## Context
DSC-0041 closed the `Game -> Home/Shell -> same Game` lifecycle for the first
runtime implementation that supports both native Shell apps and VM-backed Shell
apps. The important architectural shift was not only pausing the Game. The
runtime also had to stop treating one firmware-owned VM as the place where all
guest execution state lives.
The final model separates two questions:
- Who owns the foreground visual/input authority right now?
- Which VM-backed process owns a mutable VM execution context?
Foreground ownership is global SystemOS policy. VM execution state belongs to a
session associated with a task/process. A suspended resident Game is not a saved
snapshot and not a special global VM. It is a live VM session that is temporarily
ineligible for normal foreground ticks, input, frame pacing, and render
publication.
## Key Decisions
### Foreground Stack and Game Pause Contract
**What:** PROMETEU uses a single foreground owner in v1. Hub/Home is the root
Shell owner, a Game may remain resident, and one foreground Shell app may run in
front of the resident Game. Pressing desktop `Esc` is a host/SystemOS Home
request, not guest input.
**Why:** This preserves console-like navigation without turning the runtime into
a general multitasking scheduler. It lets the system pause and suspend a Game,
run Hub or Shell, then resume the same Game deterministically.
**Trade-offs:** The model intentionally rejects multiple resident Games, direct
Shell-to-Game return, background execution, and Game-to-Game switching in v1.
Those features remain future work, but the lifecycle structure now has a place
to add them.
### VM Session Ownership for VM-Backed Processes
**What:** VM-backed mutable execution state is owned by VM sessions. A
VM-backed Game and a VM-backed Shell never share the same VM, stack, heap, PC,
open handles, cartridge identity, or VM-scoped runtime state.
**Why:** A single firmware-owned VM cannot represent a suspended resident Game
and a foreground VM Shell at the same time. Loading the Shell into that global
VM would overwrite the Game context that the lifecycle model says is resident.
**Trade-offs:** Session ownership is more explicit than one global VM, but it
keeps the model simpler than snapshot/restore. It also prepares the runtime for
future background-capable sessions without committing to background scheduling
yet.
## Patterns and Algorithms
### Pause Is Cooperative, Suspension Is OS Authority
The Game receives lifecycle events such as pause and resume, but the OS owns the
actual scheduling state. If the Game does not cooperate within the bounded pause
budget, SystemOS can suspend it anyway. On resume, the Game receives a foreground
restore event and may still keep its internal pause screen while it
synchronizes.
This distinction prevents userland from owning system navigation. The Game may
observe and react. It cannot veto Home.
### Foreground Owner Is Not the Same as Tick Eligibility
Foreground ownership controls input, visual authority, render ownership, and
normal frame pacing. Tick eligibility controls whether a VM session may execute.
In v1, only the foreground VM session receives normal ticks, and the resident
Game does not tick while Shell or Hub is foreground.
The important design detail is that the data model does not assume this must be
true forever. Sessions can later become background-eligible without changing who
owns render/input foreground.
### VM Sessions Are the Unit of Mutable Guest State
Each VM-backed task/process maps to a `VmSession`. The session owns the VM,
runtime state, session-scoped filesystem state, open file handles, next handle
allocation, cartridge identity, lifecycle delivery state, and debug state.
Cartridge loading creates or reuses a VM session through SystemOS session
services. Firmware orchestrates macro states such as `LoadCartridge`,
`GameRunning`, `HubHome`, and `ShellRunning`, but it no longer owns a canonical
guest VM.
### Debugger and Host Inspection Must Follow the Active Session
Host debugging must inspect and mutate the active session VM, not a transitional
global VM. PC, operand stack, breakpoints, debug-step execution, cartridge
identity, and breakpoint-hit events all need to resolve through active session
state.
This keeps debugger behavior aligned with actual execution. A debugger that
observes a different VM than the scheduler ticks is worse than no debugger: it
creates false confidence and hides lifecycle bugs.
### Operational Failures Should Cross Facades as Typed Errors
Session creation and loading are operational boundaries. Missing task, missing
process, duplicate VM session, missing VM session during initialization, and
foreground Shell rejection should be typed errors. Firmware can then choose a
controlled crash report, a launch rejection, or a logged no-transition outcome.
Panics are reserved for test/setup wrappers or impossible internal invariants
that have already been proven by prior checks.
## Pitfalls
- Do not use a global VM as a compatibility bridge after session ownership is
introduced. It will eventually become stale or contradictory.
- Do not let Home/SystemOS be guest input. It changes OS authority, so it must
bypass guest pad state.
- Do not present old frames after foreground ownership changes. Render ownership
and epoch validation are part of lifecycle correctness, not renderer polish.
- Do not model pause and suspension as the same state. Pause is visible to the
Game; suspension is the scheduler mechanism.
- Do not encode v1 limits such as one resident Game or one foreground Shell by
collapsing storage into one VM. Express those limits as lifecycle/session
policy.
- Do not let debugger state drift from scheduler state. The debugger must use
the same active session as normal execution.
## Takeaways
- Foreground ownership is global policy; VM execution state is session-owned.
- A suspended Game is a preserved session, not a serialized VM and not a global
VM waiting to be reused.
- Shell apps can be native or VM-backed, but only VM-backed processes need VM
sessions.
- Session ownership is the foundation for future background-capable processes,
even before background execution exists.
- Typed session/lifecycle errors make launch and recovery behavior explicit at
firmware boundaries.

View File

@ -1,202 +0,0 @@
---
id: AGD-0003
ticket: system-run-cart
title: Agenda - System Run Cart
status: accepted
created: 2026-03-27
resolved:
decision:
tags: []
---
# Agenda - System Run Cart
## Problema
`system.run_cart` existe na ABI pública, mas hoje não produz efeito real no runtime.
Isso expõe duas questões: a superfície atual é um placebo e, mesmo quando houver
fluxo real, rodar cartuchos não deve ser uma operação userland. A autoridade
para iniciar cartuchos deve pertencer ao sistema/firmware.
Isso cria uma falsa promessa de plataforma:
- o programa consegue declarar e chamar uma operação que aparenta iniciar cartuchos;
- o loader aceita sua existência;
- o runtime retorna sucesso vazio;
- nenhuma troca de cartucho, transição de firmware, validação de alvo ou política de segurança ocorre.
## Dor
- A ABI expõe uma capability de sistema que, na prática, nao existe.
- Mesmo se implementada, a operação não deve ser uma syscall userland comum:
apps não devem iniciar cartuchos diretamente.
- O guest pode acreditar que solicitou uma troca de app quando nada aconteceu.
- Isso corrói a confiabilidade da plataforma: a interface pública deixa de ser contrato e vira placebo.
- Qualquer ferramenta, SDK ou documentação construída sobre essa syscall passa a modelar um comportamento inexistente.
## Alvo da Discussao
Definir o contrato real da troca de cartucho iniciada pelo sistema do ponto de vista:
- do guest;
- do runtime;
- do firmware;
- do host/cartridge loader.
Ao final da discussão, deve ficar claro se a superfície atual:
- permanece apenas como mecanismo interno do sistema e sai da ABI userland; ou
- permanece na ABI somente como superfície privilegiada impossível de chamar por apps comuns; ou
- sai temporariamente da ABI até existir fluxo fechado.
Esta discussao nao deve depender do fechamento previo do formato `.pmc`.
Para o primeiro fluxo fechado, `system.run_cart` pode resolver um alvo por
um loader ja disponivel, incluindo cartucho em diretorio ou fixture de teste.
O suporte ao artefato empacotado `.pmc` permanece responsabilidade da
`DSC-0003 / packed-cartridge-loader-pmc` e nao deve bloquear a prova inicial
de troca `Shell/Hub -> Game`.
Tambem precisa ficar claro que `run_cart`, se existir, nao e o orquestrador da
troca. No boot direto por linha de comando, carregar um cartucho e simples
porque o runtime nasce ja apontado para aquele jogo. Com o SO em execucao, o
caso real e diferente: pode haver um Game residente/rodando, o usuario volta
para Home, e o sistema decide iniciar outro Game. Nesse caso o SO precisa
orquestrar fechamento/suspensao descartada do app atual, limpeza de estado,
resolucao do novo alvo, carregamento e transicao para o proximo estado de
firmware. O carregador de cartucho e apenas uma etapa desse fluxo.
A direcao preferida e que a orquestracao de troca de cartucho viva no
SystemOS. Porem, essa agenda nao deve capturar o caso `Game -> Home -> mesmo
Game`: esse e o escopo da `AGD-0041`, que precisa fechar foreground,
pausa/suspensao e retorno ao mesmo Game. A troca `Home -> outro Game`, com
fechamento do jogo atual e carregamento de outro cartucho, pode merecer uma
agenda propria depois da `AGD-0041`, derivada do contrato de foreground que ela
fechar.
## O Que Precisa Ser Definido
1. Semântica da operação.
O que significa "run cart" quando existe SO: trocar imediatamente,
agendar para o próximo frame, iniciar uma transicao orquestrada pelo
sistema, ou apenas carregar bytes de cartucho como etapa interna?
2. Origem do alvo.
Como o guest identifica o cartucho alvo: id numérico, nome canônico, path virtual, handle, manifesto, ou nenhum argumento nesta primeira versão?
Para v1, uma opcao aceitavel e limitar a resolucao a alvos conhecidos pelo host/test harness e carregaveis pelo loader de diretorio, deixando `.pmc` fora do caminho critico.
3. Modelo de segurança.
Nenhum app/userland deve chamar `run_cart`. O sistema/firmware e o caminho
host/CLI/debug devem ser as unicas autoridades para iniciar cartuchos.
Se apps precisarem solicitar navegacao no futuro, isso deve ser uma
superficie nova e mediada pelo sistema.
4. Ponto de integração.
Onde a transição vive: `VirtualMachineRuntime`, firmware, host, SystemOS,
ou combinação dos três? Para esta agenda, a decisao minima e separar o boot
direto host/CLI/debug da ABI guest. A orquestracao `Home -> outro Game`
pertence a `DSC-0043 / AGD-0044`.
5. Efeitos observáveis.
O que acontece quando um guest tenta chamar a superficie removida, e como
ferramentas/boot direto continuam carregando cartuchos sem depender de uma
syscall userland?
6. Contrato de erro.
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
- remocao de `system.run_cart` da ABI userland;
- definicao de que boot direto continua por caminho host/CLI/debug, nao por
syscall guest;
- separacao explicita entre boot direto, loader interno e orquestracao
`Home -> outro Game` da `DSC-0043 / AGD-0044`;
- testes garantindo que guest/userland nao pode declarar/chamar `run_cart`;
- testes garantindo que boot direto de cartucho continua funcional.
## Fora de Escopo
- catálogo completo de apps instalados;
- UX final do launcher/hub;
- resolução remota ou download de cartuchos;
- política de marketplace/distribuição.
- obrigatoriedade de boot a partir de `.pmc` no primeiro fluxo funcional.
## Sugestao / Recomendacao
Recomendo remover `system.run_cart` da ABI userland para v1. Nao ha necessidade
de compatibilidade com a superficie atual: ela nao deve permanecer como stub,
syscall reservada chamavel por guest, nem sucesso vazio.
Rodar cartuchos deve ser uma operacao interna do SystemOS/firmware ou do
caminho host/CLI/debug, nunca uma syscall de app. O boot direto deve continuar
existindo porque e o fluxo essencial para testar cartuchos em desenvolvimento e
tambem representa o modo futuro de executar um jogo unico, como em uma
integracao tipo Steam.
Recomendo tambem separar dois conceitos:
- **boot direto:** usado por linha de comando/debug, inicia o runtime ja com um
cartucho alvo e pode continuar simples;
- **troca via SO:** fluxo orquestrado pelo SystemOS, que fecha o app
atual quando necessario, limpa estado cartridge-scoped, resolve o proximo
alvo e so entao chama o carregador.
Essa agenda nao deve implementar o fluxo `Game -> Home -> mesmo Game`; esse
trabalho pertence a `AGD-0041`. Depois dela, a troca `Home -> outro Game` deve
ser aberta ou refinada como discussao propria, porque adiciona fechamento do
jogo atual, limpeza cartridge-scoped, resolucao de novo alvo e falhas de load.
Se a plataforma precisar de uma ação solicitável por apps no futuro, ela deve
ser modelada separadamente como pedido de navegação mediado pelo sistema, com
nome e erros que não prometam boot direto.
## Respostas Atuais
- `system.run_cart` deve ser removido da ABI userland.
- Nao precisamos manter compatibilidade com a superficie atual.
- Nao deve existir syscall publica para app iniciar cartucho.
- Boot direto por linha de comando, host, debug ou fluxo equivalente de jogo
unico deve permanecer.
- O carregamento interno pode continuar usando loader ja disponivel, incluindo
cartucho em diretorio/fixture.
- `.pmc`, catalogo, UX final de launcher e troca `Game A -> Home -> Game B`
ficam fora desta agenda.
- Se no futuro apps precisarem pedir navegacao, isso deve ser uma nova
superficie mediada pelo sistema, nao a volta de `run_cart` userland.
## Critério de Saida Desta Agenda
Esta agenda só pode virar PR de implementação quando existir decisão escrita para:
- retirada da superfície userland atual;
- preservacao explicita do boot direto host/CLI/debug;
- comportamento esperado para declaracao/chamada guest obsoleta;
- fronteira com `AGD-0041` e `AGD-0044`;
- cobertura minima de testes para remocao userland e boot direto.
## Discussao
- 2026-07-03: A agenda deve ser aberta antes da `AGD-0041` como habilitadora
de validacao do ciclo `Shell/Hub -> Game`. Ficou definido como premissa de
agenda que o primeiro `system.run_cart` testavel nao depende de `.pmc`; ele
pode usar cartucho em diretorio ou fixture resolvida pelo host/test harness.
- 2026-07-03: Direcao ajustada: `run_cart` nao deve ser uma operacao userland.
A autoridade de iniciar cartuchos deve pertencer ao sistema/firmware; apps
podem, no maximo, solicitar navegacao por uma superficie separada e mediada.
- 2026-07-03: Direcao refinada: `run_cart` simples nao resolve o caso com SO.
Boot direto por linha de comando continua sendo um caminho simples, mas
iniciar outro jogo a partir do Home exige um orquestrador do SO/firmware que
fecha o app atual, limpa estado e so entao carrega o novo cartucho.
- 2026-07-03: Escopo refinado: `Game -> Home -> mesmo Game` pertence a
`AGD-0041`. A agenda atual nao deve absorver esse trabalho. Depois da
`AGD-0041`, a troca `Home -> outro Game` pode ser aberta ou refinada como
agenda propria para o orquestrador de troca de cartucho no SystemOS.
- 2026-07-03: A troca `Game A -> Home -> Game B` foi separada para
`DSC-0043 / AGD-0044`, mantendo esta agenda focada na superficie
`run_cart`/loader e sua remocao ou restricao como operacao userland.
- 2026-07-03: Direcao aceita para esta agenda: remover `system.run_cart` da ABI
userland sem necessidade de compatibilidade, manter boot direto para
CLI/debug/teste de cartuchos e futuro modo de jogo unico, e deixar navegacao
mediada por apps para uma superficie futura separada se ela vier a existir.

View File

@ -1,126 +0,0 @@
---
id: AGD-0041
ticket: foreground-stack-game-pause-shell-vm-backed
title: Foreground Stack, Game Pause, and VM-Backed Shell Coexistence
status: open
created: 2026-06-05
resolved:
decision:
tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture]
---
## Contexto
Durante a discussao da `DSC-0040` sobre separar VM e render worker, surgiu um caso que nao pertence somente ao render: `Game -> Shell -> Game`.
O caso minimo e:
- um Game esta rodando;
- o usuario aperta Home;
- o Game deve sair do foreground sem ser encerrado;
- o Hub/Shell assume a tela;
- o usuario pode abrir um app Shell, inclusive VM-backed;
- depois o usuario retorna ao mesmo Game.
O render worker precisa saber qual visual owner pode apresentar frames, mas a decisao principal e de lifecycle: quais VMs existem, quais podem executar, o que significa pausar um Game, e como Shell apps coexistem com um Game pausado.
Material relacionado:
- `DSC-0031`: separacao entre perfis Game e System/Shell.
- `DSC-0032`: SystemOS como autoridade de lifecycle/process/task.
- `DSC-0035`: janelas Shell pertencem a tasks e definem liveness do app.
- `DSC-0036`: Hub/Shell prova fatias de lifecycle.
- `DSC-0040`: render ownership/epoch durante transicoes e paralelismo VM/render.
## Problema
Precisamos definir o contrato de foreground para impedir ambiguidades como:
- Game pausado continuando a executar frames logicos enquanto Shell esta no foreground;
- Shell VM-backed competindo com Game por pacing, input, superficie ou render ownership;
- multiplos Shells foreground ou multiplos Games ativos sem uma politica explicita;
- retorno ao Game sem clareza sobre estado pausado, input, render invalidation e retomada da VM.
Sem esse contrato, a separacao VM/render da `DSC-0040` pode ficar tecnicamente correta, mas o SystemOS ainda pode permitir estados incoerentes.
## Pontos Criticos
- Existe apenas um foreground visual owner por vez.
- Background execution e outro assunto e deve ficar fora da primeira decisao, salvo se for necessario definir uma proibicao explicita.
- Shell VM-backed nao deve herdar politica de game loop so por usar VM.
- Game pausado provavelmente deve reter estado, mas nao deve consumir frames logicos.
- O Hub/Shell precisa ser capaz de rodar enquanto o Game esta suspenso.
- A transicao deve invalidar render submissions antigas para impedir present de frames obsoletos.
- O modelo precisa caber no SystemOS/lifecycle existente, nao diretamente no render worker.
## Opcoes
### Opcao A - Foreground unico, Game suspenso, Shell unico
- **Abordagem:** permitir no maximo um Game carregado e no maximo um Shell foreground; ao apertar Home, o Game entra em estado pausado/suspenso e Shell/Hub assume o foreground. Shell VM-backed pode executar apenas enquanto for o foreground Shell.
- **Pro:** modelo simples, previsivel e alinhado com console; reduz concorrencia real entre VMs; combina com a regra de render ownership unico.
- **Contra:** nao cobre multitarefa/background; futuras features de overlay ou apps persistentes precisarao expandir o contrato.
- **Manutenibilidade:** alta; SystemOS pode modelar uma pilha pequena de foreground/suspended sem virar scheduler generico.
### Opcao B - Multiplas VMs residentes, somente foreground executa
- **Abordagem:** permitir que Game e Shell VM-backed coexistam como VMs residentes, mas somente a entidade foreground executa. O Game pausado fica residente, sem tick; Shell VM-backed roda sob lifecycle Shell.
- **Pro:** preserva estado e permite retorno rapido; nao exige background execution.
- **Contra:** precisa contrato claro de memoria, ownership de input, foco, cancelamento e fechamento.
- **Manutenibilidade:** boa se os estados forem explicitos; ruim se a residencia for confundida com execucao em background.
### Opcao C - Encerrar Game ao entrar no Shell VM-backed
- **Abordagem:** Home fecha/salva o Game antes de abrir Shell VM-backed, evitando coexistencia.
- **Pro:** simplifica o runtime.
- **Contra:** quebra a expectativa de console moderno; torna `Game -> Shell -> Game` um reload, nao uma pausa.
- **Manutenibilidade:** simples no curto prazo, mas provavelmente gera mais excecoes e UX ruim depois.
### Opcao D - Background real desde ja
- **Abordagem:** permitir Game pausado ou Shell apps rodando em background com politicas de prioridade.
- **Pro:** cobre casos futuros ricos.
- **Contra:** amplia demais escopo: scheduler, budget, energia, audio, input, IO e memoria.
- **Manutenibilidade:** baixa para v1; mistura a decisao de foreground com multitarefa real.
## Sugestao / Recomendacao
Recomendo seguir com a combinacao das opcoes A e B:
- somente um Game residente por vez;
- somente um Shell foreground por vez;
- Game pode ficar pausado/suspenso enquanto Shell esta foreground;
- Shell VM-backed pode existir junto com Game pausado, mas segue lifecycle Shell;
- somente o owner foreground executa VM/tick/event loop conforme sua politica;
- background execution fica explicitamente fora de escopo;
- render ownership muda junto com foreground owner e deve invalidar submissions antigas via epoch/generation, conforme `DSC-0040`.
Isso preserva o ciclo `Game -> Home/Shell -> Game` sem transformar o runtime em multitarefa geral agora.
## Perguntas em Aberto
- [ ] Qual e o nome canonico dos estados de Game fora do foreground: `Paused`, `Suspended`, ambos, ou outro?
- [ ] Ao apertar Home, o Game deve receber um evento/interrupt/trap de pausa antes de parar de executar?
- [ ] O Hub e um Shell especial sempre presente ou uma task Shell comum?
- [ ] Um app Shell VM-backed pode substituir o Hub no foreground ou o Hub permanece como owner raiz?
- [ ] Quando o Shell VM-backed fecha, o retorno vai sempre para Hub ou pode voltar diretamente ao Game?
- [ ] O input do Game e drenado/limpo ao pausar e ao retomar?
- [ ] Audio do Game pausa junto com a VM ou tem politica propria?
- [ ] Existe limite de memoria/residencia para manter Game pausado enquanto Shell app roda?
- [ ] O retorno ao Game exige novo frame antes de trocar a superficie, ou a tela pode manter Hub/Shell ate a primeira submission valida?
- [ ] Quais eventos de lifecycle precisam ser visiveis para firmware, OS, VM e host?
## Criterio para Encerrar
A agenda pode ser encerrada quando tivermos uma decisao clara sobre:
- cardinalidade de Games e Shells residentes/foreground;
- semantica de Game pausado/suspenso;
- politica de execucao para Shell VM-backed durante Game pausado;
- regra de foreground visual owner e render invalidation;
- comportamento minimo do ciclo `Game -> Shell -> Game`;
- itens explicitamente fora de escopo, especialmente background execution real.
## Discussao
- 2026-06-05: Agenda criada a partir da `DSC-0040`, quando o caso de Shell VM-backed aberto sobre um Game pausado mostrou que a discussao pertence ao lifecycle/SystemOS, nao apenas ao render worker.

View File

@ -1,161 +0,0 @@
---
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.

View File

@ -1,136 +0,0 @@
---
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`.

View File

@ -1,156 +0,0 @@
---
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`.

View File

@ -1,90 +0,0 @@
---
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`

View File

@ -1,79 +0,0 @@
---
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`

View File

@ -1,83 +0,0 @@
---
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/`

View File

@ -1,103 +0,0 @@
---
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`

View File

@ -1,118 +0,0 @@
---
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`

View File

@ -1,104 +0,0 @@
---
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`

View File

@ -1,99 +0,0 @@
---
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`

View File

@ -239,8 +239,10 @@ The same physical surface may be reused by multiple visual owners, but logical
ownership MUST remain explicit.
Game pause/resume, foreground stack behavior, and coexistence of a paused Game
with VM-backed Shell apps are outside this contract and are tracked by the
foreground/lifecycle discussion.
with VM-backed Shell apps are defined by the foreground/lifecycle contract. The
render boundary's responsibility is narrower: foreground owner changes MUST
advance ownership, stale submissions MUST be discarded, and visual return to a
resumed Game MUST wait for a valid submission from the current ownership epoch.
### 4.7 Render telemetry

View File

@ -107,3 +107,21 @@ Input access is VM-owned and should not be reported as host syscall consumption.
All platforms must provide the mandatory input elements (`pad`, `touch`, `button`) to the runtime.
Platform differences in physical device mapping are resolved outside VM semantics.
## 9 Host/System Controls
Host/System controls are not part of the guest-visible input surface.
The Home/SystemOS request changes machine foreground authority. It must be
handled by the host, firmware, or SystemOS before input is exposed to the Game.
It must not be represented as `pad.start`, `pad.select`, a new pad field, a VM
intrinsic, or a userland syscall.
For the desktop host, `Esc` is the primary keyboard mapping for the
Home/SystemOS request. The physical `Home` key may be supported as an alias.
Both mappings are host controls and must not mutate `InputSignals`.
When a Game leaves or re-enters foreground, pending Game input must be cleared
or barriered so held or pressed input cannot leak across pause/resume
boundaries. This barrier applies to the Game-facing snapshot; it does not
change the meaning of host/system Home controls.

View File

@ -100,7 +100,70 @@ Important properties:
- no execution occurs outside the frame loop;
- frame structure remains observable for host tooling and host-owned certification.
## 7 Render Worker Concurrency
## 7 Foreground Lifecycle Events
Foreground lifecycle events are system events with bounded delivery points.
They do not execute arbitrary guest callbacks at arrival time.
The Game pause path is:
```text
Home/SystemOS request
-> pause event becomes visible to the Game
-> bounded pause reaction budget
-> SystemOS suspension, even if the Game did not cooperate
```
The Game resume path is:
```text
SystemOS resume request
-> VM reactivation
-> resume or foreground-restore event becomes visible to the Game
-> Game may synchronize while still internally paused
-> visual foreground returns after a valid current-epoch render submission
```
`Paused` is the Game-visible event/state. `Suspended` is the OS-owned
scheduler/runtime state. A Game may observe pause/resume, but it must not
control whether SystemOS suspends or resumes the VM.
Pause budgets are measured in deterministic machine units, not host wall-clock
time. A suspended Game receives no normal gameplay ticks, no normal Game input,
and no frame pacing. Real background execution is outside the v1 foreground
contract.
## 8 VM Sessions and Execution Eligibility
Every VM-backed process MUST own a distinct VM session. A VM session owns the
mutable execution context for that process, including the VM instance,
per-session runtime counters, lifecycle delivery bookkeeping, debug/pause state,
and transient handles opened through VM-facing services.
Foreground ownership and execution eligibility are related but separate
concepts:
- foreground ownership selects the visual/navigation owner;
- execution eligibility selects which sessions may receive VM ticks;
- v1 grants normal VM execution only to the foreground VM-backed task, plus the
bounded Game pause handoff tick defined above;
- a suspended resident Game preserves its VM session but MUST NOT receive
normal gameplay ticks, normal Game input, or frame pacing;
- a VM-backed Shell owns its own VM session and MUST NOT overwrite or advance
the resident Game session while the Shell is foreground;
- a native Shell does not own a VM session.
The model is intentionally compatible with future background execution. Future
service or media processes may become execution-eligible without changing the
rule that mutable VM context belongs to the owning session. This chapter does
not grant any v1 background progress guarantee.
Durable app data remains keyed by `app_id` where the relevant storage contract
requires it. Transient VM handles, open files, lifecycle delivery queues, and
staging state are session-scoped unless another spec explicitly makes them
durable app data.
## 9 Render Worker Concurrency
The render worker is not a machine-visible event source and does not introduce
guest callbacks. It is an implementation-side consumer of closed render
@ -127,7 +190,7 @@ Shutdown is explicit and bounded. A shutdown request wakes a waiting worker,
causes pending work that will not be consumed to be discarded, and reports a
typed failure if the worker cannot join within the configured timeout.
## 8 Async Asset and IO Work Lane
## 10 Async Asset and IO Work Lane
The asset/IO async work lane is not a machine-visible event source and does not
introduce guest callbacks. It is an implementation-side lane for asset
@ -151,7 +214,7 @@ does not publish resident graphics/audio/scene state directly.
FS and game persistence services may consume this lane for IO-style work, but
public FS API shape is defined by the FS/app-home contract, not by this chapter.
## 9 Determinism and Best Practices
## 11 Determinism and Best Practices
PROMETEU encourages:
@ -166,7 +229,7 @@ PROMETEU discourages:
- hidden timing channels;
- ambiguous out-of-band execution.
## 10 Relationship to Other Specs
## 12 Relationship to Other Specs
- [`09a-coroutines-and-cooperative-scheduling.md`](09a-coroutines-and-cooperative-scheduling.md) defines coroutine lifecycle and scheduling behavior.
- [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces.

View File

@ -43,13 +43,23 @@ The VM does not own the machine lifecycle. Firmware does.
Game directory cartridges.
- **Library entry**: an internal catalog record retained by Hub/SystemOS for a
discovered cartridge.
- **Foreground visual owner**: the single machine owner whose visual output may
be presented at a given time.
- **Paused**: a Game-visible lifecycle event/state. The Game may observe it and
prepare for foreground loss.
- **Suspended**: an OS-owned scheduler/runtime state. A suspended Game remains
resident but does not receive normal gameplay ticks, normal Game input, or
frame pacing.
- **VM session**: the POS-owned mutable execution context for one VM-backed
process. It contains the VM instance and per-session runtime state.
## 3 POS Responsibilities
POS is responsible for:
- deterministic reset into known machine state;
- VM initialization and teardown for cartridge execution;
- VM session creation, initialization, activation, and teardown for cartridge
execution;
- input latching and logical-frame budgeting;
- peripheral reset/orchestration;
- fault capture and crash-flow transition;
@ -61,6 +71,10 @@ At the VM boundary, POS must preserve:
- `FRAME_SYNC` as the canonical frame boundary;
- deterministic transition from host tick to VM slice execution.
Firmware must not treat a single global VM object as the canonical execution
owner for all cartridges. The firmware state machine orchestrates macro-state;
POS/session services own VM-backed process context.
## 4 PrometeuHub Responsibilities
PrometeuHub is responsible for:
@ -154,19 +168,74 @@ Shell task loses its eligible focused window
-> FirmwareState::HubHome
```
## 7 Cartridge Load Flow
## 7 Foreground Stack and Game Pause
The v1 foreground contract is single-owner:
- exactly one foreground visual owner exists at a time;
- at most one Game may be resident;
- at most one Shell app may be foreground;
- Hub/Home is the root Shell owner and back-stack controller.
The minimal supported navigation cycle is:
```text
Game foreground
-> Home request
-> Game pause notification
-> Game suspended by SystemOS after a bounded budget
-> Hub/Home foreground
-> optional Shell foreground
-> Shell close returns to Hub/Home
-> same Game resume/foreground-restore notification
-> Game foreground after a valid current-epoch render submission
```
`Paused` is cooperative and visible to the Game. `Suspended` is imposed by
SystemOS. The Game may react to pause/resume, but it must not control whether
the OS suspends or resumes its VM.
When Home is requested during Game execution, SystemOS must notify the Game and
grant a short bounded pause budget. After the budget, SystemOS may suspend the
VM even if the Game did not cooperate. While suspended, the Game must not
receive normal gameplay ticks, normal Game input, or frame pacing.
When returning to the same Game, SystemOS must reactivate the VM and send a
resume or foreground-restore notification before visual foreground is restored.
The Game may remain internally paused while it synchronizes. Hub/Shell remains
visible until the resumed Game publishes a valid render submission for the
current ownership epoch.
Input pending for the Game must be cleared or barriered across pause/resume
boundaries. Game audio pauses with the Game VM in v1. Independent background
audio, real background execution, multiple resident Games, multiple foreground
Shells, and direct Game-to-Game switching are outside this contract.
The resident Game VM session remains allocated while Hub or Shell is foreground.
It preserves Game VM state but is not execution-eligible except for the bounded
pause handoff and later foreground resume. A VM-backed Shell has a separate VM
session. A native Shell has no VM session.
## 8 Cartridge Load Flow
Current high-level flow:
1. POS receives a cartridge to load;
2. asset manager is initialized from the cartridge asset table, preload list, and packed asset bytes;
3. POS initializes the VM/runtime for the cartridge;
4. launch behavior branches by `AppMode`:
3. POS creates or selects the VM session for the cartridge;
4. POS initializes the session-owned VM/runtime for the cartridge;
5. launch behavior branches by `AppMode`:
- `Game` -> transition to the game-running firmware state;
- `System` -> create/focus a Hub window and return to the Hub state.
If VM initialization fails, firmware transitions to the crash path.
For v1, POS must enforce at most one resident Game session. Selecting the same
resident Game from Home resumes that session. Selecting a different Game while a
resident Game exists is outside the v1 foreground contract and must not create a
second resident Game session. VM-backed Shell loading creates a distinct Shell
session and must not overwrite resident Game VM state.
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
@ -179,7 +248,7 @@ 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
## 9 Firmware States
The current firmware state model includes:
@ -189,11 +258,12 @@ The current firmware state model includes:
- `HubHome`
- `LoadCartridge`
- `GameRunning`
- `ShellRunning`
- `AppCrashes`
These states express machine orchestration above the VM. They are not guest-visible bytecode states.
## 9 Execution Contract
## 10 Execution Contract
For game-mode execution, firmware/runtime coordination preserves:
@ -204,7 +274,7 @@ For game-mode execution, firmware/runtime coordination preserves:
This keeps the machine model deterministic and observable.
## 10 Crash Handling
## 11 Crash Handling
Firmware owns terminal fault presentation.
@ -216,7 +286,32 @@ When a terminal app fault occurs:
Crash handling is outside the guest VM execution model.
## 11 Relationship to Other Specs
## 12 Desktop Smoke Validation
The foreground contract can be smoke-tested on the desktop host with:
```text
cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console
cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges
```
Expected behavior:
- direct `--run` still boots the selected Game without using a guest cartridge
switching API;
- `--games-root` starts in Home and launches Games through the Home library;
- while a Game is running, `Esc` opens Home/SystemOS without entering the guest
pad input surface;
- the Game is paused, then suspended after the bounded SystemOS budget;
- Home remains foreground while the Game is resident;
- launching and closing a Shell app returns to Home without losing the resident
Game;
- a VM-backed Shell advances its own VM session and does not tick the suspended
resident Game session;
- returning to the same Game resumes it through the foreground lifecycle and
waits for current-epoch Game rendering before visual Game presentation.
## 13 Relationship to Other Specs
- [`02-vm-instruction-set.md`](02-vm-instruction-set.md) defines the VM subsystem run by firmware.
- [`09-events-and-concurrency.md`](09-events-and-concurrency.md) defines the frame-boundary model used by firmware.

View File

@ -50,6 +50,8 @@ For a cartridge boot target:
- `Game` cartridges transition into the game-running pipeline;
- `System` cartridges transition into a Runtime/Hub pipeline dedicated to
system UI and app hosting.
- VM-backed cartridges are initialized in a POS-owned VM session associated with
the created task/process.
This preserves the distinction between machine firmware state and app execution mode.
@ -99,6 +101,14 @@ 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.
The foreground/home contract defines `Game -> Home/Shell -> same Game` after a
Game is already running. It does not change direct `--run`, debugger direct
boot, or `--games-root` Home library startup semantics. Home/SystemOS requests
are host/system controls, not guest cartridge boot APIs.
Direct boot creates the initial VM session for the selected cartridge. It does
not create a reusable global VM shared by later Game or Shell cartridges.
## 5 Firmware State Relationship
Boot target selection feeds into firmware states such as:

View File

@ -187,6 +187,11 @@ The game memcard profile uses module `mem` with status-first return shapes.
The `mem` module is a game profile surface. It is not the public storage model
for `System` profile apps.
Durable memcard data is keyed by `app_id` according to the save-memory
contract. Runtime handles, open file tables, pending lifecycle delivery, and
other mutable VM-facing syscall context are scoped to the owning VM session and
MUST NOT be used as durable app identity.
Canonical operations in v1 are:
- `mem.slot_count() -> (status, count)`