dev/system-os-cartridge-switch-orchestrator #36
@ -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();
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"}]}
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user