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