dev/system-os-cartridge-switch-orchestrator #36

Merged
bquarkz merged 13 commits from dev/system-os-cartridge-switch-orchestrator into master 2026-07-05 17:47:27 +00:00
5 changed files with 133 additions and 9 deletions
Showing only changes of commit 3df8227ebf - Show all commits

View File

@ -299,6 +299,18 @@ mod tests {
title: &str, title: &str,
app_version: &str, app_version: &str,
with_program: bool, with_program: bool,
) -> PathBuf {
let program = with_program.then(halting_program);
self.add_game_with_program(name, app_id, title, app_version, program)
}
fn add_game_with_program(
&self,
name: &str,
app_id: u32,
title: &str,
app_version: &str,
program: Option<Vec<u8>>,
) -> PathBuf { ) -> PathBuf {
let cart = self.path.join(name); let cart = self.path.join(name);
fs::create_dir_all(&cart).expect("test cartridge should be created"); fs::create_dir_all(&cart).expect("test cartridge should be created");
@ -314,8 +326,8 @@ mod tests {
); );
fs::write(cart.join("manifest.json"), manifest) fs::write(cart.join("manifest.json"), manifest)
.expect("test manifest should be written"); .expect("test manifest should be written");
if with_program { if let Some(program) = program {
fs::write(cart.join("program.pbx"), halting_program()) fs::write(cart.join("program.pbx"), program)
.expect("test program should be written"); .expect("test program should be written");
} }
cart cart
@ -807,6 +819,73 @@ mod tests {
} }
} }
#[test]
fn failed_game_switch_returns_home_without_restoring_previous_game() {
let root = TestGameRoot::new();
root.add_game("stress", true);
root.add_game_with_program("zzz-broken", 43, "Zzz Broken", "1.0.0", Some(vec![0, 0, 0, 0]));
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 stress_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
firmware.tick(&stress_click, &mut platform);
firmware.tick(&InputSignals::default(), &mut platform);
let first_game_task = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected first GameRunning state, got {:?}", other),
};
firmware.request_home_from_host();
firmware.tick(&InputSignals::default(), &mut platform);
firmware.tick(&InputSignals::default(), &mut platform);
let broken_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
firmware.tick(&broken_click, &mut platform);
assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed));
assert_eq!(
firmware
.os
.sessions()
.vm_session_for_task(first_game_task)
.map(|session| session.task_id),
None
);
let partial_task = firmware
.os
.lifecycle()
.resident_game_task()
.expect("failed target should be resident until recovery tick");
assert_ne!(partial_task, first_game_task);
firmware.tick(&InputSignals::default(), &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed));
assert_eq!(firmware.os.lifecycle().task_state(partial_task), Some(TaskState::Closed));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(partial_task),
Ok(ProcessState::Stopped)
);
assert_eq!(
firmware.os.sessions().vm_session_for_task(partial_task).map(|session| session.task_id),
None
);
assert!(
firmware
.os
.recent_logs(16)
.iter()
.any(|event| event.msg.contains("Failed to switch to game Zzz Broken app_id 43"))
);
}
#[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();

View File

@ -83,9 +83,9 @@ impl HubHomeStep {
} }
} }
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new( return Some(FirmwareState::LoadCartridge(
cartridge, LoadCartridgeStep::new_game_switch(cartridge),
))); ));
} }
Err(error) => { Err(error) => {
ctx.os.log( ctx.os.log(

View File

@ -1,5 +1,5 @@
use crate::firmware::firmware_state::{ use crate::firmware::firmware_state::{
AppCrashesStep, FirmwareState, GameRunningStep, ShellRunningStep, AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep, ShellRunningStep,
}; };
use crate::firmware::prometeu_context::PrometeuContext; use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::app_mode::AppMode; use prometeu_hal::app_mode::AppMode;
@ -16,11 +16,28 @@ pub struct LoadCartridgeStep {
init_error: Option<CrashReport>, init_error: Option<CrashReport>,
loaded_task_id: Option<prometeu_system::task::TaskId>, loaded_task_id: Option<prometeu_system::task::TaskId>,
loaded_app_mode: Option<AppMode>, loaded_app_mode: Option<AppMode>,
failure_recovery: LoadCartridgeFailureRecovery,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LoadCartridgeFailureRecovery {
AppCrash,
ReturnToHub,
} }
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,
loaded_task_id: None,
loaded_app_mode: None,
failure_recovery: LoadCartridgeFailureRecovery::AppCrash,
}
}
pub fn new_game_switch(cartridge: Cartridge) -> Self {
Self { failure_recovery: LoadCartridgeFailureRecovery::ReturnToHub, ..Self::new(cartridge) }
} }
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
@ -51,6 +68,34 @@ impl LoadCartridgeStep {
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
if let Some(report) = self.init_error.take() { if let Some(report) = self.init_error.take() {
if self.failure_recovery == LoadCartridgeFailureRecovery::ReturnToHub {
if self.cartridge.app_mode == AppMode::Game
&& let Some(task_id) = ctx.os.lifecycle().resident_game_task()
{
let is_partial_target = ctx
.os
.sessions()
.vm_session_for_task(task_id)
.map(|session| session.app_id == self.cartridge.app_id)
.unwrap_or(false);
if is_partial_target {
let _ = ctx.os.lifecycle().terminate_resident_game();
}
}
ctx.os.log(
LogLevel::Error,
LogSource::Hub,
0,
format!(
"Failed to switch to game {} app_id {}; returning to Home: {:?}",
self.cartridge.title, self.cartridge.app_id, report
),
);
return Some(FirmwareState::HubHome(HubHomeStep));
}
return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
} }

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}}
{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} {"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-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":"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"}]}

View File

@ -2,7 +2,7 @@
id: PLN-0159 id: PLN-0159
ticket: system-os-cartridge-switch-orchestrator ticket: system-os-cartridge-switch-orchestrator
title: Recover Failed Game Switches Back To Home title: Recover Failed Game Switches Back To Home
status: open status: done
created: 2026-07-05 created: 2026-07-05
ref_decisions: [DEC-0039] ref_decisions: [DEC-0039]
tags: [runtime, os, lifecycle, game, cartridge, architecture] tags: [runtime, os, lifecycle, game, cartridge, architecture]