Compare commits
No commits in common. "313b896e7baecaea5c9a2a2b6d25dd419b653cbb" and "4bc9ce2b73af4f2c9ae32e387aedd5d65d9db91e" have entirely different histories.
313b896e7b
...
4bc9ce2b73
@ -1,14 +1,11 @@
|
|||||||
use crate::firmware::boot_target::BootTarget;
|
use crate::firmware::boot_target::BootTarget;
|
||||||
use crate::firmware::firmware_state::{
|
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
|
||||||
FirmwareState, GameRunningStep, LoadCartridgeStep, ResetStep,
|
|
||||||
};
|
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
use crate::firmware::prometeu_context::PrometeuContext;
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::cartridge::Cartridge;
|
use prometeu_hal::cartridge::Cartridge;
|
||||||
use prometeu_hal::log::LogSource;
|
|
||||||
use prometeu_hal::telemetry::CertificationConfig;
|
use prometeu_hal::telemetry::CertificationConfig;
|
||||||
use prometeu_hal::{InputSignals, RuntimePlatform};
|
use prometeu_hal::{InputSignals, RuntimePlatform};
|
||||||
use prometeu_system::{PrometeuHub, ResidentGameState, SystemOS};
|
use prometeu_system::{PrometeuHub, SystemOS};
|
||||||
|
use prometeu_vm::VirtualMachine;
|
||||||
|
|
||||||
/// PROMETEU Firmware.
|
/// PROMETEU Firmware.
|
||||||
///
|
///
|
||||||
@ -26,6 +23,8 @@ use prometeu_system::{PrometeuHub, ResidentGameState, SystemOS};
|
|||||||
/// 2. Delegates the logic update to the current active state.
|
/// 2. Delegates the logic update to the current active state.
|
||||||
/// 3. Handles state transitions (e.g., from Loading to Playing).
|
/// 3. Handles state transitions (e.g., from Loading to Playing).
|
||||||
pub struct Firmware {
|
pub struct Firmware {
|
||||||
|
/// The execution engine (PVM) for user applications.
|
||||||
|
pub vm: VirtualMachine,
|
||||||
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
|
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
|
||||||
pub os: SystemOS,
|
pub os: SystemOS,
|
||||||
/// The internal state of the system launcher (Hub).
|
/// The internal state of the system launcher (Hub).
|
||||||
@ -36,19 +35,18 @@ pub struct Firmware {
|
|||||||
pub boot_target: BootTarget,
|
pub boot_target: BootTarget,
|
||||||
/// State-machine lifecycle tracker.
|
/// State-machine lifecycle tracker.
|
||||||
state_initialized: bool,
|
state_initialized: bool,
|
||||||
game_input_barrier_frames: u8,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Firmware {
|
impl Firmware {
|
||||||
/// Initializes the firmware in the `Reset` state.
|
/// Initializes the firmware in the `Reset` state.
|
||||||
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
vm: VirtualMachine::default(),
|
||||||
os: SystemOS::new(cap_config),
|
os: SystemOS::new(cap_config),
|
||||||
hub: PrometeuHub::new(),
|
hub: PrometeuHub::new(),
|
||||||
state: FirmwareState::Reset(ResetStep),
|
state: FirmwareState::Reset(ResetStep),
|
||||||
boot_target: BootTarget::Hub,
|
boot_target: BootTarget::Hub,
|
||||||
state_initialized: false,
|
state_initialized: false,
|
||||||
game_input_barrier_frames: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,31 +55,23 @@ impl Firmware {
|
|||||||
/// This method is called exactly once per Host frame (60Hz).
|
/// This method is called exactly once per Host frame (60Hz).
|
||||||
/// It updates peripheral signals and delegates the logic to the current state.
|
/// It updates peripheral signals and delegates the logic to the current state.
|
||||||
pub fn tick(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
|
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.
|
// 0. Process asset commits at the beginning of the frame boundary.
|
||||||
platform.assets_mut().apply_commits();
|
platform.assets_mut().apply_commits();
|
||||||
|
|
||||||
// 1. Update the peripheral state using the latest signals from the Host.
|
// 1. Update the peripheral state using the latest signals from the Host.
|
||||||
// This ensures input is consistent throughout the entire update.
|
// This ensures input is consistent throughout the entire update.
|
||||||
platform.input_mut().pad_mut().begin_frame(effective_signals);
|
platform.input_mut().pad_mut().begin_frame(signals);
|
||||||
platform.input_mut().touch_mut().begin_frame(effective_signals);
|
platform.input_mut().touch_mut().begin_frame(signals);
|
||||||
|
|
||||||
// 2. State machine lifecycle management.
|
// 2. State machine lifecycle management.
|
||||||
if !self.state_initialized {
|
if !self.state_initialized {
|
||||||
self.on_enter(effective_signals, platform);
|
self.on_enter(signals, platform);
|
||||||
self.state_initialized = true;
|
self.state_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Update the current state and check for transitions.
|
// 3. Update the current state and check for transitions.
|
||||||
if let Some(next_state) = self.on_update(effective_signals, platform) {
|
if let Some(next_state) = self.on_update(signals, platform) {
|
||||||
self.change_state(next_state, effective_signals, platform);
|
self.change_state(next_state, signals, platform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,23 +82,9 @@ impl Firmware {
|
|||||||
signals: &InputSignals,
|
signals: &InputSignals,
|
||||||
platform: &mut dyn RuntimePlatform,
|
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.on_exit(signals, platform);
|
||||||
self.state = new_state;
|
self.state = new_state;
|
||||||
self.state_initialized = false;
|
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.
|
// Enter the new state immediately to avoid "empty" frames during transitions.
|
||||||
self.on_enter(signals, platform);
|
self.on_enter(signals, platform);
|
||||||
@ -118,6 +94,7 @@ impl Firmware {
|
|||||||
/// Dispatches the `on_enter` event to the current state implementation.
|
/// Dispatches the `on_enter` event to the current state implementation.
|
||||||
fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
|
fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
|
||||||
let mut req = PrometeuContext {
|
let mut req = PrometeuContext {
|
||||||
|
vm: &mut self.vm,
|
||||||
os: &mut self.os,
|
os: &mut self.os,
|
||||||
hub: &mut self.hub,
|
hub: &mut self.hub,
|
||||||
boot_target: &self.boot_target,
|
boot_target: &self.boot_target,
|
||||||
@ -130,7 +107,6 @@ impl Firmware {
|
|||||||
FirmwareState::LaunchHub(s) => s.on_enter(&mut req),
|
FirmwareState::LaunchHub(s) => s.on_enter(&mut req),
|
||||||
FirmwareState::HubHome(s) => s.on_enter(&mut req),
|
FirmwareState::HubHome(s) => s.on_enter(&mut req),
|
||||||
FirmwareState::LoadCartridge(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::GameRunning(s) => s.on_enter(&mut req),
|
||||||
FirmwareState::ShellRunning(s) => s.on_enter(&mut req),
|
FirmwareState::ShellRunning(s) => s.on_enter(&mut req),
|
||||||
FirmwareState::AppCrashes(s) => s.on_enter(&mut req),
|
FirmwareState::AppCrashes(s) => s.on_enter(&mut req),
|
||||||
@ -145,6 +121,7 @@ impl Firmware {
|
|||||||
platform: &mut dyn RuntimePlatform,
|
platform: &mut dyn RuntimePlatform,
|
||||||
) -> Option<FirmwareState> {
|
) -> Option<FirmwareState> {
|
||||||
let mut req = PrometeuContext {
|
let mut req = PrometeuContext {
|
||||||
|
vm: &mut self.vm,
|
||||||
os: &mut self.os,
|
os: &mut self.os,
|
||||||
hub: &mut self.hub,
|
hub: &mut self.hub,
|
||||||
boot_target: &self.boot_target,
|
boot_target: &self.boot_target,
|
||||||
@ -157,7 +134,6 @@ impl Firmware {
|
|||||||
FirmwareState::LaunchHub(s) => s.on_update(&mut req),
|
FirmwareState::LaunchHub(s) => s.on_update(&mut req),
|
||||||
FirmwareState::HubHome(s) => s.on_update(&mut req),
|
FirmwareState::HubHome(s) => s.on_update(&mut req),
|
||||||
FirmwareState::LoadCartridge(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::GameRunning(s) => s.on_update(&mut req),
|
||||||
FirmwareState::ShellRunning(s) => s.on_update(&mut req),
|
FirmwareState::ShellRunning(s) => s.on_update(&mut req),
|
||||||
FirmwareState::AppCrashes(s) => s.on_update(&mut req),
|
FirmwareState::AppCrashes(s) => s.on_update(&mut req),
|
||||||
@ -167,6 +143,7 @@ impl Firmware {
|
|||||||
/// Dispatches the `on_exit` event to the current state implementation.
|
/// Dispatches the `on_exit` event to the current state implementation.
|
||||||
fn on_exit(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
|
fn on_exit(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
|
||||||
let mut req = PrometeuContext {
|
let mut req = PrometeuContext {
|
||||||
|
vm: &mut self.vm,
|
||||||
os: &mut self.os,
|
os: &mut self.os,
|
||||||
hub: &mut self.hub,
|
hub: &mut self.hub,
|
||||||
boot_target: &self.boot_target,
|
boot_target: &self.boot_target,
|
||||||
@ -179,7 +156,6 @@ impl Firmware {
|
|||||||
FirmwareState::LaunchHub(s) => s.on_exit(&mut req),
|
FirmwareState::LaunchHub(s) => s.on_exit(&mut req),
|
||||||
FirmwareState::HubHome(s) => s.on_exit(&mut req),
|
FirmwareState::HubHome(s) => s.on_exit(&mut req),
|
||||||
FirmwareState::LoadCartridge(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::GameRunning(s) => s.on_exit(&mut req),
|
||||||
FirmwareState::ShellRunning(s) => s.on_exit(&mut req),
|
FirmwareState::ShellRunning(s) => s.on_exit(&mut req),
|
||||||
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
|
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
|
||||||
@ -190,60 +166,6 @@ impl Firmware {
|
|||||||
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge));
|
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge));
|
||||||
self.state_initialized = false;
|
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)]
|
#[cfg(test)]
|
||||||
@ -253,7 +175,6 @@ mod tests {
|
|||||||
use prometeu_bytecode::assembler::assemble;
|
use prometeu_bytecode::assembler::assemble;
|
||||||
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
||||||
use prometeu_drivers::TestPlatform;
|
use prometeu_drivers::TestPlatform;
|
||||||
use prometeu_hal::RuntimePlatform;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
use prometeu_hal::app_mode::AppMode;
|
||||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
@ -262,10 +183,7 @@ mod tests {
|
|||||||
use prometeu_system::process::ProcessState;
|
use prometeu_system::process::ProcessState;
|
||||||
use prometeu_system::task::{TaskId, TaskState};
|
use prometeu_system::task::{TaskId, TaskState};
|
||||||
use prometeu_system::windows::WindowOwner;
|
use prometeu_system::windows::WindowOwner;
|
||||||
use prometeu_system::{
|
use prometeu_system::{CrashReport, discover_games_root};
|
||||||
CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState,
|
|
||||||
discover_games_root,
|
|
||||||
};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
@ -384,13 +302,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn valid_cartridge(app_mode: AppMode) -> Cartridge {
|
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 {
|
Cartridge {
|
||||||
app_id,
|
app_id: 9,
|
||||||
title: title.into(),
|
title: "Valid Cart".into(),
|
||||||
app_version: "1.0.0".into(),
|
app_version: "1.0.0".into(),
|
||||||
app_mode,
|
app_mode,
|
||||||
capabilities: caps::NONE,
|
capabilities: caps::NONE,
|
||||||
@ -401,16 +315,6 @@ 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) {
|
fn load_shell_running_firmware() -> (Firmware, TestPlatform, InputSignals, TaskId) {
|
||||||
let mut firmware = Firmware::new(None);
|
let mut firmware = Firmware::new(None);
|
||||||
let mut platform = TestPlatform::new();
|
let mut platform = TestPlatform::new();
|
||||||
@ -525,24 +429,6 @@ mod tests {
|
|||||||
assert!(matches!(firmware.state, FirmwareState::LaunchHub(_)));
|
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]
|
#[test]
|
||||||
fn load_cartridge_routes_system_apps_to_system_running() {
|
fn load_cartridge_routes_system_apps_to_system_running() {
|
||||||
let mut firmware = Firmware::new(None);
|
let mut firmware = Firmware::new(None);
|
||||||
@ -674,60 +560,6 @@ mod tests {
|
|||||||
assert_eq!(firmware.os.windows().window_count(), 0);
|
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]
|
#[test]
|
||||||
fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() {
|
fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() {
|
||||||
let root = TestGameRoot::new();
|
let root = TestGameRoot::new();
|
||||||
@ -895,236 +727,6 @@ mod tests {
|
|||||||
assert_eq!(firmware.os.windows().window_count(), 0);
|
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]
|
#[test]
|
||||||
fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() {
|
fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() {
|
||||||
assert!(Syscall::from_u32(0x0002).is_none());
|
assert!(Syscall::from_u32(0x0002).is_none());
|
||||||
|
|||||||
@ -14,7 +14,6 @@ pub enum FirmwareState {
|
|||||||
LaunchHub(LaunchHubStep),
|
LaunchHub(LaunchHubStep),
|
||||||
HubHome(HubHomeStep),
|
HubHome(HubHomeStep),
|
||||||
LoadCartridge(LoadCartridgeStep),
|
LoadCartridge(LoadCartridgeStep),
|
||||||
ResumeResidentGame,
|
|
||||||
GameRunning(GameRunningStep),
|
GameRunning(GameRunningStep),
|
||||||
ShellRunning(ShellRunningStep),
|
ShellRunning(ShellRunningStep),
|
||||||
AppCrashes(AppCrashesStep),
|
AppCrashes(AppCrashesStep),
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
|
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
use crate::firmware::prometeu_context::PrometeuContext;
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
|
use prometeu_system::CrashReport;
|
||||||
use prometeu_system::task::{TaskId, TaskState};
|
use prometeu_system::task::{TaskId, TaskState};
|
||||||
use prometeu_system::{CrashReport, ResidentGameState};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GameRunningStep {
|
pub struct GameRunningStep {
|
||||||
@ -37,35 +36,13 @@ impl GameRunningStep {
|
|||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pause_requested = matches!(
|
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform);
|
||||||
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 {
|
if let Some(report) = result {
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { 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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ use crate::firmware::firmware_state::{
|
|||||||
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
|
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
|
||||||
};
|
};
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
use crate::firmware::prometeu_context::PrometeuContext;
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
@ -20,29 +19,14 @@ impl HubHomeStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.platform);
|
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform);
|
||||||
if let Some(report) = outcome.crash {
|
if let Some(report) = outcome.crash {
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
match outcome.action {
|
match outcome.action {
|
||||||
Some(SystemProfileAction::LaunchNativeShell(app)) => {
|
Some(SystemProfileAction::LaunchNativeShell(app)) => {
|
||||||
let task_id = match ctx
|
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
|
||||||
.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(
|
let id = ctx.os.windows().add_window(
|
||||||
app.title().to_string(),
|
app.title().to_string(),
|
||||||
WindowOwner::Task(task_id),
|
WindowOwner::Task(task_id),
|
||||||
@ -54,32 +38,6 @@ impl HubHomeStep {
|
|||||||
}
|
}
|
||||||
Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) {
|
Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) {
|
||||||
Ok(cartridge) => {
|
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)));
|
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@ -14,13 +14,11 @@ use prometeu_system::windows::WindowOwner;
|
|||||||
pub struct LoadCartridgeStep {
|
pub struct LoadCartridgeStep {
|
||||||
pub cartridge: Cartridge,
|
pub cartridge: Cartridge,
|
||||||
init_error: Option<CrashReport>,
|
init_error: Option<CrashReport>,
|
||||||
loaded_task_id: Option<prometeu_system::task::TaskId>,
|
|
||||||
loaded_app_mode: Option<AppMode>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoadCartridgeStep {
|
impl LoadCartridgeStep {
|
||||||
pub fn new(cartridge: Cartridge) -> Self {
|
pub fn new(cartridge: Cartridge) -> Self {
|
||||||
Self { cartridge, init_error: None, loaded_task_id: None, loaded_app_mode: None }
|
Self { cartridge, init_error: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||||
@ -38,15 +36,7 @@ impl LoadCartridgeStep {
|
|||||||
self.cartridge.assets.clone(),
|
self.cartridge.assets.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
match ctx.os.sessions().load_vm_cartridge(&self.cartridge) {
|
self.init_error = ctx.os.vm().initialize(ctx.vm, &self.cartridge).err();
|
||||||
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> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
@ -54,18 +44,11 @@ impl LoadCartridgeStep {
|
|||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(task_id) = self.loaded_task_id.take() else {
|
if self.cartridge.app_mode == AppMode::Shell {
|
||||||
let report = CrashReport::VmPanic {
|
let task_id = ctx
|
||||||
message: format!(
|
.os
|
||||||
"cartridge {} was not loaded into a VM session",
|
.sessions()
|
||||||
self.cartridge.title
|
.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone());
|
||||||
),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.loaded_app_mode == Some(AppMode::Shell) {
|
|
||||||
let id = ctx.os.windows().add_window(
|
let id = ctx.os.windows().add_window(
|
||||||
self.cartridge.title.clone(),
|
self.cartridge.title.clone(),
|
||||||
WindowOwner::Task(task_id),
|
WindowOwner::Task(task_id),
|
||||||
@ -76,6 +59,10 @@ impl LoadCartridgeStep {
|
|||||||
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
|
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)))
|
Some(FirmwareState::GameRunning(GameRunningStep::new(task_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,7 @@ pub struct ResetStep;
|
|||||||
impl ResetStep {
|
impl ResetStep {
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
|
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
|
||||||
ctx.os.sessions().clear_vm_sessions();
|
ctx.os.vm().reset(ctx.vm);
|
||||||
ctx.os.vm().reset_global_runtime_state();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
|
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
use crate::firmware::prometeu_context::PrometeuContext;
|
||||||
use prometeu_hal::log::LogSource;
|
use prometeu_hal::log::LogSource;
|
||||||
use prometeu_system::process::ProcessKind;
|
|
||||||
use prometeu_system::task::{TaskId, TaskState};
|
use prometeu_system::task::{TaskId, TaskState};
|
||||||
use prometeu_system::{CrashReport, SystemProfileAction};
|
use prometeu_system::{CrashReport, SystemProfileAction};
|
||||||
|
|
||||||
@ -60,36 +59,7 @@ impl ShellRunningStep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let process_kind = match ctx.os.lifecycle().process_kind_for_task(self.task_id) {
|
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform);
|
||||||
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 {
|
if let Some(report) = outcome.crash {
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
use crate::firmware::boot_target::BootTarget;
|
use crate::firmware::boot_target::BootTarget;
|
||||||
use prometeu_hal::{InputSignals, RuntimePlatform};
|
use prometeu_hal::{InputSignals, RuntimePlatform};
|
||||||
use prometeu_system::{PrometeuHub, SystemOS};
|
use prometeu_system::{PrometeuHub, SystemOS};
|
||||||
|
use prometeu_vm::VirtualMachine;
|
||||||
|
|
||||||
pub struct PrometeuContext<'a> {
|
pub struct PrometeuContext<'a> {
|
||||||
|
pub vm: &'a mut VirtualMachine,
|
||||||
pub os: &'a mut SystemOS,
|
pub os: &'a mut SystemOS,
|
||||||
pub hub: &'a mut PrometeuHub,
|
pub hub: &'a mut PrometeuHub,
|
||||||
pub boot_target: &'a BootTarget,
|
pub boot_target: &'a BootTarget,
|
||||||
|
|||||||
@ -40,7 +40,7 @@ pub enum DebugResponse {
|
|||||||
Breakpoints { pcs: Vec<usize> },
|
Breakpoints { pcs: Vec<usize> },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct HandshakeCartridge {
|
pub struct HandshakeCartridge {
|
||||||
pub app_id: u32,
|
pub app_id: u32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|||||||
@ -4,10 +4,7 @@ mod programs;
|
|||||||
mod services;
|
mod services;
|
||||||
|
|
||||||
pub use crash_report::CrashReport;
|
pub use crash_report::CrashReport;
|
||||||
pub use os::{
|
pub use os::{LifecycleError, LifecycleOperation, SystemOS};
|
||||||
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
|
|
||||||
LifecycleOperation, SessionError, SystemOS,
|
|
||||||
};
|
|
||||||
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
||||||
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
|
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
|
||||||
pub use services::async_work::{
|
pub use services::async_work::{
|
||||||
@ -15,9 +12,6 @@ pub use services::async_work::{
|
|||||||
AsyncWorkJobKind, AsyncWorkJobOutcome, AsyncWorkLane, AsyncWorkLaneConfig,
|
AsyncWorkJobKind, AsyncWorkJobOutcome, AsyncWorkLane, AsyncWorkLaneConfig,
|
||||||
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
|
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
|
||||||
};
|
};
|
||||||
pub use services::foreground::{
|
|
||||||
ForegroundOwner, ForegroundStack, ForegroundStackError, ResidentGame, ResidentGameState,
|
|
||||||
};
|
|
||||||
pub use services::fs;
|
pub use services::fs;
|
||||||
pub use services::game_library::{
|
pub use services::game_library::{
|
||||||
GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root,
|
GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root,
|
||||||
@ -29,5 +23,4 @@ pub use services::vm_runtime::{
|
|||||||
LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff,
|
LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff,
|
||||||
RenderWorkerHandoffWait, RenderWorkerOwnership, VirtualMachineRuntime,
|
RenderWorkerHandoffWait, RenderWorkerOwnership, VirtualMachineRuntime,
|
||||||
};
|
};
|
||||||
pub use services::vm_session;
|
|
||||||
pub use services::windows;
|
pub use services::windows;
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
use crate::CrashReport;
|
use crate::CrashReport;
|
||||||
use crate::os::SystemOS;
|
use crate::os::SystemOS;
|
||||||
use crate::os::{
|
use crate::os::{LifecycleError, LifecycleOperation};
|
||||||
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
|
use crate::process::{ProcessId, ProcessState};
|
||||||
LifecycleOperation,
|
use crate::task::{TaskId, TaskState};
|
||||||
};
|
|
||||||
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 struct LifecycleFacade<'a> {
|
||||||
pub(in crate::os) os: &'a mut SystemOS,
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
@ -20,145 +14,28 @@ impl<'a> LifecycleFacade<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_state_for_task(&self, task_id: TaskId) -> Result<ProcessState, LifecycleError> {
|
pub fn process_state_for_task(&self, task_id: TaskId) -> Result<ProcessState, LifecycleError> {
|
||||||
process_for_task(self.os, task_id).map(|process| process.state)
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
}
|
self.os
|
||||||
|
.process_manager
|
||||||
pub fn process_kind_for_task(&self, task_id: TaskId) -> Result<ProcessKind, LifecycleError> {
|
.get(process_id)
|
||||||
process_for_task(self.os, task_id).map(|process| process.kind)
|
.map(|process| process.state)
|
||||||
|
.ok_or(LifecycleError::ProcessNotFound(process_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
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)?;
|
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.task_manager.set_foreground(task_id);
|
||||||
self.os.process_manager.mark_running(process_id);
|
self.os.process_manager.mark_running(process_id);
|
||||||
|
|
||||||
Ok(())
|
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> {
|
pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
self.os.task_manager.mark_suspended(task_id);
|
self.os.task_manager.mark_suspended(task_id);
|
||||||
self.os.process_manager.mark_suspended(process_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -177,32 +54,8 @@ impl<'a> LifecycleFacade<'a> {
|
|||||||
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
match task_kind_for_task(self.os, task_id)? {
|
self.os.task_manager.set_foreground(task_id);
|
||||||
TaskKind::Game => {
|
self.os.process_manager.mark_running(process_id);
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -212,12 +65,6 @@ impl<'a> LifecycleFacade<'a> {
|
|||||||
|
|
||||||
self.os.task_manager.close_task(task_id);
|
self.os.task_manager.close_task(task_id);
|
||||||
self.os.process_manager.mark_stopped(process_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -231,22 +78,11 @@ impl<'a> LifecycleFacade<'a> {
|
|||||||
|
|
||||||
self.os.task_manager.mark_crashed(task_id);
|
self.os.task_manager.mark_crashed(task_id);
|
||||||
self.os.process_manager.mark_crashed(process_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(())
|
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> {
|
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))?;
|
let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
||||||
|
|
||||||
@ -256,22 +92,3 @@ fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result<ProcessId, Life
|
|||||||
|
|
||||||
Ok(task.process_id)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,6 +6,6 @@ mod window;
|
|||||||
|
|
||||||
pub use fs::FsFacade;
|
pub use fs::FsFacade;
|
||||||
pub use lifecycle::LifecycleFacade;
|
pub use lifecycle::LifecycleFacade;
|
||||||
pub use sessions::{SessionError, SessionsFacade};
|
pub use sessions::SessionsFacade;
|
||||||
pub use vm::VmFacade;
|
pub use vm::VmFacade;
|
||||||
pub use window::WindowFacade;
|
pub use window::WindowFacade;
|
||||||
|
|||||||
@ -1,209 +1,44 @@
|
|||||||
use crate::CrashReport;
|
|
||||||
use crate::os::LifecycleError;
|
|
||||||
use crate::os::SystemOS;
|
use crate::os::SystemOS;
|
||||||
use crate::process::ProcessId;
|
|
||||||
use crate::task::TaskId;
|
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 struct SessionsFacade<'a> {
|
||||||
pub(in crate::os) os: &'a mut SystemOS,
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SessionsFacade<'a> {
|
impl<'a> SessionsFacade<'a> {
|
||||||
#[cfg(test)]
|
|
||||||
pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
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 title = title.into();
|
||||||
let process_id = self.os.process_manager.spawn_vm_game(app_id, title.clone());
|
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);
|
let task_id = self.os.task_manager.create_game_task(process_id, app_id, title);
|
||||||
self.os.lifecycle().set_game_foreground_task(task_id)?;
|
self.os
|
||||||
self.try_create_vm_session_for_task(task_id)?;
|
.lifecycle()
|
||||||
|
.set_foreground_task(task_id)
|
||||||
|
.expect("newly created game task should have an associated process");
|
||||||
|
|
||||||
Ok(task_id)
|
task_id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
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 title = title.into();
|
||||||
let process_id = self.os.process_manager.spawn_vm_shell(app_id, title.clone());
|
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);
|
let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title);
|
||||||
self.os.lifecycle().set_shell_foreground_task(task_id)?;
|
self.os
|
||||||
self.try_create_vm_session_for_task(task_id)?;
|
.lifecycle()
|
||||||
|
.set_foreground_task(task_id)
|
||||||
|
.expect("newly created shell task should have an associated process");
|
||||||
|
|
||||||
Ok(task_id)
|
task_id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
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 title = title.into();
|
||||||
let process_id = self.os.process_manager.spawn_native_shell(app_id, title.clone());
|
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);
|
let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title);
|
||||||
self.os.lifecycle().set_shell_foreground_task(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
|
self.os
|
||||||
.vm_sessions
|
.lifecycle()
|
||||||
.create_for_task(&task, &process, "", Some(cap_config), logs_count)
|
.set_foreground_task(task_id)
|
||||||
.map(|_| ())
|
.expect("newly created native shell task should have an associated process");
|
||||||
.map_err(SessionError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_initialize_session_cartridge(
|
task_id
|
||||||
&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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
use crate::CrashReport;
|
use crate::CrashReport;
|
||||||
use crate::os::{GameLifecycleEvent, SystemOS};
|
use crate::os::SystemOS;
|
||||||
use crate::task::TaskId;
|
|
||||||
use crate::vm_session::VmSession;
|
|
||||||
use crate::{RenderWorkerConfig, RenderWorkerController};
|
use crate::{RenderWorkerConfig, RenderWorkerController};
|
||||||
use prometeu_bytecode::Value;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
use prometeu_hal::app_mode::AppMode;
|
||||||
|
use prometeu_hal::cartridge::Cartridge;
|
||||||
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
|
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
|
||||||
use prometeu_hal::{
|
use prometeu_hal::{
|
||||||
FrameId, InputSignals, RenderOwnership, RenderWorkerBackend, RenderWorkerError,
|
FrameId, InputSignals, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink,
|
||||||
RenderWorkerFrameSink, RuntimePlatform,
|
RuntimePlatform,
|
||||||
};
|
};
|
||||||
|
use prometeu_vm::VirtualMachine;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
pub struct VmFacade<'a> {
|
pub struct VmFacade<'a> {
|
||||||
@ -17,216 +16,88 @@ pub struct VmFacade<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VmFacade<'a> {
|
impl<'a> VmFacade<'a> {
|
||||||
pub fn tick_session(
|
pub fn initialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
task_id: TaskId,
|
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,
|
||||||
signals: &InputSignals,
|
signals: &InputSignals,
|
||||||
platform: &mut dyn RuntimePlatform,
|
platform: &mut dyn RuntimePlatform,
|
||||||
) -> Option<CrashReport> {
|
) -> Option<CrashReport> {
|
||||||
self.deliver_pending_game_lifecycle_events_to_session(task_id);
|
self.os.vm_runtime.tick(
|
||||||
|
|
||||||
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.log_service,
|
||||||
&mut self.os.fs,
|
&mut self.os.fs,
|
||||||
&mut session.fs_state,
|
&mut self.os.fs_state,
|
||||||
&mut self.os.memcard,
|
&mut self.os.memcard,
|
||||||
&mut session.open_files,
|
&mut self.os.open_files,
|
||||||
&mut session.next_handle,
|
&mut self.os.next_handle,
|
||||||
&mut session.vm,
|
vm,
|
||||||
signals,
|
signals,
|
||||||
platform,
|
platform,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deliver_pending_game_lifecycle_events_to_session(&mut self, task_id: TaskId) {
|
pub fn reset(&mut self, vm: &mut VirtualMachine) {
|
||||||
let Some(session) = self.os.vm_sessions.for_task(task_id) else {
|
self.os.vm_runtime.reset(vm);
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if session.runtime.current_cartridge_app_mode != AppMode::Game {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
pub fn debug_step_instruction(
|
||||||
self.os.clear_cartridge_service_state();
|
|
||||||
self.os.vm_runtime.clear_cartridge_state();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn debug_step_active_session(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
|
vm: &mut VirtualMachine,
|
||||||
platform: &mut dyn RuntimePlatform,
|
platform: &mut dyn RuntimePlatform,
|
||||||
) -> Option<CrashReport> {
|
) -> Option<CrashReport> {
|
||||||
let Some(task_id) = self.active_session_task_id() else {
|
self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, platform)
|
||||||
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 {
|
pub fn paused(&self) -> bool {
|
||||||
self.active_session()
|
self.os.vm_runtime.paused
|
||||||
.map(|session| session.runtime.paused)
|
|
||||||
.unwrap_or(self.os.vm_runtime.paused)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_paused(&mut self, paused: bool) {
|
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;
|
self.os.vm_runtime.paused = paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_inspection_active(&mut self, active: bool) {
|
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;
|
self.os.vm_runtime.inspection_active = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_debug_step(&mut self) {
|
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;
|
self.os.vm_runtime.debug_step_request = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_step_requested(&self) -> bool {
|
pub fn debug_step_requested(&self) -> bool {
|
||||||
self.active_session()
|
self.os.vm_runtime.debug_step_request
|
||||||
.map(|session| session.runtime.debug_step_request)
|
|
||||||
.unwrap_or(self.os.vm_runtime.debug_step_request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logical_frame_active(&self) -> bool {
|
pub fn logical_frame_active(&self) -> bool {
|
||||||
self.active_session()
|
self.os.vm_runtime.logical_frame_active
|
||||||
.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 {
|
pub fn tick_index(&self) -> u64 {
|
||||||
self.active_session()
|
self.os.vm_runtime.tick_index
|
||||||
.map(|session| session.runtime.tick_index)
|
|
||||||
.unwrap_or(self.os.vm_runtime.tick_index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logical_frame_index(&self) -> u64 {
|
pub fn logical_frame_index(&self) -> u64 {
|
||||||
self.active_session()
|
self.os.vm_runtime.logical_frame_index
|
||||||
.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 {
|
pub fn last_frame_cpu_time_us(&self) -> u64 {
|
||||||
self.active_session()
|
self.os.vm_runtime.last_frame_cpu_time_us
|
||||||
.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 {
|
pub fn completed_logical_frames(&self) -> u64 {
|
||||||
self.active_session()
|
self.os.vm_runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed).into()
|
||||||
.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 {
|
pub fn telemetry_snapshot(&self) -> TelemetryFrame {
|
||||||
self.active_session()
|
self.os.vm_runtime.atomic_telemetry.snapshot()
|
||||||
.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)
|
pub fn start_render_worker<B, S>(&mut self, config: RenderWorkerConfig, backend: B, sink: S)
|
||||||
@ -249,72 +120,27 @@ impl<'a> VmFacade<'a> {
|
|||||||
self.os.vm_runtime.record_repeated_render_worker_frame(frame_id);
|
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 {
|
pub fn cert_config(&self) -> &CertificationConfig {
|
||||||
&self.os.vm_runtime.certifier.config
|
&self.os.vm_runtime.certifier.config
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn last_crash_report(&self) -> Option<&CrashReport> {
|
pub fn last_crash_report(&self) -> Option<&CrashReport> {
|
||||||
self.active_session()
|
self.os.vm_runtime.last_crash_report.as_ref()
|
||||||
.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 {
|
pub fn current_app_id(&self) -> u32 {
|
||||||
self.active_session()
|
self.os.vm_runtime.current_app_id
|
||||||
.map(|session| session.runtime.current_app_id)
|
|
||||||
.unwrap_or(self.os.vm_runtime.current_app_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_cartridge_title(&self) -> String {
|
pub fn current_cartridge_title(&self) -> String {
|
||||||
self.active_session()
|
self.os.vm_runtime.current_cartridge_title.clone()
|
||||||
.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 {
|
pub fn current_cartridge_app_version(&self) -> String {
|
||||||
self.active_session()
|
self.os.vm_runtime.current_cartridge_app_version.clone()
|
||||||
.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 {
|
pub fn current_cartridge_app_mode(&self) -> AppMode {
|
||||||
self.active_session()
|
self.os.vm_runtime.current_cartridge_app_mode
|
||||||
.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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
use crate::process::ProcessId;
|
use crate::process::ProcessId;
|
||||||
use crate::task::{TaskId, TaskState};
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum LifecycleOperation {
|
pub enum LifecycleOperation {
|
||||||
SetForeground,
|
SetForeground,
|
||||||
@ -29,7 +15,4 @@ pub enum LifecycleError {
|
|||||||
TaskNotFound(TaskId),
|
TaskNotFound(TaskId),
|
||||||
ProcessNotFound(ProcessId),
|
ProcessNotFound(ProcessId),
|
||||||
InvalidTransition { task_id: TaskId, from: TaskState, operation: LifecycleOperation },
|
InvalidTransition { task_id: TaskId, from: TaskState, operation: LifecycleOperation },
|
||||||
ResidentGameAlreadyExists { existing: TaskId, requested: TaskId },
|
|
||||||
ForegroundShellAlreadyExists { existing: TaskId, requested: TaskId },
|
|
||||||
TaskIsNotResidentGame(TaskId),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,6 @@ mod facades;
|
|||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
mod system_os;
|
mod system_os;
|
||||||
|
|
||||||
pub use facades::{
|
pub use facades::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade};
|
||||||
FsFacade, LifecycleFacade, SessionError, SessionsFacade, VmFacade, WindowFacade,
|
pub use lifecycle::{LifecycleError, LifecycleOperation};
|
||||||
};
|
|
||||||
pub use lifecycle::{
|
|
||||||
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
|
|
||||||
LifecycleOperation,
|
|
||||||
};
|
|
||||||
pub use system_os::SystemOS;
|
pub use system_os::SystemOS;
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
use crate::VirtualMachineRuntime;
|
use crate::VirtualMachineRuntime;
|
||||||
use crate::fs::{FsState, VirtualFS};
|
use crate::fs::{FsState, VirtualFS};
|
||||||
use crate::os::{
|
use crate::os::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade};
|
||||||
FsFacade, GameLifecycleEvent, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade,
|
|
||||||
};
|
|
||||||
use crate::process::ProcessManager;
|
use crate::process::ProcessManager;
|
||||||
use crate::services::foreground::ForegroundStack;
|
|
||||||
use crate::services::memcard::MemcardService;
|
use crate::services::memcard::MemcardService;
|
||||||
use crate::services::vm_session::VmSessionRegistry;
|
|
||||||
use crate::services::windows::WindowManager;
|
use crate::services::windows::WindowManager;
|
||||||
use crate::task::TaskManager;
|
use crate::task::TaskManager;
|
||||||
use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource};
|
use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource};
|
||||||
@ -18,9 +14,6 @@ pub struct SystemOS {
|
|||||||
pub(super) vm_runtime: VirtualMachineRuntime,
|
pub(super) vm_runtime: VirtualMachineRuntime,
|
||||||
pub(super) process_manager: ProcessManager,
|
pub(super) process_manager: ProcessManager,
|
||||||
pub(super) task_manager: TaskManager,
|
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) window_manager: WindowManager,
|
||||||
pub(super) log_service: LogService,
|
pub(super) log_service: LogService,
|
||||||
pub(super) fs: VirtualFS,
|
pub(super) fs: VirtualFS,
|
||||||
@ -40,9 +33,6 @@ impl SystemOS {
|
|||||||
),
|
),
|
||||||
process_manager: ProcessManager::new(),
|
process_manager: ProcessManager::new(),
|
||||||
task_manager: TaskManager::new(),
|
task_manager: TaskManager::new(),
|
||||||
foreground_stack: ForegroundStack::new(),
|
|
||||||
vm_sessions: VmSessionRegistry::new(),
|
|
||||||
game_lifecycle_events: Vec::new(),
|
|
||||||
window_manager: WindowManager::new(),
|
window_manager: WindowManager::new(),
|
||||||
log_service,
|
log_service,
|
||||||
fs: VirtualFS::new(),
|
fs: VirtualFS::new(),
|
||||||
@ -143,33 +133,15 @@ impl SystemOS {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError};
|
use crate::os::{LifecycleError, LifecycleOperation};
|
||||||
use crate::process::{ProcessId, ProcessKind, ProcessState};
|
use crate::process::ProcessState;
|
||||||
use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState};
|
|
||||||
use crate::task::{TaskId, TaskState};
|
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 {
|
fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState {
|
||||||
let task = os.task_manager.get(task_id).expect("task should exist");
|
let task = os.task_manager.get(task_id).expect("task should exist");
|
||||||
os.process_manager.get(task.process_id).expect("process should exist").state
|
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]
|
#[test]
|
||||||
fn set_foreground_task_marks_task_and_process_running() {
|
fn set_foreground_task_marks_task_and_process_running() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
@ -185,358 +157,6 @@ mod tests {
|
|||||||
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running);
|
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]
|
#[test]
|
||||||
fn suspend_task_marks_task_and_process_suspended() {
|
fn suspend_task_marks_task_and_process_suspended() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
@ -564,10 +184,6 @@ mod tests {
|
|||||||
TaskState::Foreground
|
TaskState::Foreground
|
||||||
);
|
);
|
||||||
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running);
|
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]
|
#[test]
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::task::TaskId;
|
|
||||||
use crate::{CrashReport, GameLibrary, SystemOS};
|
use crate::{CrashReport, GameLibrary, SystemOS};
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
@ -6,6 +5,7 @@ use prometeu_hal::{
|
|||||||
FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform,
|
FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform,
|
||||||
ShellUiFramePacket,
|
ShellUiFramePacket,
|
||||||
};
|
};
|
||||||
|
use prometeu_vm::VirtualMachine;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 };
|
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 };
|
||||||
@ -174,23 +174,7 @@ impl PrometeuHub {
|
|||||||
pub fn update_shell_profile(
|
pub fn update_shell_profile(
|
||||||
&mut self,
|
&mut self,
|
||||||
os: &mut SystemOS,
|
os: &mut SystemOS,
|
||||||
platform: &mut dyn RuntimePlatform,
|
vm: &mut VirtualMachine,
|
||||||
) -> 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,
|
signals: &InputSignals,
|
||||||
platform: &mut dyn RuntimePlatform,
|
platform: &mut dyn RuntimePlatform,
|
||||||
) -> SystemProfileUpdate {
|
) -> SystemProfileUpdate {
|
||||||
@ -201,7 +185,7 @@ impl PrometeuHub {
|
|||||||
if platform.input().pad().start().down {
|
if platform.input().pad().start().down {
|
||||||
action = Some(SystemProfileAction::CloseShell);
|
action = Some(SystemProfileAction::CloseShell);
|
||||||
} else if action != Some(SystemProfileAction::CloseShell) {
|
} else if action != Some(SystemProfileAction::CloseShell) {
|
||||||
crash = os.vm().tick_session(task_id, signals, platform);
|
crash = os.vm().tick(vm, signals, platform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,22 +193,6 @@ impl PrometeuHub {
|
|||||||
|
|
||||||
SystemProfileUpdate { crash, action }
|
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(
|
fn action_for_click(
|
||||||
@ -430,34 +398,6 @@ fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn shell_a_button_click_emits_launch_action() {
|
fn shell_a_button_click_emits_launch_action() {
|
||||||
@ -578,35 +518,4 @@ mod tests {
|
|||||||
CLOSE_BUTTON.y + CLOSE_BUTTON.h - 1
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,330 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,8 @@
|
|||||||
pub mod async_work;
|
pub mod async_work;
|
||||||
pub mod foreground;
|
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
pub mod game_library;
|
pub mod game_library;
|
||||||
pub mod memcard;
|
pub mod memcard;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod task;
|
pub mod task;
|
||||||
pub mod vm_runtime;
|
pub mod vm_runtime;
|
||||||
pub mod vm_session;
|
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|||||||
@ -32,7 +32,6 @@ impl VirtualMachineRuntime {
|
|||||||
gfx2d_commands: Vec::new(),
|
gfx2d_commands: Vec::new(),
|
||||||
gfxui_commands: Vec::new(),
|
gfxui_commands: Vec::new(),
|
||||||
logs_written_this_frame: HashMap::new(),
|
logs_written_this_frame: HashMap::new(),
|
||||||
game_lifecycle_events_delivered: Vec::new(),
|
|
||||||
atomic_telemetry,
|
atomic_telemetry,
|
||||||
last_crash_report: None,
|
last_crash_report: None,
|
||||||
certifier: Certifier::new(cap_config.unwrap_or_default()),
|
certifier: Certifier::new(cap_config.unwrap_or_default()),
|
||||||
@ -136,7 +135,6 @@ impl VirtualMachineRuntime {
|
|||||||
self.gfx2d_commands.clear();
|
self.gfx2d_commands.clear();
|
||||||
self.gfxui_commands.clear();
|
self.gfxui_commands.clear();
|
||||||
self.logs_written_this_frame.clear();
|
self.logs_written_this_frame.clear();
|
||||||
self.game_lifecycle_events_delivered.clear();
|
|
||||||
|
|
||||||
self.last_crash_report = None;
|
self.last_crash_report = None;
|
||||||
|
|
||||||
@ -193,21 +191,6 @@ 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(
|
pub fn initialize_vm(
|
||||||
&mut self,
|
&mut self,
|
||||||
log_service: &mut LogService,
|
log_service: &mut LogService,
|
||||||
@ -241,26 +224,3 @@ 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@ mod tests;
|
|||||||
mod tick;
|
mod tick;
|
||||||
|
|
||||||
use crate::CrashReport;
|
use crate::CrashReport;
|
||||||
use crate::os::GameLifecycleEvent;
|
|
||||||
pub use frame_scheduler::FrameScheduler;
|
pub use frame_scheduler::FrameScheduler;
|
||||||
use prometeu_bytecode::string_materialization_count;
|
use prometeu_bytecode::string_materialization_count;
|
||||||
use prometeu_hal::app_mode::AppMode;
|
use prometeu_hal::app_mode::AppMode;
|
||||||
@ -50,7 +49,6 @@ pub struct VirtualMachineRuntime {
|
|||||||
pub gfx2d_commands: Vec<Gfx2dCommand>,
|
pub gfx2d_commands: Vec<Gfx2dCommand>,
|
||||||
pub gfxui_commands: Vec<GfxUiCommand>,
|
pub gfxui_commands: Vec<GfxUiCommand>,
|
||||||
pub logs_written_this_frame: HashMap<u32, u32>,
|
pub logs_written_this_frame: HashMap<u32, u32>,
|
||||||
pub game_lifecycle_events_delivered: Vec<GameLifecycleEvent>,
|
|
||||||
pub atomic_telemetry: Arc<AtomicTelemetry>,
|
pub atomic_telemetry: Arc<AtomicTelemetry>,
|
||||||
pub last_crash_report: Option<CrashReport>,
|
pub last_crash_report: Option<CrashReport>,
|
||||||
pub certifier: Certifier,
|
pub certifier: Certifier,
|
||||||
|
|||||||
@ -1,281 +0,0 @@
|
|||||||
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -28,8 +28,6 @@ pub struct HostDebugger {
|
|||||||
last_telemetry_frame: u64,
|
last_telemetry_frame: u64,
|
||||||
/// Last fault summary sent to the debugger client.
|
/// Last fault summary sent to the debugger client.
|
||||||
last_fault_summary: Option<String>,
|
last_fault_summary: Option<String>,
|
||||||
/// Debug cartridge metadata captured before the VM session exists.
|
|
||||||
debug_cartridge: Option<HandshakeCartridge>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HostDebugger {
|
impl Default for HostDebugger {
|
||||||
@ -48,25 +46,19 @@ impl HostDebugger {
|
|||||||
last_log_seq: 0,
|
last_log_seq: 0,
|
||||||
last_telemetry_frame: 0,
|
last_telemetry_frame: 0,
|
||||||
last_fault_summary: None,
|
last_fault_summary: None,
|
||||||
debug_cartridge: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configures the debugger based on the boot target.
|
/// Configures the debugger based on the boot target.
|
||||||
/// If debug mode is enabled, it binds to the specified TCP port.
|
/// 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 {
|
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
|
||||||
self.waiting_for_start = true;
|
self.waiting_for_start = true;
|
||||||
|
|
||||||
// Pre-load cartridge metadata so the Handshake message can contain
|
// Pre-load cartridge metadata so the Handshake message can contain
|
||||||
// valid information before the VM session is created.
|
// valid information about the App being debugged.
|
||||||
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
||||||
self.debug_cartridge = Some(HandshakeCartridge {
|
let _ = firmware.os.vm().initialize(&mut firmware.vm, &cartridge);
|
||||||
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)) {
|
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
||||||
@ -124,7 +116,12 @@ impl HostDebugger {
|
|||||||
let handshake = DebugResponse::Handshake {
|
let handshake = DebugResponse::Handshake {
|
||||||
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
||||||
runtime_version: "0.1".to_string(),
|
runtime_version: "0.1".to_string(),
|
||||||
cartridge: self.handshake_cartridge(firmware),
|
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(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
self.send_response(handshake);
|
self.send_response(handshake);
|
||||||
} else {
|
} else {
|
||||||
@ -204,7 +201,7 @@ impl HostDebugger {
|
|||||||
DebugCommand::Step => {
|
DebugCommand::Step => {
|
||||||
// Execute exactly one instruction and keep paused.
|
// Execute exactly one instruction and keep paused.
|
||||||
firmware.os.vm().set_paused(true);
|
firmware.os.vm().set_paused(true);
|
||||||
let _ = firmware.os.vm().debug_step_active_session(platform);
|
let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, platform);
|
||||||
}
|
}
|
||||||
DebugCommand::StepFrame => {
|
DebugCommand::StepFrame => {
|
||||||
// Execute until the end of the current logical frame.
|
// Execute until the end of the current logical frame.
|
||||||
@ -213,10 +210,10 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
DebugCommand::GetState => {
|
DebugCommand::GetState => {
|
||||||
// Return detailed VM register and stack state.
|
// Return detailed VM register and stack state.
|
||||||
let stack_top = firmware.os.vm().active_operand_stack_top(10);
|
let stack_top = firmware.vm.operand_stack_top(10);
|
||||||
|
|
||||||
let resp = DebugResponse::GetState {
|
let resp = DebugResponse::GetState {
|
||||||
pc: firmware.os.vm().active_pc().unwrap_or(0),
|
pc: firmware.vm.pc(),
|
||||||
stack_top,
|
stack_top,
|
||||||
frame_index: firmware.os.vm().logical_frame_index(),
|
frame_index: firmware.os.vm().logical_frame_index(),
|
||||||
app_id: firmware.os.vm().current_app_id(),
|
app_id: firmware.os.vm().current_app_id(),
|
||||||
@ -224,37 +221,18 @@ impl HostDebugger {
|
|||||||
self.send_response(resp);
|
self.send_response(resp);
|
||||||
}
|
}
|
||||||
DebugCommand::SetBreakpoint { pc } => {
|
DebugCommand::SetBreakpoint { pc } => {
|
||||||
let _ = firmware.os.vm().insert_active_breakpoint(pc);
|
firmware.vm.insert_breakpoint(pc);
|
||||||
}
|
}
|
||||||
DebugCommand::ListBreakpoints => {
|
DebugCommand::ListBreakpoints => {
|
||||||
let pcs = firmware.os.vm().active_breakpoints_list();
|
let pcs = firmware.vm.breakpoints_list();
|
||||||
self.send_response(DebugResponse::Breakpoints { pcs });
|
self.send_response(DebugResponse::Breakpoints { pcs });
|
||||||
}
|
}
|
||||||
DebugCommand::ClearBreakpoint { pc } => {
|
DebugCommand::ClearBreakpoint { pc } => {
|
||||||
let _ = firmware.os.vm().remove_active_breakpoint(pc);
|
firmware.vm.remove_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(
|
pub(crate) fn cert_event_from_snapshot(
|
||||||
tag: u16,
|
tag: u16,
|
||||||
telemetry: TelemetryFrame,
|
telemetry: TelemetryFrame,
|
||||||
@ -337,7 +315,7 @@ impl HostDebugger {
|
|||||||
// Map specific internal log tags to protocol events.
|
// Map specific internal log tags to protocol events.
|
||||||
if event.tag == 0xDEB1 {
|
if event.tag == 0xDEB1 {
|
||||||
self.send_event(DebugEvent::BreakpointHit {
|
self.send_event(DebugEvent::BreakpointHit {
|
||||||
pc: firmware.os.vm().active_pc().unwrap_or(0),
|
pc: firmware.vm.pc(),
|
||||||
frame_index: firmware.os.vm().logical_frame_index(),
|
frame_index: firmware.os.vm().logical_frame_index(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -431,52 +409,6 @@ mod tests {
|
|||||||
assert!(firmware.os.vm().debug_step_requested());
|
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]
|
#[test]
|
||||||
fn handle_command_start_leaves_waiting_for_start_mode() {
|
fn handle_command_start_leaves_waiting_for_start_mode() {
|
||||||
let mut debugger = HostDebugger::new();
|
let mut debugger = HostDebugger::new();
|
||||||
|
|||||||
@ -43,27 +43,9 @@ impl FbViewport {
|
|||||||
|
|
||||||
pub struct HostInputHandler {
|
pub struct HostInputHandler {
|
||||||
pub signals: InputSignals,
|
pub signals: InputSignals,
|
||||||
system_controls: HostSystemControls,
|
|
||||||
display_size: (usize, usize),
|
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 {
|
impl Default for HostInputHandler {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new((480, 270))
|
Self::new((480, 270))
|
||||||
@ -72,51 +54,35 @@ impl Default for HostInputHandler {
|
|||||||
|
|
||||||
impl HostInputHandler {
|
impl HostInputHandler {
|
||||||
pub fn new(display_size: (usize, usize)) -> Self {
|
pub fn new(display_size: (usize, usize)) -> Self {
|
||||||
Self {
|
Self { signals: InputSignals::default(), display_size }
|
||||||
signals: InputSignals::default(),
|
|
||||||
system_controls: HostSystemControls::default(),
|
|
||||||
display_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
KeyCode::ArrowRight => self.signals.right_signal = is_down,
|
|
||||||
|
|
||||||
KeyCode::KeyA => self.signals.a_signal = is_down,
|
|
||||||
KeyCode::KeyD => self.signals.b_signal = is_down,
|
|
||||||
KeyCode::KeyW => self.signals.x_signal = is_down,
|
|
||||||
KeyCode::KeyS => self.signals.y_signal = is_down,
|
|
||||||
KeyCode::KeyQ => self.signals.l_signal = is_down,
|
|
||||||
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,
|
|
||||||
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
|
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
if let PhysicalKey::Code(code) = event.physical_key {
|
if let PhysicalKey::Code(code) = event.physical_key {
|
||||||
self.handle_key_code(code, event.state);
|
let is_down = event.state == ElementState::Pressed;
|
||||||
|
|
||||||
|
match code {
|
||||||
|
KeyCode::ArrowUp => self.signals.up_signal = is_down,
|
||||||
|
KeyCode::ArrowDown => self.signals.down_signal = is_down,
|
||||||
|
KeyCode::ArrowLeft => self.signals.left_signal = is_down,
|
||||||
|
KeyCode::ArrowRight => self.signals.right_signal = is_down,
|
||||||
|
|
||||||
|
KeyCode::KeyA => self.signals.a_signal = is_down,
|
||||||
|
KeyCode::KeyD => self.signals.b_signal = is_down,
|
||||||
|
KeyCode::KeyW => self.signals.x_signal = is_down,
|
||||||
|
KeyCode::KeyS => self.signals.y_signal = is_down,
|
||||||
|
KeyCode::KeyQ => self.signals.l_signal = is_down,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,81 +134,3 @@ 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,8 +10,8 @@ use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
|
|||||||
use prometeu_drivers::hardware::Hardware;
|
use prometeu_drivers::hardware::Hardware;
|
||||||
use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks};
|
use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks};
|
||||||
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
|
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
|
||||||
|
use prometeu_hal::RuntimePlatform;
|
||||||
use prometeu_hal::telemetry::CertificationConfig;
|
use prometeu_hal::telemetry::CertificationConfig;
|
||||||
use prometeu_hal::{OwnedRgba8888Frame, RenderOwnership, RuntimePlatform};
|
|
||||||
use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root};
|
use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@ -88,12 +88,8 @@ fn desired_control_flow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_present_worker_frame(
|
fn should_present_worker_frame(state: &FirmwareState) -> bool {
|
||||||
state: &FirmwareState,
|
matches!(state, FirmwareState::GameRunning(_))
|
||||||
frame: &OwnedRgba8888Frame,
|
|
||||||
active_ownership: RenderOwnership,
|
|
||||||
) -> bool {
|
|
||||||
matches!(state, FirmwareState::GameRunning(_)) && frame.ownership == active_ownership
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Desktop implementation of the PROMETEU Runtime.
|
/// The Desktop implementation of the PROMETEU Runtime.
|
||||||
@ -316,13 +312,8 @@ impl ApplicationHandler for HostRunner {
|
|||||||
{
|
{
|
||||||
let frame = pixels.frame_mut();
|
let frame = pixels.frame_mut();
|
||||||
|
|
||||||
let active_ownership = self.firmware.os.vm().active_render_ownership();
|
if should_present_worker_frame(&self.firmware.state)
|
||||||
if let Some(worker_frame) = self.render_frame_store.latest_frame()
|
&& let Some(worker_frame) = self.render_frame_store.latest_frame()
|
||||||
&& should_present_worker_frame(
|
|
||||||
&self.firmware.state,
|
|
||||||
&worker_frame,
|
|
||||||
active_ownership,
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
self.firmware
|
self.firmware
|
||||||
.os
|
.os
|
||||||
@ -381,13 +372,6 @@ impl ApplicationHandler for HostRunner {
|
|||||||
self.firmware.os.fs().mount(Box::new(backend));
|
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).
|
// 3. Timing Management (The heart of determinism).
|
||||||
// We measure the elapsed time since the last iteration and add it to an
|
// 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
|
// accumulator. We then execute exactly as many 60Hz slices as the
|
||||||
@ -415,10 +399,7 @@ impl ApplicationHandler for HostRunner {
|
|||||||
// Sync pause state with audio.
|
// Sync pause state with audio.
|
||||||
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
|
// 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.
|
// being cleared by the OS if a new logical frame starts in this tick.
|
||||||
let lifecycle_audio_paused = self.firmware.game_lifecycle_audio_paused();
|
let is_paused = self.firmware.os.vm().paused() || self.debugger.waiting_for_start;
|
||||||
let is_paused = lifecycle_audio_paused
|
|
||||||
|| self.firmware.os.vm().paused()
|
|
||||||
|| self.debugger.waiting_for_start;
|
|
||||||
if is_paused != self.last_paused_state {
|
if is_paused != self.last_paused_state {
|
||||||
self.last_paused_state = is_paused;
|
self.last_paused_state = is_paused;
|
||||||
let cmd =
|
let cmd =
|
||||||
@ -433,9 +414,8 @@ impl ApplicationHandler for HostRunner {
|
|||||||
self.stats.record_frame();
|
self.stats.record_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
let active_ownership = self.firmware.os.vm().active_render_ownership();
|
if should_present_worker_frame(&self.firmware.state)
|
||||||
if let Some(worker_frame) = self.render_frame_store.latest_frame()
|
&& 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());
|
self.presentation.note_published_frame(worker_frame.frame_id.get());
|
||||||
} else {
|
} else {
|
||||||
@ -556,39 +536,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn worker_frame_presentation_requires_game_state_and_current_ownership() {
|
fn worker_frame_presentation_is_game_only() {
|
||||||
let active = RenderOwnership::new(3, AppMode::Game, 1);
|
assert!(should_present_worker_frame(&FirmwareState::GameRunning(GameRunningStep::new(
|
||||||
let current = OwnedRgba8888Frame::packed(FrameId::new(9), active, 1, 1, vec![0xFF00FFFF])
|
TaskId(1),
|
||||||
.expect("test frame");
|
))));
|
||||||
let stale = OwnedRgba8888Frame::packed(
|
assert!(!should_present_worker_frame(&FirmwareState::ShellRunning(ShellRunningStep::new(
|
||||||
FrameId::new(8),
|
TaskId(2)
|
||||||
RenderOwnership::new(2, AppMode::Game, 1),
|
),)));
|
||||||
1,
|
assert!(!should_present_worker_frame(&FirmwareState::HubHome(HubHomeStep)));
|
||||||
1,
|
|
||||||
vec![0xFF00FFFF],
|
|
||||||
)
|
|
||||||
.expect("test frame");
|
|
||||||
|
|
||||||
assert!(should_present_worker_frame(
|
|
||||||
&FirmwareState::GameRunning(GameRunningStep::new(TaskId(1))),
|
|
||||||
¤t,
|
|
||||||
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))),
|
|
||||||
¤t,
|
|
||||||
active,
|
|
||||||
));
|
|
||||||
assert!(!should_present_worker_frame(
|
|
||||||
&FirmwareState::HubHome(HubHomeStep),
|
|
||||||
¤t,
|
|
||||||
active,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":157,"LSN":53,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":37,"PLN":136,"LSN":51,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0043","status":"open","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-03","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0043","status":"open","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-03","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
|
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
|
||||||
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
|
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
|
||||||
{"type":"discussion","id":"DSC-0041","status":"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-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-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-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-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"}]}
|
{"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-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
{"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]}
|
{"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]}
|
||||||
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0004","status":"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-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
202
discussion/workflow/agendas/AGD-0003-system-run-cart.md
Normal file
202
discussion/workflow/agendas/AGD-0003-system-run-cart.md
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0045
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: SystemOS Library Root and Home Game Launch
|
||||||
|
status: accepted
|
||||||
|
created: 2026-07-03
|
||||||
|
resolved:
|
||||||
|
decision:
|
||||||
|
tags: [runtime, os, hub, cartridge, launcher, library, game, architecture]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
A `DEC-0035` fechou que `system.run_cart` nao deve existir como syscall
|
||||||
|
userland e que boot direto por host/CLI/debug deve permanecer.
|
||||||
|
|
||||||
|
Isso resolveu a autoridade negativa, mas ainda nao resolveu a experiencia
|
||||||
|
desejada para o SO: apontar o runtime para um diretorio raiz de biblioteca,
|
||||||
|
fazer a Home listar os cartuchos encontrados ali e permitir que o usuario clique
|
||||||
|
em um jogo para carrega-lo.
|
||||||
|
|
||||||
|
O objetivo pratico e um "Switch feio": uma lista simples na Home, sem UX final,
|
||||||
|
sem marketplace e sem catalogo rico, mas capaz de provar que o SO, e nao o
|
||||||
|
guest, escolhe e carrega um cartucho a partir de uma biblioteca local.
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
Hoje temos dois caminhos separados:
|
||||||
|
|
||||||
|
- boot direto por `--run <cart>` funciona e carrega um cartucho unico;
|
||||||
|
- Hub/Home existe, mas mostra acoes fixas de Shell nativo e nao lista jogos de
|
||||||
|
uma biblioteca real.
|
||||||
|
|
||||||
|
Sem uma biblioteca de cartuchos visivel para o SO, o usuario nao consegue
|
||||||
|
iniciar jogos a partir da Home. Isso deixa a agenda `system-run-cart` incompleta
|
||||||
|
do ponto de vista de produto: removemos o `run_cart` userland, mas ainda falta a
|
||||||
|
alternativa correta, que e o SO iniciar cartuchos por acao de usuario.
|
||||||
|
|
||||||
|
## Pontos Criticos
|
||||||
|
|
||||||
|
- O root da biblioteca deve ser fornecido pelo host como `--games-root <dir>`.
|
||||||
|
- O scan inicial deve cobrir subdiretorios imediatos com `manifest.json`.
|
||||||
|
- O loader de diretorio existente deve continuar sendo a autoridade para validar
|
||||||
|
cartucho completo; a biblioteca pode ler metadados minimos para listagem.
|
||||||
|
- A Home pode mostrar uma lista feia/clicavel; polimento visual fica fora.
|
||||||
|
- O clique em um Game deve iniciar `LoadCartridge` pelo firmware/SystemOS, nao
|
||||||
|
por syscall guest.
|
||||||
|
- V1 nao precisa tratar retorno de um Game para Home, porque isso pertence a
|
||||||
|
`AGD-0041`.
|
||||||
|
- `.pmc`, outros tipos de app, catalogo remoto, marketplace, icones, busca e
|
||||||
|
metadados ricos ficam fora.
|
||||||
|
|
||||||
|
## Opcoes
|
||||||
|
|
||||||
|
### Opcao A - Biblioteca apenas para listagem
|
||||||
|
|
||||||
|
- **Abordagem:** adicionar `--games-root`, escanear manifestos e mostrar jogos
|
||||||
|
na Home, mas sem permitir launch ainda.
|
||||||
|
- **Pro:** baixa complexidade; prova descoberta/catalogo local.
|
||||||
|
- **Contra:** nao entrega o objetivo principal de carregar jogo pela Home.
|
||||||
|
- **Manutenibilidade:** boa, mas valor funcional limitado.
|
||||||
|
|
||||||
|
### Opcao B - Home lista biblioteca e carrega Game quando nao ha Game ativo
|
||||||
|
|
||||||
|
- **Abordagem:** adicionar `--games-root`, escanear cartuchos locais, renderizar
|
||||||
|
lista simples na Home e permitir clique para carregar um Game via
|
||||||
|
`LoadCartridge`. A primeira versao cobre Home inicial/sem Game residente.
|
||||||
|
- **Pro:** entrega o fluxo desejado sem abrir troca completa entre jogos.
|
||||||
|
- **Contra:** exige passar biblioteca/acao selecionada entre host, Hub,
|
||||||
|
firmware e loader.
|
||||||
|
- **Manutenibilidade:** boa se a fronteira ficar clara: biblioteca descobre,
|
||||||
|
Hub escolhe, firmware carrega.
|
||||||
|
|
||||||
|
### Opcao C - Biblioteca mais troca completa entre jogos
|
||||||
|
|
||||||
|
- **Abordagem:** Home lista biblioteca e tambem permite `Game A -> Home ->
|
||||||
|
Game B` ja no primeiro passo.
|
||||||
|
- **Pro:** cobre a experiencia final.
|
||||||
|
- **Contra:** mistura biblioteca com pausa, fechamento, cleanup e falha de load;
|
||||||
|
depende de `AGD-0041` e `AGD-0044`.
|
||||||
|
- **Manutenibilidade:** baixa para v1.
|
||||||
|
|
||||||
|
## Sugestao / Recomendacao
|
||||||
|
|
||||||
|
Recomendo a Opcao B.
|
||||||
|
|
||||||
|
O v1 deve permitir:
|
||||||
|
|
||||||
|
- host recebe `--games-root <dir>`;
|
||||||
|
- o SO/Hub recebe uma lista de cartuchos descobertos;
|
||||||
|
- a Home mostra uma lista simples/clicavel de jogos;
|
||||||
|
- clicar em um Game carrega o cartucho via firmware `LoadCartridge`;
|
||||||
|
- boot direto por `--run <cart>` continua separado e intacto;
|
||||||
|
- nenhum guest/app chama `run_cart`;
|
||||||
|
- o escopo v1 acontece na Home inicial; retorno de Game para Home fica para
|
||||||
|
`AGD-0041`, e troca `Game A -> Home -> Game B` fica para `AGD-0044`.
|
||||||
|
|
||||||
|
Detalhes aceitos para v1:
|
||||||
|
|
||||||
|
- scan apenas de subdiretorios imediatos;
|
||||||
|
- somente cartuchos `app_mode: Game`;
|
||||||
|
- `.pmc` fica para futuro;
|
||||||
|
- cartuchos invalidos sao logados e omitidos da lista;
|
||||||
|
- o modelo interno deve ser uma entry de biblioteca expansivel, nao apenas um
|
||||||
|
summary de UI. A entry pode manter o manifest carregado em memoria, path
|
||||||
|
interno necessario para launch e metadados operacionais como instante de
|
||||||
|
descoberta/instalacao. A UI v1 mostra apenas `title`, `app_id` e
|
||||||
|
`app_version`, mas futuros metadados ingeridos pela biblioteca poderao ser
|
||||||
|
usados pelo catalogo e pela propria UI;
|
||||||
|
- clique carrega imediatamente, sem confirmacao;
|
||||||
|
- falha de load volta para Home e registra log/erro.
|
||||||
|
|
||||||
|
Essa agenda deve produzir a alternativa correta ao `run_cart` userland: uma
|
||||||
|
acao de sistema, iniciada pela Home, que carrega cartucho escolhido pelo usuario
|
||||||
|
a partir de uma biblioteca local.
|
||||||
|
|
||||||
|
## Perguntas em Aberto
|
||||||
|
|
||||||
|
- [x] O argumento deve se chamar `--games-root`.
|
||||||
|
- [x] A biblioteca deve escanear apenas subdiretorios imediatos.
|
||||||
|
- [x] A listagem v1 deve incluir apenas `app_mode: Game`.
|
||||||
|
- [x] O scan deve logar cartuchos invalidos e omiti-los da lista.
|
||||||
|
- [x] O Hub/SystemOS deve receber uma entry de biblioteca expansivel, capaz de
|
||||||
|
manter o manifest carregado, path interno para launch e metadados
|
||||||
|
operacionais como timestamp de descoberta/instalacao. A UI v1 so precisa
|
||||||
|
mostrar `title`, `app_id` e `app_version`.
|
||||||
|
- [x] O clique deve carregar imediatamente, sem confirmacao.
|
||||||
|
- [x] Se um cartucho falha ao carregar, o sistema volta para Home e registra
|
||||||
|
erro/log.
|
||||||
|
- [x] Retorno de Game para Home nao pertence a esta agenda; fica para
|
||||||
|
`AGD-0041`.
|
||||||
|
- [x] O teste minimo deve provar o fluxo `games-root -> Home list -> click ->
|
||||||
|
LoadCartridge -> GameRunning`?
|
||||||
|
|
||||||
|
## Criterio para Encerrar
|
||||||
|
|
||||||
|
A agenda pode virar decisao quando estiver claro:
|
||||||
|
|
||||||
|
- nome e semantica do argumento de host para root da biblioteca;
|
||||||
|
- escopo do scan e metadados minimos;
|
||||||
|
- fronteira entre biblioteca, Hub, firmware e loader;
|
||||||
|
- politica v1 para cartuchos invalidos;
|
||||||
|
- regra v1 para launch quando nao ha Game ativo;
|
||||||
|
- fronteira explicita com `AGD-0041` e `AGD-0044`;
|
||||||
|
- testes minimos para discovery e launch pela Home.
|
||||||
|
|
||||||
|
## Discussao
|
||||||
|
|
||||||
|
- 2026-07-03: Agenda criada como continuacao da `DSC-0004` apos a `DEC-0035`.
|
||||||
|
A decisao anterior removeu `run_cart` userland; esta agenda define o caminho
|
||||||
|
correto para o SO/Home carregar jogos a partir de uma biblioteca local.
|
||||||
|
- 2026-07-03: Respostas aceitas: usar `--games-root`, escanear apenas
|
||||||
|
subdiretorios imediatos, listar somente Games, deixar `.pmc` e outros apps
|
||||||
|
para depois, logar/omitir invalidos, usar summary com `title`, `app_id` e
|
||||||
|
`app_version`, carregar imediatamente no clique, e deixar retorno para Home
|
||||||
|
sob responsabilidade da `AGD-0041`.
|
||||||
|
- 2026-07-03: Modelo de dados refinado: a biblioteca deve produzir entries
|
||||||
|
expansíveis, nao somente summaries de UI. Cada entry pode manter manifest em
|
||||||
|
memoria, path interno de launch e metadados operacionais como timestamp de
|
||||||
|
descoberta/instalacao; a UI v1 escolhe quais campos mostrar, mas o modelo
|
||||||
|
deve permitir que metadados futuros sejam ingeridos e usados pelo catalogo/UI.
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
id: DEC-0035
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Remove Userland system.run_cart and Preserve Direct Boot
|
||||||
|
status: accepted
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_agenda: AGD-0003
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted.
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
`system.run_cart` existe na ABI publica, mas nao possui comportamento real de
|
||||||
|
troca de cartucho. Isso cria uma falsa promessa: um guest pode declarar/chamar
|
||||||
|
uma operacao que aparenta iniciar outro cartucho, enquanto o runtime retorna
|
||||||
|
sucesso vazio ou nao executa uma transicao completa.
|
||||||
|
|
||||||
|
A discussao fechou tres separacoes:
|
||||||
|
|
||||||
|
- boot direto por host/CLI/debug e necessario e deve permanecer;
|
||||||
|
- apps/userland nao devem iniciar cartuchos diretamente;
|
||||||
|
- troca `Game A -> Home -> Game B` exige um orquestrador de SystemOS proprio,
|
||||||
|
separado em `DSC-0043 / AGD-0044`.
|
||||||
|
|
||||||
|
`AGD-0041` permanece responsavel por `Game -> Home -> mesmo Game`, foreground,
|
||||||
|
pausa/suspensao e retorno ao mesmo Game.
|
||||||
|
|
||||||
|
## Decisao
|
||||||
|
|
||||||
|
`system.run_cart` MUST be removed from the userland guest ABI.
|
||||||
|
|
||||||
|
No app, cartridge, Shell app, or other userland guest code MAY directly request
|
||||||
|
cartridge boot through `system.run_cart`.
|
||||||
|
|
||||||
|
The platform MUST NOT preserve compatibility for the current userland
|
||||||
|
`system.run_cart` surface. It MUST NOT remain as a callable stub, reserved
|
||||||
|
guest syscall that returns success, or capability-gated userland escape hatch.
|
||||||
|
|
||||||
|
Direct boot MUST remain supported through host/CLI/debug or equivalent
|
||||||
|
single-game launch paths. This direct boot path is a host/system entrypoint,
|
||||||
|
not a guest syscall.
|
||||||
|
|
||||||
|
Internal cartridge loading MAY continue to use the existing loader path,
|
||||||
|
including directory cartridges and test fixtures. `.pmc` support is not required
|
||||||
|
by this decision.
|
||||||
|
|
||||||
|
If apps ever need to request navigation in the future, that MUST be designed as
|
||||||
|
a separate SystemOS-mediated surface with different naming and semantics. It
|
||||||
|
MUST NOT reintroduce userland `run_cart` as direct cartridge boot.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
Direct cartridge boot is essential for development, debugging, automated tests,
|
||||||
|
and future distribution modes where the runtime launches as a single game. That
|
||||||
|
use case does not require a guest-visible syscall.
|
||||||
|
|
||||||
|
Userland cartridge boot would give app code authority over system navigation,
|
||||||
|
lifecycle, cleanup, target resolution, and failure policy. Those concerns belong
|
||||||
|
to SystemOS/firmware and later to the cartridge switch orchestrator, not to a VM
|
||||||
|
guest syscall.
|
||||||
|
|
||||||
|
Keeping a stub or compatibility surface would preserve the current ambiguity.
|
||||||
|
The public ABI should not advertise an operation that either does nothing or
|
||||||
|
looks like direct app-controlled navigation.
|
||||||
|
|
||||||
|
## Invariantes / Contrato
|
||||||
|
|
||||||
|
- Guest/userland MUST NOT expose or call `system.run_cart`.
|
||||||
|
- The public syscall registry, declarations, generated bindings, and docs MUST
|
||||||
|
stop presenting `system.run_cart` as an app-callable operation.
|
||||||
|
- Booting a cartridge directly MUST remain available from host/CLI/debug flows.
|
||||||
|
- Direct boot MUST enter the runtime with a selected cartridge target; it MUST
|
||||||
|
NOT depend on a guest syscall issued after VM startup.
|
||||||
|
- Loader code MAY remain reusable by host/firmware/SystemOS internals.
|
||||||
|
- Loader code MUST NOT become lifecycle orchestration authority.
|
||||||
|
- `Game -> Home -> same Game` remains owned by `AGD-0041`.
|
||||||
|
- `Game A -> Home -> Game B` remains owned by `DSC-0043 / AGD-0044`.
|
||||||
|
- `.pmc`, app catalog, launcher UX, marketplace/distribution, and full cartridge
|
||||||
|
replacement under Home are outside this decision.
|
||||||
|
|
||||||
|
## Impactos
|
||||||
|
|
||||||
|
- **Spec / ABI:** remove or retire `system.run_cart` from the userland syscall
|
||||||
|
surface. Any syscall table, PBS declarations, generated metadata, or docs
|
||||||
|
must stop advertising it to apps.
|
||||||
|
- **Runtime / VM:** guest dispatch must no longer treat `system.run_cart` as a
|
||||||
|
valid userland operation. If obsolete bytecode or declarations still reach the
|
||||||
|
runtime during migration, they must fail explicitly rather than report success.
|
||||||
|
- **Firmware / Host:** direct boot remains supported for command-line,
|
||||||
|
debugger, test harness, and future single-game launch flows.
|
||||||
|
- **SystemOS:** no new cart-switch orchestrator is required by this decision.
|
||||||
|
That work is deferred to `DSC-0043 / AGD-0044`.
|
||||||
|
- **Tooling:** tools must continue to support direct cartridge launch for
|
||||||
|
development and test workflows, without relying on a guest syscall.
|
||||||
|
- **Tests:** coverage must prove both sides: userland cannot declare/call
|
||||||
|
`system.run_cart`, and direct boot of a cartridge still works.
|
||||||
|
|
||||||
|
## Alternativas Descartadas
|
||||||
|
|
||||||
|
- **Implement `system.run_cart` as a normal syscall:** rejected because it gives
|
||||||
|
userland direct navigation authority and mixes guest execution with system
|
||||||
|
lifecycle.
|
||||||
|
- **Keep a compatibility stub:** rejected because no compatibility is required
|
||||||
|
and a stub preserves the false contract.
|
||||||
|
- **Make `system.run_cart` privileged but still guest-visible:** rejected for v1
|
||||||
|
because it keeps the ambiguous idea that some guest profile may directly boot
|
||||||
|
cartuchos.
|
||||||
|
- **Implement full `Home -> another Game` now:** rejected for this decision; it
|
||||||
|
requires SystemOS orchestration after the `AGD-0041` foreground contract.
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- Agenda: `AGD-0003`
|
||||||
|
- Foreground/pause scope: `AGD-0041`
|
||||||
|
- Future cartridge switch orchestrator: `DSC-0043 / AGD-0044`
|
||||||
|
- Related lessons: `LSN-0041` SystemOS lifecycle authority, `LSN-0042`
|
||||||
|
SystemOS service ownership boundary, `LSN-0043` SystemOS domain facades.
|
||||||
|
|
||||||
|
## Propagacao Necessaria
|
||||||
|
|
||||||
|
- Remove `SystemRunCart` from userland ABI declarations and generated syscall
|
||||||
|
metadata.
|
||||||
|
- Remove or rewrite runtime dispatch paths that return success for
|
||||||
|
`system.run_cart`.
|
||||||
|
- Preserve host/CLI/debug direct boot paths and add regression tests for them.
|
||||||
|
- Add negative tests for guest/userland declaration or call attempts.
|
||||||
|
- Update any documentation/spec text that describes `system.run_cart` as
|
||||||
|
app-callable.
|
||||||
|
|
||||||
|
## Revision Log
|
||||||
|
|
||||||
|
- 2026-07-03: Initial draft from `AGD-0003`.
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
id: DEC-0036
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: SystemOS Games Root and Home Game Launch
|
||||||
|
status: accepted
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_agenda: AGD-0045
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted.
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
`DEC-0035` removed `system.run_cart` from the userland guest ABI and preserved
|
||||||
|
direct host/CLI/debug cartridge boot. That decision intentionally did not
|
||||||
|
deliver the Home/SO product flow: pointing PROMETEU at a local games directory,
|
||||||
|
listing available games in Home, and launching a selected game by user action.
|
||||||
|
|
||||||
|
This decision defines the first system-owned replacement for userland
|
||||||
|
`run_cart`: a local games-root library discovered by the host/SystemOS and
|
||||||
|
exposed through a simple Home UI.
|
||||||
|
|
||||||
|
## Decisao
|
||||||
|
|
||||||
|
PROMETEU MUST support a host-provided games library root through
|
||||||
|
`--games-root <dir>`.
|
||||||
|
|
||||||
|
The games root is a directory containing game cartridge directories as immediate
|
||||||
|
children. V1 MUST scan only immediate child directories. Recursive discovery is
|
||||||
|
out of scope.
|
||||||
|
|
||||||
|
The v1 games library MUST include only cartridges whose manifest declares
|
||||||
|
`app_mode: Game`. Shell/System apps, `.pmc` packages, remote catalogs,
|
||||||
|
marketplace/distribution metadata, rich icons, search, and non-game apps are out
|
||||||
|
of scope.
|
||||||
|
|
||||||
|
The library MUST produce expandable internal entries, not UI-only summaries.
|
||||||
|
Each entry MUST retain enough internal data to launch the cartridge and to grow
|
||||||
|
as a catalog model. At minimum, each entry MUST include:
|
||||||
|
|
||||||
|
- loaded manifest data;
|
||||||
|
- `title`;
|
||||||
|
- `app_id`;
|
||||||
|
- `app_version`;
|
||||||
|
- internal cartridge path;
|
||||||
|
- operational discovery metadata, including a timestamp or equivalent discovery
|
||||||
|
marker.
|
||||||
|
|
||||||
|
The Home UI v1 SHOULD display only `title`, `app_id`, and `app_version`. It MAY
|
||||||
|
hide path and operational metadata.
|
||||||
|
|
||||||
|
Invalid cartridge candidates MUST be logged and omitted from the Home game list.
|
||||||
|
|
||||||
|
Selecting a listed Game in Home MUST immediately request a system-owned launch
|
||||||
|
through firmware/SystemOS, resulting in `LoadCartridge` for that cartridge. This
|
||||||
|
MUST NOT use a guest syscall and MUST NOT reintroduce userland `run_cart`.
|
||||||
|
|
||||||
|
If launch fails, the machine MUST return to or remain in Home and record an
|
||||||
|
error/log. A polished error UI is out of scope.
|
||||||
|
|
||||||
|
Booting directly with `--run <cart>` MUST remain separate and unchanged.
|
||||||
|
|
||||||
|
Returning from a running Game to Home is outside this decision and belongs to
|
||||||
|
`AGD-0041`. Switching from `Game A -> Home -> Game B` after a game is already
|
||||||
|
active belongs to `DSC-0043 / AGD-0044`.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
The previous `run_cart` surface was wrong because it made cartridge launch look
|
||||||
|
like an app/userland capability. The desired behavior is still valid, but the
|
||||||
|
authority must live in the system: Home presents available games and the
|
||||||
|
firmware/SystemOS loads the selected cartridge.
|
||||||
|
|
||||||
|
Using `--games-root` keeps the v1 product model honest: this is a local game
|
||||||
|
library, not a general app root. Other app types can be introduced later without
|
||||||
|
forcing the first launcher model to pretend it is a full marketplace.
|
||||||
|
|
||||||
|
Immediate child scanning keeps discovery deterministic and testable. `.pmc` and
|
||||||
|
recursive layouts can be added after the directory-cartridge path is proven.
|
||||||
|
|
||||||
|
An expandable entry model avoids prematurely freezing the UI shape as the data
|
||||||
|
model. The UI is intentionally simple, but the catalog model needs room for
|
||||||
|
future metadata such as package origin, installation/discovery timestamps, rich
|
||||||
|
presentation fields, and packaged cartridge details.
|
||||||
|
|
||||||
|
## Invariantes / Contrato
|
||||||
|
|
||||||
|
- `--games-root <dir>` is the v1 host argument for a local games library.
|
||||||
|
- The games root is Game-only in v1.
|
||||||
|
- Discovery scans immediate child directories only.
|
||||||
|
- Directory candidates without valid Game cartridge metadata are logged and
|
||||||
|
omitted.
|
||||||
|
- Library entries are internal catalog records, not direct UI DTOs.
|
||||||
|
- The UI v1 displays a simple clickable list and may show only `title`,
|
||||||
|
`app_id`, and `app_version`.
|
||||||
|
- The internal entry retains cartridge path and loaded manifest data for launch.
|
||||||
|
- Home selection is a system action that enters firmware/SystemOS loading. It is
|
||||||
|
not a guest syscall.
|
||||||
|
- `--run <cart>` direct boot remains independent from `--games-root`.
|
||||||
|
- Return-to-Home and game-to-game switching are not implemented by this
|
||||||
|
decision.
|
||||||
|
|
||||||
|
## Impactos
|
||||||
|
|
||||||
|
- **Host:** add a `--games-root <dir>` argument and pass the resolved root into
|
||||||
|
runtime/firmware/SystemOS initialization.
|
||||||
|
- **SystemOS / Hub:** add a local game library model and expose entries to Home.
|
||||||
|
Home must render a minimal clickable list and emit a system-owned launch
|
||||||
|
action for a selected entry.
|
||||||
|
- **Firmware:** consume the Home launch action and transition to
|
||||||
|
`LoadCartridge` with the selected cartridge path.
|
||||||
|
- **Loader:** continue to validate complete directory cartridges. Discovery may
|
||||||
|
read manifest metadata, but full launch still uses the existing loader.
|
||||||
|
- **Runtime / VM:** no userland ABI is added. No guest syscall participates in
|
||||||
|
launch.
|
||||||
|
- **Specs / Docs:** document `--games-root`, Game-only v1 discovery, and the
|
||||||
|
distinction between direct boot, games library launch, and future game
|
||||||
|
switching.
|
||||||
|
- **Tests:** cover discovery, invalid omission/logging, Home action emission,
|
||||||
|
and the end-to-end path from games root selection to `GameRunning`.
|
||||||
|
|
||||||
|
## Alternativas Descartadas
|
||||||
|
|
||||||
|
- **List only, no launch:** rejected because it does not satisfy the product
|
||||||
|
goal of starting games from Home.
|
||||||
|
- **General app root:** rejected for v1 because Shell/System apps need separate
|
||||||
|
lifecycle and UI contracts.
|
||||||
|
- **Recursive discovery:** rejected for v1 to keep semantics deterministic.
|
||||||
|
- **`.pmc` discovery:** rejected for v1 because packed cartridge loading is a
|
||||||
|
separate open agenda.
|
||||||
|
- **Game-to-game switch now:** rejected because it depends on foreground and
|
||||||
|
cartridge-switch orchestration work in `AGD-0041` and `AGD-0044`.
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- Source agenda: `AGD-0045`
|
||||||
|
- Userland run-cart removal: `DEC-0035`
|
||||||
|
- Foreground/Home return scope: `AGD-0041`
|
||||||
|
- Future cartridge switch orchestration: `DSC-0043 / AGD-0044`
|
||||||
|
|
||||||
|
## Propagacao Necessaria
|
||||||
|
|
||||||
|
- Add or update runtime specs for games-root discovery and Home launch.
|
||||||
|
- Add host CLI argument plumbing for `--games-root`.
|
||||||
|
- Add a catalog/library entry model.
|
||||||
|
- Add Hub/Home list rendering and click handling.
|
||||||
|
- Add firmware/SystemOS action routing to `LoadCartridge`.
|
||||||
|
- Add tests for discovery, invalid candidates, action emission, and
|
||||||
|
`games-root -> Home click -> GameRunning`.
|
||||||
|
|
||||||
|
## Revision Log
|
||||||
|
|
||||||
|
- 2026-07-03: Initial draft from `AGD-0045`.
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0129
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Remove Userland Run Cart ABI Surface
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0035]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
`DEC-0035` removes `system.run_cart` from the userland guest ABI. This plan
|
||||||
|
covers the public ABI, syscall registry/declaration surface, generated metadata,
|
||||||
|
and documentation/spec propagation. It does not change runtime dispatch behavior
|
||||||
|
or direct boot tests; those are covered by `PLN-0130` and `PLN-0131`.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Remove `system.run_cart` from every app-callable/userland ABI surface while
|
||||||
|
preserving non-guest direct boot entrypoints.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0035`.
|
||||||
|
- Must stay scoped away from `AGD-0041` and `AGD-0044`.
|
||||||
|
- Coordinate with `PLN-0130` so runtime dispatch does not retain a callable
|
||||||
|
success path after the declaration is removed.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Remove or retire `Syscall::SystemRunCart` from userland-visible syscall
|
||||||
|
declarations in `crates/console/prometeu-hal/src/syscalls.rs`.
|
||||||
|
- Remove the `system.run_cart` registry entry from
|
||||||
|
`crates/console/prometeu-hal/src/syscalls/domains/system.rs` and
|
||||||
|
`crates/console/prometeu-hal/src/syscalls/registry.rs`, unless implementation
|
||||||
|
discovers an internal-only registry distinct from guest ABI.
|
||||||
|
- Remove generated/static bytecode or VM-facing declarations that expose
|
||||||
|
`system.run_cart`, including known references in
|
||||||
|
`crates/console/prometeu-bytecode/src/model.rs` and
|
||||||
|
`crates/console/prometeu-vm/src/virtual_machine.rs` if they include it.
|
||||||
|
- Update docs/specs that mention `system.run_cart` as app-callable.
|
||||||
|
- Update `AGD-0004` references only if they become factually misleading after
|
||||||
|
removal; do not close or reinterpret that agenda.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Do not implement `Home -> another Game`.
|
||||||
|
- Do not add a replacement app navigation syscall.
|
||||||
|
- Do not implement `.pmc`.
|
||||||
|
- Do not change direct host/CLI/debug boot behavior.
|
||||||
|
- Do not add compatibility stubs for obsolete guest code.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Search for `SystemRunCart`, `run_cart`, and `"system", "run_cart"` across
|
||||||
|
`crates/`, `docs/`, and `discussion/`.
|
||||||
|
2. Remove `system.run_cart` from userland syscall domain declarations and
|
||||||
|
lookup/name registries.
|
||||||
|
3. Remove generated/static guest-facing metadata entries that advertise
|
||||||
|
`system.run_cart`.
|
||||||
|
4. Update docs/specs to state that cartridges are booted by host/system paths,
|
||||||
|
not by guest syscall.
|
||||||
|
5. Compile the workspace or targeted crates to find stale references.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- No userland ABI declaration advertises `system.run_cart`.
|
||||||
|
- No generated guest metadata exposes `run_cart` as callable.
|
||||||
|
- Documentation/spec text no longer describes app-callable cartridge boot.
|
||||||
|
- Any retained code path is clearly internal-only and is not reachable through
|
||||||
|
guest syscall lookup.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Add or update syscall registry tests to assert `system.run_cart` is absent
|
||||||
|
from the public/userland registry.
|
||||||
|
- Run targeted tests for `prometeu-hal`, `prometeu-bytecode`, and
|
||||||
|
`prometeu-vm` if available.
|
||||||
|
- Run repository validation commands used for syscall declaration consistency.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/console/prometeu-hal/src/syscalls.rs`
|
||||||
|
- `crates/console/prometeu-hal/src/syscalls/domains/system.rs`
|
||||||
|
- `crates/console/prometeu-hal/src/syscalls/registry.rs`
|
||||||
|
- `crates/console/prometeu-bytecode/src/model.rs`
|
||||||
|
- `crates/console/prometeu-vm/src/virtual_machine.rs`
|
||||||
|
- `docs/specs/runtime/`
|
||||||
|
- `discussion/workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md`
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0130
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Remove Runtime Run Cart Stub Dispatch
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0035]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
`DEC-0035` forbids a callable userland `system.run_cart` stub. This plan covers
|
||||||
|
runtime and VM dispatch cleanup so obsolete calls cannot silently succeed.
|
||||||
|
Public ABI removal is handled by `PLN-0129`; direct boot preservation is handled
|
||||||
|
by `PLN-0131`.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Remove the runtime no-op dispatch path for `SystemRunCart` and ensure stale or
|
||||||
|
obsolete userland attempts fail explicitly rather than returning success.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0035`.
|
||||||
|
- Depends on the ABI direction in `PLN-0129`, but can be implemented in the same
|
||||||
|
PR if the public enum/registry removal and dispatch cleanup move together.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Remove the `Syscall::SystemRunCart => return Ok(())` path from
|
||||||
|
`crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`.
|
||||||
|
- Update any match arms made unreachable or obsolete by removing
|
||||||
|
`SystemRunCart`.
|
||||||
|
- Ensure stale numeric syscall ids or stale bytecode declarations fail with the
|
||||||
|
repository's existing unknown/invalid syscall behavior.
|
||||||
|
- Add negative coverage for userland attempts to invoke the removed operation,
|
||||||
|
using the closest existing VM runtime syscall tests.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Do not implement a replacement navigation operation.
|
||||||
|
- Do not implement the `Home -> another Game` orchestrator.
|
||||||
|
- Do not alter direct host/CLI/debug cartridge boot paths.
|
||||||
|
- Do not change `system.has_cart` behavior unless required by compile fallout.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Inspect `dispatch.rs` for all `SystemRunCart` match arms.
|
||||||
|
2. Remove the no-op success path and any unreachable arms tied only to
|
||||||
|
`SystemRunCart`.
|
||||||
|
3. If the public enum variant is removed by `PLN-0129`, update exhaustiveness
|
||||||
|
and registry mapping accordingly.
|
||||||
|
4. Add or update VM runtime tests that demonstrate `system.run_cart` is not
|
||||||
|
callable from guest/userland.
|
||||||
|
5. Run targeted runtime tests and compile checks.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- There is no runtime path where a guest `system.run_cart` call returns `Ok(())`.
|
||||||
|
- Stale/obsolete attempts fail explicitly through existing invalid syscall or
|
||||||
|
declaration validation behavior.
|
||||||
|
- `system.has_cart` and unrelated system syscalls keep their current behavior.
|
||||||
|
- Runtime dispatch remains exhaustive and clear after removing `SystemRunCart`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Add a negative test in `crates/console/prometeu-system/src/services/vm_runtime`
|
||||||
|
for stale userland `run_cart` attempts, if the test harness can express it.
|
||||||
|
- Run `cargo test` for `prometeu-system` and any impacted syscall crates.
|
||||||
|
- Run a workspace compile/test command if targeted tests reveal cross-crate enum
|
||||||
|
fallout.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`
|
||||||
|
- `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`
|
||||||
|
- `crates/console/prometeu-hal/src/syscalls.rs`
|
||||||
|
- `crates/console/prometeu-hal/src/syscalls/registry.rs`
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0131
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Preserve Direct Cartridge Boot Coverage
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0035]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
`DEC-0035` removes userland `system.run_cart`, but direct cartridge boot remains
|
||||||
|
mandatory for development, debugging, automated tests, and future single-game
|
||||||
|
launch scenarios. This plan protects that path while the guest syscall is
|
||||||
|
removed.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Preserve and test host/CLI/debug direct cartridge boot without relying on a
|
||||||
|
guest syscall.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0035`.
|
||||||
|
- Can be implemented after or alongside `PLN-0129` and `PLN-0130`.
|
||||||
|
- Must not depend on `.pmc`, app catalog, launcher UX, `AGD-0041`, or
|
||||||
|
`AGD-0044`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Identify direct boot entrypoints in `crates/tools/prometeu-cli/src/main.rs`,
|
||||||
|
`crates/host/prometeu-host-desktop-winit/src/lib.rs`,
|
||||||
|
`crates/host/prometeu-host-desktop-winit/src/debugger.rs`, and firmware
|
||||||
|
cartridge loading code.
|
||||||
|
- Add or preserve tests showing a cartridge can be loaded directly into
|
||||||
|
firmware/runtime from host or CLI paths.
|
||||||
|
- Ensure these paths do not call or require `system.run_cart`.
|
||||||
|
- Document the distinction between direct boot and guest navigation where
|
||||||
|
runtime specs describe boot behavior.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Do not implement `Home -> another Game`.
|
||||||
|
- Do not add app-callable navigation.
|
||||||
|
- Do not require `.pmc`.
|
||||||
|
- Do not redesign cartridge loader internals beyond what is required to keep
|
||||||
|
direct boot working.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Trace the CLI/debug direct boot path from cartridge path argument through
|
||||||
|
loader, firmware `load_cartridge`, and runtime initialization.
|
||||||
|
2. Add regression tests at the lowest practical layer proving direct boot still
|
||||||
|
reaches `GameRunning` for a valid game cartridge.
|
||||||
|
3. Add a guard or assertion that direct boot does not depend on `system.run_cart`
|
||||||
|
metadata.
|
||||||
|
4. Update docs/specs to describe direct boot as host/system-controlled.
|
||||||
|
5. Run targeted CLI/firmware/host tests.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- A valid game cartridge can still boot directly from host/CLI/debug paths.
|
||||||
|
- Direct boot does not depend on `system.run_cart` being in the guest syscall
|
||||||
|
registry.
|
||||||
|
- Existing debugger/handshake metadata that reports cartridge info still works.
|
||||||
|
- The direct boot behavior is documented as host/system entry, not guest ABI.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Add or preserve firmware tests around `load_cartridge` and `GameRunning`.
|
||||||
|
- Add or preserve host/CLI tests for cartridge path boot when practical.
|
||||||
|
- Run `cargo test` for `prometeu-firmware`, `prometeu-host-desktop-winit`, and
|
||||||
|
`prometeu-cli` where available.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/tools/prometeu-cli/src/main.rs`
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/src/lib.rs`
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/src/debugger.rs`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/firmware.rs`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`
|
||||||
|
- `docs/specs/runtime/`
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0132
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Specify Games Root Home Launch Contract
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0036]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
`DEC-0036` establishes a new system-owned game library launch path:
|
||||||
|
the host receives `--games-root <dir>`, SystemOS/Home lists valid Game
|
||||||
|
directory cartridges from that root, and selecting one starts the cartridge
|
||||||
|
through firmware loading rather than a guest syscall.
|
||||||
|
|
||||||
|
This plan updates the normative specs before code is changed.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Document the v1 games-root and Home launch contract in the canonical runtime
|
||||||
|
specs so implementation plans cannot drift back into userland `run_cart` or
|
||||||
|
general app-library semantics.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0036`.
|
||||||
|
- Prior decision: `DEC-0035`, which removed userland `system.run_cart` and
|
||||||
|
preserved direct `--run` boot.
|
||||||
|
- This plan must be completed before `PLN-0133`, `PLN-0134`, and `PLN-0135`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Update the runtime specs to define `--games-root <dir>` as the v1 local games
|
||||||
|
library root.
|
||||||
|
- Specify that discovery scans only immediate child directories.
|
||||||
|
- Specify that v1 library discovery includes only valid directory cartridges
|
||||||
|
with `app_mode: Game`.
|
||||||
|
- Specify that `.pmc`, recursive discovery, Shell/System apps, marketplace
|
||||||
|
metadata, rich presentation, search, and non-game apps are out of scope.
|
||||||
|
- Specify the internal library entry contract: loaded manifest data, `title`,
|
||||||
|
`app_id`, `app_version`, internal cartridge path, and discovery metadata.
|
||||||
|
- Specify that Home v1 may display only `title`, `app_id`, and `app_version`.
|
||||||
|
- Specify that selecting a Home game emits a system-owned launch request that
|
||||||
|
transitions through firmware/SystemOS into `LoadCartridge`.
|
||||||
|
- Specify that invalid candidates are logged and omitted.
|
||||||
|
- Specify that launch failure remains or returns to Home and logs the failure.
|
||||||
|
- Reaffirm that direct `--run <cart>` boot remains independent and unchanged.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No Rust implementation.
|
||||||
|
- No Home UI changes.
|
||||||
|
- No `.pmc` discovery or packaged-cartridge spec changes.
|
||||||
|
- No recursive library layout.
|
||||||
|
- No return-to-Home behavior for a running Game.
|
||||||
|
- No Game A -> Home -> Game B orchestration.
|
||||||
|
- No guest syscall or userland ABI addition.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Update [docs/specs/runtime/14-boot-profiles.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/14-boot-profiles.md) with:
|
||||||
|
- direct `--run` boot remaining a single-cartridge boot profile;
|
||||||
|
- `--games-root <dir>` as a SystemOS/Home library profile;
|
||||||
|
- the distinction between direct boot, Home launch, and future game-switch
|
||||||
|
orchestration.
|
||||||
|
2. Update [docs/specs/runtime/12-firmware-pos-and-prometeuhub.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md) with:
|
||||||
|
- Home's responsibility to display the local games library;
|
||||||
|
- the system-owned launch action boundary;
|
||||||
|
- failure handling that leaves the machine in Home and records an error.
|
||||||
|
3. Update [docs/specs/runtime/13-cartridge.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/13-cartridge.md) with:
|
||||||
|
- directory-cartridge discovery requirements for library candidates;
|
||||||
|
- Game-only filtering by manifest `app_mode`;
|
||||||
|
- invalid candidate omission and diagnostics.
|
||||||
|
4. Update [docs/specs/runtime/16-host-abi-and-syscalls.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/16-host-abi-and-syscalls.md) only if needed to reinforce that no guest syscall owns cartridge launch.
|
||||||
|
5. Keep all spec text in English.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- The specs define `--games-root <dir>` and do not describe it as a generic app
|
||||||
|
root.
|
||||||
|
- The specs state that v1 scans immediate child directories only.
|
||||||
|
- The specs state that only valid Game directory cartridges enter the Home list.
|
||||||
|
- The specs state that Home launch is system-owned and enters `LoadCartridge`.
|
||||||
|
- The specs preserve `--run <cart>` as a separate direct boot path.
|
||||||
|
- The specs explicitly exclude `.pmc`, recursive discovery, return-to-Home, and
|
||||||
|
game-switch orchestration from this implementation slice.
|
||||||
|
- No spec reintroduces userland `run_cart`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Run `discussion validate`.
|
||||||
|
- Run targeted text checks for `--games-root`, `LoadCartridge`, and absence of
|
||||||
|
`system.run_cart` as an active public launch mechanism in edited specs.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md`
|
||||||
|
- `docs/specs/runtime/13-cartridge.md`
|
||||||
|
- `docs/specs/runtime/14-boot-profiles.md`
|
||||||
|
- `docs/specs/runtime/16-host-abi-and-syscalls.md`
|
||||||
|
- `discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md`
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0133
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Implement Games Root Library Discovery
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0036]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
`DEC-0036` requires a host-provided games root and an expandable internal
|
||||||
|
library model before Home can render or launch games. This plan implements the
|
||||||
|
library discovery and host plumbing only; UI launch wiring is handled by
|
||||||
|
`PLN-0134`.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Add a tested games-root discovery model that scans immediate child directory
|
||||||
|
cartridges, filters to valid Game manifests, records diagnostics for invalid
|
||||||
|
candidates, and carries enough metadata for Home rendering and later launch.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0036`.
|
||||||
|
- `PLN-0132` should be completed first so code follows the documented contract.
|
||||||
|
- Existing cartridge manifest and directory loader behavior in the console
|
||||||
|
crates.
|
||||||
|
- Existing host CLI and `HostRunner` boot setup.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add `--games-root <PATH>` to the desktop host CLI.
|
||||||
|
- Pass the optional games root into the host runner / firmware initialization
|
||||||
|
path without changing direct `--run` or `--debug` boot behavior.
|
||||||
|
- Add a SystemOS-owned games library model for directory cartridge discovery.
|
||||||
|
- Scan only immediate child directories of the root.
|
||||||
|
- Read and retain manifest data for each valid candidate.
|
||||||
|
- Include only candidates whose manifest declares `app_mode: Game`.
|
||||||
|
- Retain internal cartridge path and operational discovery metadata on each
|
||||||
|
entry.
|
||||||
|
- Emit diagnostics for invalid, unreadable, or non-Game candidates and omit
|
||||||
|
them from the returned entries.
|
||||||
|
- Keep discovery independent from full cartridge launch: discovery reads
|
||||||
|
metadata; launch still uses the existing loader in `PLN-0134`.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No Home list rendering.
|
||||||
|
- No click handling or `LoadCartridge` transition.
|
||||||
|
- No `.pmc` discovery.
|
||||||
|
- No recursive scan.
|
||||||
|
- No Shell/System app catalog.
|
||||||
|
- No marketplace metadata, icons, search, sorting UI, or polished errors.
|
||||||
|
- No new guest syscall.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Introduce a library/discovery module in the SystemOS side, preferably under
|
||||||
|
`crates/console/prometeu-system/src/services/` unless the existing module
|
||||||
|
layout has a more local owner.
|
||||||
|
2. Define an internal entry type with at least:
|
||||||
|
- loaded cartridge manifest data;
|
||||||
|
- `title`;
|
||||||
|
- `app_id`;
|
||||||
|
- `app_version`;
|
||||||
|
- internal cartridge path;
|
||||||
|
- discovery metadata, using `SystemTime` or another explicit discovery
|
||||||
|
marker.
|
||||||
|
3. Define discovery diagnostics that can be logged by the caller and asserted in
|
||||||
|
tests.
|
||||||
|
4. Implement a scanner that:
|
||||||
|
- accepts an optional root path;
|
||||||
|
- returns an empty library when no root is configured;
|
||||||
|
- iterates only immediate child directories;
|
||||||
|
- parses `manifest.json` using structured manifest types, not ad hoc string
|
||||||
|
matching;
|
||||||
|
- filters to `app_mode: Game`;
|
||||||
|
- sorts entries deterministically for stable Home presentation and tests.
|
||||||
|
5. Add `--games-root <PATH>` to
|
||||||
|
[crates/host/prometeu-host-desktop-winit/src/lib.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/lib.rs)
|
||||||
|
and pass the path into
|
||||||
|
[crates/host/prometeu-host-desktop-winit/src/runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs).
|
||||||
|
6. Store or expose the discovered library to firmware/SystemOS in a way that
|
||||||
|
`PLN-0134` can consume without re-scanning inside the Home click path.
|
||||||
|
7. Add focused unit tests using temporary cartridge roots with:
|
||||||
|
- one valid Game directory cartridge;
|
||||||
|
- one valid non-Game cartridge;
|
||||||
|
- one invalid directory;
|
||||||
|
- one nested directory that must not be discovered.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `--games-root <PATH>` is accepted by the desktop host CLI.
|
||||||
|
- Absence of `--games-root` leaves existing Hub boot behavior unchanged.
|
||||||
|
- Direct `--run <cart>` and `--debug <cart>` behavior remains unchanged.
|
||||||
|
- Discovery returns only immediate valid Game directory cartridges.
|
||||||
|
- Each library entry retains manifest data, path, title, app id, app version,
|
||||||
|
and discovery metadata.
|
||||||
|
- Invalid candidates are represented in diagnostics and excluded from entries.
|
||||||
|
- Discovery does not fully load or boot cartridge programs.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Add and run focused games-library discovery tests in the owning console crate.
|
||||||
|
- Run `cargo test -p prometeu-system` if the library lives in
|
||||||
|
`prometeu-system`.
|
||||||
|
- Run `cargo test -p prometeu-host-desktop-winit` for CLI/runner plumbing.
|
||||||
|
- Run `discussion validate`.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/services/`
|
||||||
|
- `crates/console/prometeu-system/src/lib.rs` or module exports as needed
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/src/lib.rs`
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/src/runner.rs`
|
||||||
|
- `discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md`
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0134
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Wire Home Game List Launch Flow
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0036]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
After `PLN-0133`, SystemOS can discover a local Game library but Home still
|
||||||
|
does not present or launch those entries. This plan wires the discovered
|
||||||
|
library into Prometeu Hub/Home and routes a selected game to firmware
|
||||||
|
`LoadCartridge`.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Make Home display a minimal clickable games list and launch the selected Game
|
||||||
|
through a system-owned firmware/SystemOS path, without reintroducing userland
|
||||||
|
`run_cart`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0036`.
|
||||||
|
- `PLN-0132` for documented contract.
|
||||||
|
- `PLN-0133` for `--games-root` plumbing and the internal library entry model.
|
||||||
|
- Existing Prometeu Hub Home profile and firmware state transitions.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Provide the discovered games library to Prometeu Hub/Home.
|
||||||
|
- Render a minimal Home list showing `title`, `app_id`, and `app_version`.
|
||||||
|
- Add Home hit-testing/click handling for game entries.
|
||||||
|
- Extend the system profile action model with a system-owned game launch action.
|
||||||
|
- Handle that action in firmware/SystemOS by loading the selected cartridge via
|
||||||
|
the existing cartridge loader and entering `LoadCartridge`.
|
||||||
|
- On load failure, log/record the error and keep or return the machine to Home.
|
||||||
|
- Preserve existing native shell launch behavior.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No polished launcher UI.
|
||||||
|
- No icons, screenshots, search, filtering, or marketplace metadata.
|
||||||
|
- No return-to-Home while a Game is running.
|
||||||
|
- No Game A -> Home -> Game B foreground orchestration.
|
||||||
|
- No `.pmc` launch support.
|
||||||
|
- No guest syscall.
|
||||||
|
- No direct-boot behavior changes.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Extend the SystemOS/Hub action type currently represented by
|
||||||
|
`SystemProfileAction::LaunchNativeShell` with a game-launch action that
|
||||||
|
carries a stable reference to the selected library entry or cartridge path.
|
||||||
|
2. Update the Prometeu Hub state in
|
||||||
|
[crates/console/prometeu-system/src/programs/prometeu_hub.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/programs/prometeu_hub.rs)
|
||||||
|
or its local modules to accept the discovered game entries.
|
||||||
|
3. Render the Home games list with stable dimensions and simple clickable rows.
|
||||||
|
Rows must display only `title`, `app_id`, and `app_version` for v1.
|
||||||
|
4. Add hit-testing so selecting a row emits the game-launch action immediately.
|
||||||
|
5. Update
|
||||||
|
[crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs)
|
||||||
|
to consume the new action.
|
||||||
|
6. In the firmware action handler:
|
||||||
|
- call the existing cartridge loader for the selected path;
|
||||||
|
- on success, return `FirmwareState::LoadCartridge(...)`;
|
||||||
|
- on failure, log/record the error and remain in the Home path.
|
||||||
|
7. Ensure shell launch still works exactly as before.
|
||||||
|
8. Add unit or integration tests for:
|
||||||
|
- Home list presentation data;
|
||||||
|
- click-to-action emission;
|
||||||
|
- firmware action handling success;
|
||||||
|
- firmware action handling failure.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Starting the host with `--games-root <dir>` exposes valid Game entries in
|
||||||
|
Home.
|
||||||
|
- The Home list displays `title`, `app_id`, and `app_version`.
|
||||||
|
- Clicking a listed Game routes through a system action and enters
|
||||||
|
`LoadCartridge`.
|
||||||
|
- The launch path does not use a guest syscall and does not mention
|
||||||
|
`system.run_cart`.
|
||||||
|
- Invalid or failing launch paths remain in Home and record an error.
|
||||||
|
- Existing native shell Home actions continue to work.
|
||||||
|
- Direct `--run` remains separate and unchanged.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Run `cargo test -p prometeu-system` for Hub action and presentation behavior.
|
||||||
|
- Run `cargo test -p prometeu-firmware` for firmware transition behavior.
|
||||||
|
- Run `cargo test -p prometeu-host-desktop-winit` if runner integration changes.
|
||||||
|
- Run `discussion validate`.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/programs/prometeu_hub.rs`
|
||||||
|
- `crates/console/prometeu-system/src/services/`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/firmware.rs`
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/src/runner.rs`
|
||||||
|
- `discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md`
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0135
|
||||||
|
ticket: system-run-cart
|
||||||
|
title: Validate Games Root Launch End To End
|
||||||
|
status: done
|
||||||
|
created: 2026-07-03
|
||||||
|
ref_decisions: [DEC-0036]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
`PLN-0132` through `PLN-0134` specify, discover, display, and launch games
|
||||||
|
from a local games root. This plan closes the slice by adding end-to-end
|
||||||
|
validation evidence and manual smoke instructions for the product behavior the
|
||||||
|
user wants: point the SO at a directory, click a listed game, and boot it.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Prove the complete `--games-root -> Home list -> click -> LoadCartridge ->
|
||||||
|
GameRunning` path while preserving direct `--run` boot and the absence of
|
||||||
|
userland `run_cart`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Source decision: `DEC-0036`.
|
||||||
|
- `PLN-0132`, `PLN-0133`, and `PLN-0134` must be complete.
|
||||||
|
- Existing test cartridge fixtures, especially `test-cartridges/stress-console`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add automated integration coverage for the full games-root Home launch path
|
||||||
|
where the existing host/firmware test seams allow it.
|
||||||
|
- Add or document a minimal local games-root fixture using existing directory
|
||||||
|
cartridges.
|
||||||
|
- Validate invalid-candidate omission at the end-to-end boundary.
|
||||||
|
- Validate direct `--run` still works after games-root wiring.
|
||||||
|
- Capture manual smoke-test commands for release-mode functional verification.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No new product UI polish.
|
||||||
|
- No screenshot automation unless the existing test harness already supports it
|
||||||
|
cleanly.
|
||||||
|
- No return-to-Home after a running Game.
|
||||||
|
- No Game A -> Home -> Game B orchestration.
|
||||||
|
- No `.pmc` validation.
|
||||||
|
- No broad performance tuning beyond confirming release-mode smoke behavior.
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Add an integration-style firmware or host test that constructs a games root
|
||||||
|
with a valid Game directory cartridge and boots to Hub/Home with that root.
|
||||||
|
2. Drive the Home selection path through the same action path used by user
|
||||||
|
input, not by calling the cartridge loader directly.
|
||||||
|
3. Assert that the machine reaches `LoadCartridge` or `GameRunning`, depending
|
||||||
|
on the most stable observable state available in the test harness.
|
||||||
|
4. Add a negative case with an invalid candidate and assert it is omitted or
|
||||||
|
reported as a diagnostic without appearing in Home.
|
||||||
|
5. Add a regression assertion that no public guest syscall named
|
||||||
|
`system.run_cart` is required by this path.
|
||||||
|
6. Re-run direct boot tests or add a small regression test proving
|
||||||
|
`--run test-cartridges/stress-console` remains independent from
|
||||||
|
`--games-root`.
|
||||||
|
7. Record manual smoke commands in the relevant spec or developer-facing doc:
|
||||||
|
- `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges`
|
||||||
|
- `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console`
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Automated tests cover a valid games-root Home launch path.
|
||||||
|
- Automated tests cover invalid candidate omission or diagnostics at the
|
||||||
|
end-to-end boundary.
|
||||||
|
- Automated or documented validation proves direct `--run` still works.
|
||||||
|
- Manual smoke instructions use release mode for realistic interactive speed.
|
||||||
|
- Validation evidence shows no guest syscall participates in launch.
|
||||||
|
- `discussion validate` passes.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Run `cargo test -p prometeu-system`.
|
||||||
|
- Run `cargo test -p prometeu-firmware`.
|
||||||
|
- Run `cargo test -p prometeu-host-desktop-winit`.
|
||||||
|
- Run `cargo test -p prometeu-hal syscalls` to keep the removed syscall surface
|
||||||
|
guarded.
|
||||||
|
- Manually smoke test:
|
||||||
|
`cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges`.
|
||||||
|
- Manually smoke test:
|
||||||
|
`cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console`.
|
||||||
|
- Run `discussion validate`.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/`
|
||||||
|
- `crates/console/prometeu-firmware/`
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/`
|
||||||
|
- `crates/console/prometeu-hal/src/syscalls/tests.rs`
|
||||||
|
- Runtime specs or developer docs containing smoke commands
|
||||||
|
- `discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md`
|
||||||
@ -239,10 +239,8 @@ The same physical surface may be reused by multiple visual owners, but logical
|
|||||||
ownership MUST remain explicit.
|
ownership MUST remain explicit.
|
||||||
|
|
||||||
Game pause/resume, foreground stack behavior, and coexistence of a paused Game
|
Game pause/resume, foreground stack behavior, and coexistence of a paused Game
|
||||||
with VM-backed Shell apps are defined by the foreground/lifecycle contract. The
|
with VM-backed Shell apps are outside this contract and are tracked by the
|
||||||
render boundary's responsibility is narrower: foreground owner changes MUST
|
foreground/lifecycle discussion.
|
||||||
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
|
### 4.7 Render telemetry
|
||||||
|
|
||||||
|
|||||||
@ -107,21 +107,3 @@ 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.
|
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.
|
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.
|
|
||||||
|
|||||||
@ -100,70 +100,7 @@ Important properties:
|
|||||||
- no execution occurs outside the frame loop;
|
- no execution occurs outside the frame loop;
|
||||||
- frame structure remains observable for host tooling and host-owned certification.
|
- frame structure remains observable for host tooling and host-owned certification.
|
||||||
|
|
||||||
## 7 Foreground Lifecycle Events
|
## 7 Render Worker Concurrency
|
||||||
|
|
||||||
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
|
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
|
guest callbacks. It is an implementation-side consumer of closed render
|
||||||
@ -190,7 +127,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
|
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.
|
typed failure if the worker cannot join within the configured timeout.
|
||||||
|
|
||||||
## 10 Async Asset and IO Work Lane
|
## 8 Async Asset and IO Work Lane
|
||||||
|
|
||||||
The asset/IO async work lane is not a machine-visible event source and does not
|
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
|
introduce guest callbacks. It is an implementation-side lane for asset
|
||||||
@ -214,7 +151,7 @@ does not publish resident graphics/audio/scene state directly.
|
|||||||
FS and game persistence services may consume this lane for IO-style work, but
|
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.
|
public FS API shape is defined by the FS/app-home contract, not by this chapter.
|
||||||
|
|
||||||
## 11 Determinism and Best Practices
|
## 9 Determinism and Best Practices
|
||||||
|
|
||||||
PROMETEU encourages:
|
PROMETEU encourages:
|
||||||
|
|
||||||
@ -229,7 +166,7 @@ PROMETEU discourages:
|
|||||||
- hidden timing channels;
|
- hidden timing channels;
|
||||||
- ambiguous out-of-band execution.
|
- ambiguous out-of-band execution.
|
||||||
|
|
||||||
## 12 Relationship to Other Specs
|
## 10 Relationship to Other Specs
|
||||||
|
|
||||||
- [`09a-coroutines-and-cooperative-scheduling.md`](09a-coroutines-and-cooperative-scheduling.md) defines coroutine lifecycle and scheduling behavior.
|
- [`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.
|
- [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces.
|
||||||
|
|||||||
@ -43,23 +43,13 @@ The VM does not own the machine lifecycle. Firmware does.
|
|||||||
Game directory cartridges.
|
Game directory cartridges.
|
||||||
- **Library entry**: an internal catalog record retained by Hub/SystemOS for a
|
- **Library entry**: an internal catalog record retained by Hub/SystemOS for a
|
||||||
discovered cartridge.
|
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
|
## 3 POS Responsibilities
|
||||||
|
|
||||||
POS is responsible for:
|
POS is responsible for:
|
||||||
|
|
||||||
- deterministic reset into known machine state;
|
- deterministic reset into known machine state;
|
||||||
- VM session creation, initialization, activation, and teardown for cartridge
|
- VM initialization and teardown for cartridge execution;
|
||||||
execution;
|
|
||||||
- input latching and logical-frame budgeting;
|
- input latching and logical-frame budgeting;
|
||||||
- peripheral reset/orchestration;
|
- peripheral reset/orchestration;
|
||||||
- fault capture and crash-flow transition;
|
- fault capture and crash-flow transition;
|
||||||
@ -71,10 +61,6 @@ At the VM boundary, POS must preserve:
|
|||||||
- `FRAME_SYNC` as the canonical frame boundary;
|
- `FRAME_SYNC` as the canonical frame boundary;
|
||||||
- deterministic transition from host tick to VM slice execution.
|
- 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
|
## 4 PrometeuHub Responsibilities
|
||||||
|
|
||||||
PrometeuHub is responsible for:
|
PrometeuHub is responsible for:
|
||||||
@ -168,74 +154,19 @@ Shell task loses its eligible focused window
|
|||||||
-> FirmwareState::HubHome
|
-> FirmwareState::HubHome
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7 Foreground Stack and Game Pause
|
## 7 Cartridge Load Flow
|
||||||
|
|
||||||
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:
|
Current high-level flow:
|
||||||
|
|
||||||
1. POS receives a cartridge to load;
|
1. POS receives a cartridge to load;
|
||||||
2. asset manager is initialized from the cartridge asset table, preload list, and packed asset bytes;
|
2. asset manager is initialized from the cartridge asset table, preload list, and packed asset bytes;
|
||||||
3. POS creates or selects the VM session for the cartridge;
|
3. POS initializes the VM/runtime for the cartridge;
|
||||||
4. POS initializes the session-owned VM/runtime for the cartridge;
|
4. launch behavior branches by `AppMode`:
|
||||||
5. launch behavior branches by `AppMode`:
|
|
||||||
- `Game` -> transition to the game-running firmware state;
|
- `Game` -> transition to the game-running firmware state;
|
||||||
- `System` -> create/focus a Hub window and return to the Hub state.
|
- `System` -> create/focus a Hub window and return to the Hub state.
|
||||||
|
|
||||||
If VM initialization fails, firmware transitions to the crash path.
|
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`.
|
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
|
When Home launches a discovered Game entry, the request is a system-owned action
|
||||||
@ -248,7 +179,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
|
machine in the Hub/Home flow and record the failure through diagnostics or
|
||||||
logging. A polished Home error surface is not required by v1.
|
logging. A polished Home error surface is not required by v1.
|
||||||
|
|
||||||
## 9 Firmware States
|
## 8 Firmware States
|
||||||
|
|
||||||
The current firmware state model includes:
|
The current firmware state model includes:
|
||||||
|
|
||||||
@ -258,12 +189,11 @@ The current firmware state model includes:
|
|||||||
- `HubHome`
|
- `HubHome`
|
||||||
- `LoadCartridge`
|
- `LoadCartridge`
|
||||||
- `GameRunning`
|
- `GameRunning`
|
||||||
- `ShellRunning`
|
|
||||||
- `AppCrashes`
|
- `AppCrashes`
|
||||||
|
|
||||||
These states express machine orchestration above the VM. They are not guest-visible bytecode states.
|
These states express machine orchestration above the VM. They are not guest-visible bytecode states.
|
||||||
|
|
||||||
## 10 Execution Contract
|
## 9 Execution Contract
|
||||||
|
|
||||||
For game-mode execution, firmware/runtime coordination preserves:
|
For game-mode execution, firmware/runtime coordination preserves:
|
||||||
|
|
||||||
@ -274,7 +204,7 @@ For game-mode execution, firmware/runtime coordination preserves:
|
|||||||
|
|
||||||
This keeps the machine model deterministic and observable.
|
This keeps the machine model deterministic and observable.
|
||||||
|
|
||||||
## 11 Crash Handling
|
## 10 Crash Handling
|
||||||
|
|
||||||
Firmware owns terminal fault presentation.
|
Firmware owns terminal fault presentation.
|
||||||
|
|
||||||
@ -286,32 +216,7 @@ When a terminal app fault occurs:
|
|||||||
|
|
||||||
Crash handling is outside the guest VM execution model.
|
Crash handling is outside the guest VM execution model.
|
||||||
|
|
||||||
## 12 Desktop Smoke Validation
|
## 11 Relationship to Other Specs
|
||||||
|
|
||||||
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.
|
- [`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.
|
- [`09-events-and-concurrency.md`](09-events-and-concurrency.md) defines the frame-boundary model used by firmware.
|
||||||
|
|||||||
@ -50,8 +50,6 @@ For a cartridge boot target:
|
|||||||
- `Game` cartridges transition into the game-running pipeline;
|
- `Game` cartridges transition into the game-running pipeline;
|
||||||
- `System` cartridges transition into a Runtime/Hub pipeline dedicated to
|
- `System` cartridges transition into a Runtime/Hub pipeline dedicated to
|
||||||
system UI and app hosting.
|
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.
|
This preserves the distinction between machine firmware state and app execution mode.
|
||||||
|
|
||||||
@ -101,14 +99,6 @@ 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
|
profile. That orchestration belongs to the foreground/home and cartridge switch
|
||||||
contracts.
|
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
|
## 5 Firmware State Relationship
|
||||||
|
|
||||||
Boot target selection feeds into firmware states such as:
|
Boot target selection feeds into firmware states such as:
|
||||||
|
|||||||
@ -187,11 +187,6 @@ 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
|
The `mem` module is a game profile surface. It is not the public storage model
|
||||||
for `System` profile apps.
|
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:
|
Canonical operations in v1 are:
|
||||||
|
|
||||||
- `mem.slot_count() -> (status, count)`
|
- `mem.slot_count() -> (status, count)`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user