implements PLN-0163

This commit is contained in:
bQUARKz 2026-07-05 09:36:59 +01:00
parent fa0a768b84
commit 73c8246c8d
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 293 additions and 13 deletions

View File

@ -1099,6 +1099,64 @@ mod tests {
); );
} }
#[test]
fn hub_kill_control_click_terminates_resident_game_and_stays_home() {
let library = discover_games_root(repository_test_cartridges_root());
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),
};
firmware.request_home_from_host();
firmware.tick(&InputSignals::default(), &mut platform);
firmware.tick(&InputSignals::default(), &mut platform);
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() };
firmware.tick(&kill_click, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
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
);
assert!(!firmware.game_lifecycle_audio_paused());
assert_eq!(
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
None
);
assert_eq!(
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
None
);
assert!(
firmware.os.recent_logs(32).iter().any(|event| {
event.level == LogLevel::Info
&& event.source == LogSource::Hub
&& event.msg == "Resident game killed from Hub: Stress Console"
}),
"Hub kill control should use the canonical manual kill log"
);
}
#[test] #[test]
fn failed_game_switch_returns_home_without_restoring_previous_game() { fn failed_game_switch_returns_home_without_restoring_previous_game() {
let root = TestGameRoot::new(); let root = TestGameRoot::new();

View File

@ -106,6 +106,18 @@ impl HubHomeStep {
} }
} }
} }
Some(SystemProfileAction::KillResidentGame) => {
if let Err(error) =
resident_game_termination::terminate_resident_game(ctx.os, ctx.platform, true)
{
ctx.os.log(
LogLevel::Error,
LogSource::Hub,
0,
format!("Failed to kill resident game from Hub: {error:?}"),
);
}
}
Some(SystemProfileAction::CloseShell) | None => {} Some(SystemProfileAction::CloseShell) | None => {}
} }

View File

