From 8d719843c77de4aaf215609acd9add588f5e26fa Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:24:45 +0100 Subject: [PATCH] implements PLN-0161 --- .../src/firmware/firmware.rs | 104 +++++++++++++++++- .../prometeu-system/src/os/facades/vm.rs | 10 ++ discussion/index.ndjson | 2 +- ...ess-to-dummy-boy-game-switch-end-to-end.md | 2 +- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index bfeaac3e..7d9fccdc 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -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 }; diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 367ac0f5..a76b167e 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -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 { + self.os.task_manager.foreground_task() + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 200fd52b..77a7b6c3 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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"}]} diff --git a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md index 6889679e..110b9565 100644 --- a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md +++ b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md @@ -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]