From 3aec829c70ce3a52dcb2c0c1036f1945396cbdda Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 09:41:01 +0100 Subject: [PATCH] implements PLN-0165 --- .../src/firmware/firmware.rs | 43 +++++++++++++++-- crates/console/prometeu-system/src/lib.rs | 5 +- .../prometeu-system/src/programs/mod.rs | 5 +- .../src/programs/prometeu_hub/mod.rs | 5 +- .../src/programs/prometeu_hub/prometeu_hub.rs | 46 +++++++++---------- discussion/index.ndjson | 2 +- ...-0165-validate-hub-kill-flow-end-to-end.md | 2 +- 7 files changed, 76 insertions(+), 32 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 3f56f8c1..37ddb1fa 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -266,8 +266,8 @@ mod tests { use prometeu_system::task::{TaskId, TaskState}; use prometeu_system::windows::WindowOwner; use prometeu_system::{ - CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState, - discover_games_root, + CrashReport, ForegroundOwner, GameLifecycleEventKind, HUB_SUSPENDED_GAME_KILL_CONTROL, + ResidentGameState, discover_games_root, }; use std::fs; use std::path::PathBuf; @@ -479,6 +479,12 @@ mod tests { windows.set_focus(id); } + fn front_buffer_pixel(platform: &TestPlatform, x: i32, y: i32) -> u32 { + const FRAMEBUFFER_WIDTH: usize = 480; + let index = x as usize + y as usize * FRAMEBUFFER_WIDTH; + platform.local_hardware().gfx.front_buffer()[index] + } + #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); @@ -1123,12 +1129,26 @@ mod tests { assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task)); - let kill_click = - InputSignals { f_signal: true, x_pos: 300, y_pos: 240, ..Default::default() }; + let kill_sample_x = HUB_SUSPENDED_GAME_KILL_CONTROL.x + 2; + let kill_sample_y = HUB_SUSPENDED_GAME_KILL_CONTROL.y + 2; + assert_eq!( + front_buffer_pixel(&platform, kill_sample_x, kill_sample_y), + Color::rgb(164, 32, 40).raw(), + "Hub should render the suspended-game kill control before click" + ); + + let kill_click = InputSignals { + f_signal: true, + x_pos: HUB_SUSPENDED_GAME_KILL_CONTROL.x, + y_pos: HUB_SUSPENDED_GAME_KILL_CONTROL.y, + ..Default::default() + }; firmware.tick(&kill_click, &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(), None); + assert_eq!(firmware.os.lifecycle().resident_game(), None); assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(stress_task), @@ -1139,6 +1159,12 @@ mod tests { None ); assert!(!firmware.game_lifecycle_audio_paused()); + assert!(!firmware.resume_resident_game_from_home()); + + let ownership = firmware.os.vm().active_render_ownership(); + assert_eq!(ownership.app_mode, AppMode::Shell); + assert_eq!(ownership.app_id, 0); + assert_eq!(platform.local_hardware().frame_composer.active_scene_id(), None); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, None @@ -1147,6 +1173,8 @@ mod tests { platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, None ); + assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none()); + assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none()); assert!( firmware.os.recent_logs(32).iter().any(|event| { event.level == LogLevel::Info @@ -1155,6 +1183,13 @@ mod tests { }), "Hub kill control should use the canonical manual kill log" ); + + firmware.tick(&InputSignals::default(), &mut platform); + assert_eq!( + front_buffer_pixel(&platform, kill_sample_x, kill_sample_y), + Color::rgb(12, 14, 22).raw(), + "Hub should stop rendering the kill control after the game is gone" + ); } #[test] diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 4ef78157..f06c5ada 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -8,7 +8,10 @@ pub use os::{ DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError, SystemOS, }; -pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use programs::{ + HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction, + SystemProfileUpdate, +}; pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink}; pub use services::async_work::{ AsyncWorkActiveJob, AsyncWorkCancelToken, AsyncWorkJobContext, AsyncWorkJobId, diff --git a/crates/console/prometeu-system/src/programs/mod.rs b/crates/console/prometeu-system/src/programs/mod.rs index 916c4fb1..fcb844e1 100644 --- a/crates/console/prometeu-system/src/programs/mod.rs +++ b/crates/console/prometeu-system/src/programs/mod.rs @@ -1,3 +1,6 @@ mod prometeu_hub; -pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use prometeu_hub::{ + HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction, + SystemProfileUpdate, +}; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs index 21d08e2d..fc06b2b8 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs @@ -1,4 +1,7 @@ #[allow(clippy::module_inception)] mod prometeu_hub; -pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use prometeu_hub::{ + HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction, + SystemProfileUpdate, +}; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 10ed3418..f9eeee34 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -20,7 +20,7 @@ const GAME_ROW_W: i32 = 336; const GAME_ROW_H: i32 = 22; const GAME_ROW_GAP: i32 = 6; const MAX_HOME_GAME_ROWS: usize = 3; -const SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 }; +pub const HUB_SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 }; #[cfg(test)] const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 }; @@ -255,7 +255,7 @@ fn action_for_click( return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell); } - if suspended_game.is_some() && rect_contains(SUSPENDED_GAME_KILL_CONTROL, x, y) { + if suspended_game.is_some() && rect_contains(HUB_SUSPENDED_GAME_KILL_CONTROL, x, y) { return Some(SystemProfileAction::KillResidentGame); } @@ -357,7 +357,7 @@ fn render_home_packet( } if let Some(suspended_game) = suspended_game { - let hovered = rect_contains(SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y); + let hovered = rect_contains(HUB_SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y); draw_suspended_game_kill_control(&mut commands, suspended_game, hovered); } @@ -470,13 +470,13 @@ fn draw_suspended_game_kill_control( ) { let fill = if hovered { COLOR_KILL_ACTIVE } else { COLOR_KILL }; let border = if hovered { COLOR_HILITE } else { COLOR_BORDER }; - fill_rect(commands, SUSPENDED_GAME_KILL_CONTROL, fill); - draw_rect(commands, SUSPENDED_GAME_KILL_CONTROL, border); + fill_rect(commands, HUB_SUSPENDED_GAME_KILL_CONTROL, fill); + draw_rect(commands, HUB_SUSPENDED_GAME_KILL_CONTROL, border); draw_buffer_text( commands, - SUSPENDED_GAME_KILL_CONTROL.x + 8, - SUSPENDED_GAME_KILL_CONTROL.y + 8, - &fit_text_to_control(&suspended_game.title, SUSPENDED_GAME_KILL_CONTROL.w - 16), + HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8, + HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8, + &fit_text_to_control(&suspended_game.title, HUB_SUSPENDED_GAME_KILL_CONTROL.w - 16), COLOR_TEXT, ); } @@ -695,8 +695,8 @@ mod tests { fn home_render_shows_suspended_game_kill_control_when_game_is_resident() { let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() }; let packet = render_home_packet( - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, &[], Some(&suspended_game), ); @@ -705,15 +705,15 @@ mod tests { matches!( command, GfxUiCommand::FillRect { rect, color } - if *rect == SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE + if *rect == HUB_SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE ) })); assert!(packet.commands.iter().any(|command| { matches!( command, GfxUiCommand::DrawText { x, y, text, .. } - if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8 - && *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 + if *x == HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8 + && *y == HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8 && text == "Stress Console" ) })); @@ -722,8 +722,8 @@ mod tests { #[test] fn home_render_omits_suspended_game_kill_control_without_resident_game() { let packet = render_home_packet( - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, &[], None, ); @@ -732,7 +732,7 @@ mod tests { matches!( command, GfxUiCommand::FillRect { rect, color } - if *rect == SUSPENDED_GAME_KILL_CONTROL + if *rect == HUB_SUSPENDED_GAME_KILL_CONTROL && (*color == COLOR_KILL || *color == COLOR_KILL_ACTIVE) ) })); @@ -745,8 +745,8 @@ mod tests { assert_eq!( action_for_click( false, - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, true, &[], Some(&suspended_game), @@ -756,8 +756,8 @@ mod tests { assert_eq!( action_for_click( false, - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, true, &[], None, @@ -776,8 +776,8 @@ mod tests { .iter() .find_map(|command| match command { GfxUiCommand::DrawText { x, y, text, .. } - if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8 - && *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 => + if *x == HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8 + && *y == HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8 => { Some(text) } @@ -786,7 +786,7 @@ mod tests { .expect("kill control text should render"); assert!(label.ends_with("..")); - assert!(label.chars().count() <= ((SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize); + assert!(label.chars().count() <= ((HUB_SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize); } #[test] diff --git a/discussion/index.ndjson b/discussion/index.ndjson index bd44cb2b..d1e7e8fd 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"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":"done","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"}]} diff --git a/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md b/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md index 8eba055b..1286688b 100644 --- a/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md +++ b/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md @@ -2,7 +2,7 @@ id: PLN-0165 ticket: hub-suspended-game-kill-affordance title: Validate Hub Kill Flow End To End -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0040] tags: [hub, lifecycle, game, ui]