@ -1,5 +1,6 @@
use crate::task::TaskId; use crate::task::TaskId;
use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS}; use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect; use prometeu_hal::primitives::Rect;
use prometeu_hal::{ use prometeu_hal::{
@ -19,6 +20,7 @@ const GAME_ROW_W: i32 = 336;
const GAME_ROW_H: i32 = 22; const GAME_ROW_H: i32 = 22;
const GAME_ROW_GAP: i32 = 6; const GAME_ROW_GAP: i32 = 6;
const MAX_HOME_GAME_ROWS: usize = 3; const MAX_HOME_GAME_ROWS: usize = 3;
const SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 };
#[cfg(test)] #[cfg(test)]
const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 }; const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 };
@ -31,6 +33,8 @@ const COLOR_TEXT: Color = Color::rgb(230, 236, 224);
const COLOR_MUTED: Color = Color::rgb(91, 112, 128); const COLOR_MUTED: Color = Color::rgb(91, 112, 128);
const COLOR_BUTTON: Color = Color::rgb(45, 59, 82); const COLOR_BUTTON: Color = Color::rgb(45, 59, 82);
const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122); const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122);
const COLOR_KILL: Color = Color::rgb(164, 32, 40);
const COLOR_KILL_ACTIVE: Color = Color::rgb(206, 46, 54);
/// PrometeuHub: Launcher and system UI environment. /// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub { pub struct PrometeuHub {
@ -83,6 +87,7 @@ impl NativeShellApp {
pub enum SystemProfileAction { pub enum SystemProfileAction {
LaunchNativeShell(NativeShellApp), LaunchNativeShell(NativeShellApp),
LaunchGame { target: GameSwitchTarget }, LaunchGame { target: GameSwitchTarget },
KillResidentGame,
CloseShell, CloseShell,
} }
@ -131,12 +136,14 @@ impl PrometeuHub {
} }
let input = platform.input(); let input = platform.input();
let suspended_game = if in_shell { None } else { resident_game_indicator(os) };
if let Some(action) = action_for_click( if let Some(action) = action_for_click(
in_shell, in_shell,
input.touch().x(), input.touch().x(),
input.touch().y(), input.touch().y(),
input.touch().f().pressed, input.touch().f().pressed,
self.game_library.entries(), self.game_library.entries(),
suspended_game.as_ref(),
) { ) {
return Some(action); return Some(action);
} }
@ -149,7 +156,13 @@ impl PrometeuHub {
let pointer_y = platform.input().touch().y(); let pointer_y = platform.input().touch().y();
let packet = if os.windows().window_count() == 0 { let packet = if os.windows().window_count() == 0 {
render_home_packet(pointer_x, pointer_y, self.game_library.entries()) let suspended_game = resident_game_indicator(os);
render_home_packet(
pointer_x,
pointer_y,
self.game_library.entries(),
suspended_game.as_ref(),
)
} else { } else {
let mut commands = Vec::new(); let mut commands = Vec::new();
for window in os.windows().windows() { for window in os.windows().windows() {
@ -232,6 +245,7 @@ fn action_for_click(
y: i32, y: i32,
pressed: bool, pressed: bool,
game_entries: &[crate::GameLibraryEntry], game_entries: &[crate::GameLibraryEntry],
suspended_game: Option<&SuspendedGameIndicator>,
) -> Option<SystemProfileAction> { ) -> Option<SystemProfileAction> {
if !pressed { if !pressed {
return None; return None;
@ -241,6 +255,10 @@ fn action_for_click(
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell); return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
} }
if suspended_game.is_some() && rect_contains(SUSPENDED_GAME_KILL_CONTROL, x, y) {
return Some(SystemProfileAction::KillResidentGame);
}
if let Some(entry) = game_entries if let Some(entry) = game_entries
.iter() .iter()
.take(MAX_HOME_GAME_ROWS) .take(MAX_HOME_GAME_ROWS)
@ -257,6 +275,23 @@ fn action_for_click(
.map(|button| SystemProfileAction::LaunchNativeShell(button.app)) .map(|button| SystemProfileAction::LaunchNativeShell(button.app))
} }
#[derive(Debug, Clone, PartialEq, Eq)]
struct SuspendedGameIndicator {
title: String,
}
fn resident_game_indicator(os: &mut SystemOS) -> Option<SuspendedGameIndicator> {
let task_id = os.lifecycle().resident_game_task()?;
let sessions = os.sessions();
let session = sessions.vm_session_for_task(task_id)?;
if session.app_mode != AppMode::Game {
return None;
}
Some(SuspendedGameIndicator { title: session.title.clone() })
}
fn game_row_rect(index: usize) -> Rect { fn game_row_rect(index: usize) -> Rect {
Rect { Rect {
x: GAME_ROW_X, x: GAME_ROW_X,
@ -281,6 +316,7 @@ fn render_home_packet(
pointer_x: i32, pointer_x: i32,
pointer_y: i32, pointer_y: i32,
game_entries: &[crate::GameLibraryEntry], game_entries: &[crate::GameLibraryEntry],
suspended_game: Option<&SuspendedGameIndicator>,
) -> ShellUiFramePacket { ) -> ShellUiFramePacket {
let mut commands = Vec::new(); let mut commands = Vec::new();
commands.push(GfxUiCommand::Clear { color: COLOR_BG }); commands.push(GfxUiCommand::Clear { color: COLOR_BG });
@ -320,6 +356,11 @@ fn render_home_packet(
draw_button(&mut commands, button.rect, button.label, hovered); draw_button(&mut commands, button.rect, button.label, hovered);
} }
if let Some(suspended_game) = suspended_game {
let hovered = rect_contains(SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y);
draw_suspended_game_kill_control(&mut commands, suspended_game, hovered);
}
ShellUiFramePacket::new(commands) ShellUiFramePacket::new(commands)
} }
@ -422,6 +463,39 @@ fn draw_game_row(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hove
draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT); draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT);
} }
fn draw_suspended_game_kill_control(
commands: &mut Vec<GfxUiCommand>,
suspended_game: &SuspendedGameIndicator,
hovered: bool,
) {
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);
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),
COLOR_TEXT,
);
}
fn fit_text_to_control(text: &str, max_width: i32) -> String {
let max_chars = (max_width / 8).max(0) as usize;
if text.chars().count() <= max_chars {
return text.to_string();
}
if max_chars <= 2 {
return text.chars().take(max_chars).collect();
}
let mut fitted: String = text.chars().take(max_chars - 2).collect();
fitted.push_str("..");
fitted
}
fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) { fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color }); commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color });
} }
@ -462,7 +536,14 @@ mod tests {
#[test] #[test]
fn shell_a_button_click_emits_launch_action() { fn shell_a_button_click_emits_launch_action() {
assert_eq!( assert_eq!(
action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true, &[]), action_for_click(
false,
HOME_BUTTONS[0].rect.x,
HOME_BUTTONS[0].rect.y,
true,
&[],
None
),
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA)) Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
); );
} }
@ -475,7 +556,8 @@ mod tests {
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1, SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
SHELL_B_BUTTON.y, SHELL_B_BUTTON.y,
true, true,
&[] &[],
None
), ),
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB)) Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
); );
@ -489,7 +571,8 @@ mod tests {
SHELL_A_BUTTON.x + SHELL_A_BUTTON.w, SHELL_A_BUTTON.x + SHELL_A_BUTTON.w,
SHELL_A_BUTTON.y, SHELL_A_BUTTON.y,
true, true,
&[] &[],
None
), ),
None None
); );
@ -499,7 +582,8 @@ mod tests {
SHELL_A_BUTTON.x, SHELL_A_BUTTON.x,
SHELL_A_BUTTON.y + SHELL_A_BUTTON.h, SHELL_A_BUTTON.y + SHELL_A_BUTTON.h,
true, true,
&[] &[],
None
), ),
None None
); );
@ -507,7 +591,10 @@ mod tests {
#[test] #[test]
fn held_pointer_does_not_emit_click_action() { fn held_pointer_does_not_emit_click_action() {
assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[]), None); assert_eq!(
action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[], None),
None
);
} }
#[test] #[test]
@ -535,8 +622,14 @@ mod tests {
discovered_at: std::time::SystemTime::UNIX_EPOCH, discovered_at: std::time::SystemTime::UNIX_EPOCH,
}; };
let action = let action = action_for_click(
action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)); false,
GAME_ROW_X,
GAME_ROW_Y,
true,
std::slice::from_ref(&entry),
None,
);
assert_eq!( assert_eq!(
action, action,
@ -554,10 +647,10 @@ mod tests {
#[test] #[test]
fn close_button_click_emits_close_only_in_shell() { fn close_button_click_emits_close_only_in_shell() {
assert_eq!( assert_eq!(
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None),
Some(SystemProfileAction::CloseShell) Some(SystemProfileAction::CloseShell)
); );
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), None); assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None), None);
} }
#[test] #[test]
@ -571,7 +664,7 @@ mod tests {
#[test] #[test]
fn home_render_produces_shell_ui_packet_commands() { fn home_render_produces_shell_ui_packet_commands() {
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[]); let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[], None);
assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. }))); assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. })));
assert!(packet.commands.iter().any(|command| { assert!(packet.commands.iter().any(|command| {
@ -579,6 +672,123 @@ mod tests {
})); }));
} }
#[test]
fn resident_game_indicator_uses_only_resident_game_sessions() {
let mut os = SystemOS::new(None);
assert_eq!(resident_game_indicator(&mut os), None);
let shell = os.sessions().create_vm_shell_task(7, "VM Shell");
assert_eq!(resident_game_indicator(&mut os), None);
os.lifecycle().suspend_task(shell).expect("shell suspend should succeed");
assert_eq!(resident_game_indicator(&mut os), None);
let game = os.sessions().create_vm_game_task(42, "Stress Console");
os.lifecycle().suspend_task(game).expect("game suspend should succeed");
assert_eq!(
resident_game_indicator(&mut os),
Some(SuspendedGameIndicator { title: "Stress Console".to_string() })
);
}
#[test]
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,
&[],
Some(&suspended_game),
);
assert!(packet.commands.iter().any(|command| {
matches!(
command,
GfxUiCommand::FillRect { rect, color }
if *rect == 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
&& text == "Stress Console"
)
}));
}
#[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,
&[],
None,
);
assert!(!packet.commands.iter().any(|command| {
matches!(
command,
GfxUiCommand::FillRect { rect, color }
if *rect == SUSPENDED_GAME_KILL_CONTROL
&& (*color == COLOR_KILL || *color == COLOR_KILL_ACTIVE)
)
}));
}
#[test]
fn suspended_game_kill_control_click_emits_kill_action() {
let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() };
assert_eq!(
action_for_click(
false,
SUSPENDED_GAME_KILL_CONTROL.x,
SUSPENDED_GAME_KILL_CONTROL.y,
true,
&[],
Some(&suspended_game),
),
Some(SystemProfileAction::KillResidentGame)
);
assert_eq!(
action_for_click(
false,
SUSPENDED_GAME_KILL_CONTROL.x,
SUSPENDED_GAME_KILL_CONTROL.y,
true,
&[],
None,
),
None
);
}
#[test]
fn suspended_game_kill_control_truncates_long_titles() {
let long_title =
SuspendedGameIndicator { title: "Extremely Long Resident Game Title".to_string() };
let packet = render_home_packet(0, 0, &[], Some(&long_title));
let label = packet
.commands
.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 =>
{
Some(text)
}
_ => None,
})
.expect("kill control text should render");
assert!(label.ends_with(".."));
assert!(label.chars().count() <= ((SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize);
}
#[test] #[test]
fn shell_close_button_sits_inside_shell_frame() { fn shell_close_button_sits_inside_shell_frame() {
assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y)); assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y));

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":53,"CLSN":1}} {"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":"open","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":"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-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-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-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-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-0163 id: PLN-0163
ticket: hub-suspended-game-kill-affordance ticket: hub-suspended-game-kill-affordance
title: Render Hub Suspended Game Kill Control title: Render Hub Suspended Game Kill Control
status: open status: done
created: 2026-07-05 created: 2026-07-05
ref_decisions: [DEC-0040] ref_decisions: [DEC-0040]
tags: [hub, lifecycle, game, ui] tags: [hub, lifecycle, game, ui]