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
4 changed files with 114 additions and 4 deletions
Showing only changes of commit 8d719843c7 - Show all commits

View File

@ -358,6 +358,10 @@ mod tests {
.serialize()
}
fn repository_test_cartridges_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join("test-cartridges")
}
fn invalid_game_cartridge() -> Cartridge {
Cartridge {
app_id: 7,
@ -819,6 +823,100 @@ mod tests {
}
}
#[test]
fn repository_stress_to_dummy_boy_switch_is_end_to_end() {
let library = discover_games_root(repository_test_cartridges_root());
assert!(
library
.entries()
.iter()
.any(|entry| entry.app_id == 1 && entry.title == "Stress Console")
);
assert!(
library.entries().iter().any(|entry| entry.app_id == 2 && entry.title == "Dummy Boy")
);
let (mut firmware, mut platform) = hub_home_firmware();
firmware.hub.set_game_library(library);
firmware.tick(&InputSignals::default(), &mut platform);
let stress_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
firmware.tick(&stress_click, &mut platform);
firmware.tick(&InputSignals::default(), &mut platform);
let stress_task = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected Stress GameRunning state, got {:?}", other),
};
assert_eq!(firmware.os.vm().current_app_id(), 1);
assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console");
firmware.request_home_from_host();
firmware
.os
.lifecycle()
.advance_game_pause_budget(stress_task)
.expect("pause budget should force Stress to suspend");
firmware.os.vm().transition_render_owner(AppMode::Shell, 0);
firmware.change_state(
FirmwareState::HubHome(HubHomeStep),
&InputSignals::default(),
&mut platform,
);
let hub_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(hub_ownership.app_mode, AppMode::Shell);
assert_eq!(hub_ownership.app_id, 0);
firmware.tick(&InputSignals::default(), &mut platform);
let dummy_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
firmware.tick(&dummy_click, &mut platform);
assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(stress_task),
Ok(ProcessState::Stopped)
);
assert_eq!(
firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id),
None
);
let dummy_task = firmware
.os
.lifecycle()
.resident_game_task()
.expect("Dummy Boy should be resident after switch load");
assert_ne!(dummy_task, stress_task);
assert_eq!(
firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| {
(session.app_id, session.title.clone(), session.app_version.clone())
}),
Some((2, "Dummy Boy".to_string(), "0.1.0".to_string()))
);
firmware.tick(&InputSignals::default(), &mut platform);
match &firmware.state {
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, dummy_task),
other => panic!("expected Dummy Boy GameRunning state, got {:?}", other),
}
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(dummy_task));
assert_eq!(firmware.os.vm().current_app_id(), 2);
assert_eq!(firmware.os.vm().current_cartridge_title(), "Dummy Boy");
firmware.tick(&InputSignals::default(), &mut platform);
let dummy_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(dummy_ownership.app_mode, AppMode::Game);
assert_eq!(dummy_ownership.app_id, 2);
let front = platform.local_hardware().gfx.front_buffer();
let center = 240 + 135 * 480;
assert_eq!(front[center], Color::GREEN.raw());
}
#[test]
fn failed_game_switch_returns_home_without_restoring_previous_game() {
let root = TestGameRoot::new();
@ -1068,6 +1166,8 @@ mod tests {
};
let tick_index_before_home = firmware.os.vm().tick_index();
let render_ownership_before_home = firmware.os.vm().active_render_ownership();
assert_eq!(render_ownership_before_home.app_mode, AppMode::Game);
assert_eq!(render_ownership_before_home.app_id, 9);
firmware.request_home_from_host();
@ -1099,7 +1199,6 @@ mod tests {
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),
@ -1128,6 +1227,8 @@ mod tests {
firmware.request_home_from_host();
firmware.tick(&signals, &mut platform);
let hub_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(hub_ownership.app_mode, AppMode::Shell);
assert_eq!(hub_ownership.app_id, 0);
assert!(firmware.resume_resident_game_from_home());
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
@ -1140,7 +1241,6 @@ mod tests {
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 };

View File

@ -250,6 +250,12 @@ impl<'a> VmFacade<'a> {
}
pub fn active_render_ownership(&self) -> RenderOwnership {
if let Some(task_id) = self.foreground_session_task_id()
&& let Some(session) = self.os.vm_sessions.for_task(task_id)
{
return session.runtime.render_manager.active_ownership();
}
self.os.vm_runtime.render_manager.active_ownership()
}
@ -317,4 +323,8 @@ impl<'a> VmFacade<'a> {
.foreground_task()
.or_else(|| self.os.foreground_stack.resident_game_task())
}
fn foreground_session_task_id(&self) -> Option<TaskId> {
self.os.task_manager.foreground_task()
}
}

View File

@ -1,5 +1,5 @@
{"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":"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":"done","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":"done","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":"done","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-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"}]}

View File

@ -2,7 +2,7 @@
id: PLN-0161
ticket: system-os-cartridge-switch-orchestrator
title: Validate Stress To Dummy Boy Game Switch End To End
status: open
status: done
created: 2026-07-05
ref_decisions: [DEC-0039]
tags: [runtime, os, lifecycle, game, cartridge, architecture]