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
7 changed files with 76 additions and 32 deletions
Showing only changes of commit 3aec829c70 - Show all commits

View File

@ -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]

View File

@ -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,

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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]

View File

@ -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"}]}

View File

@ -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]