implements PLN-0164
This commit is contained in:
parent
62573e9574
commit
fa0a768b84
@ -250,6 +250,7 @@ impl Firmware {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::firmware::firmware_state::HubHomeStep;
|
||||
use crate::firmware::resident_game_termination::{self, ResidentGameTerminationOutcome};
|
||||
use prometeu_bytecode::assembler::assemble;
|
||||
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
||||
use prometeu_drivers::{GlyphBankPoolAccess, SceneBankPoolAccess, TestPlatform};
|
||||
@ -258,6 +259,7 @@ mod tests {
|
||||
use prometeu_hal::asset::{BankType, SlotRef};
|
||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::primitives::Rect;
|
||||
use prometeu_hal::syscalls::{Syscall, caps};
|
||||
use prometeu_system::process::ProcessState;
|
||||
@ -988,6 +990,115 @@ mod tests {
|
||||
assert_eq!(stress_ownership.app_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_game_termination_noops_when_no_game_is_resident() {
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
let ownership_before = firmware.os.vm().active_render_ownership();
|
||||
|
||||
let outcome = resident_game_termination::terminate_resident_game(
|
||||
&mut firmware.os,
|
||||
&mut platform,
|
||||
true,
|
||||
)
|
||||
.expect("no resident game termination should not fail");
|
||||
|
||||
assert_eq!(outcome, ResidentGameTerminationOutcome::NoResidentGame);
|
||||
assert_eq!(firmware.os.vm().active_render_ownership(), ownership_before);
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
|
||||
assert!(
|
||||
!firmware
|
||||
.os
|
||||
.recent_logs(16)
|
||||
.iter()
|
||||
.any(|event| event.msg.contains("Resident game killed from Hub"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_game_termination_closes_session_and_cleans_runtime_state() {
|
||||
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);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task));
|
||||
assert!(firmware.game_lifecycle_audio_paused());
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
Some(100)
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
Some(1)
|
||||
);
|
||||
assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_some());
|
||||
assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_some());
|
||||
|
||||
let outcome = resident_game_termination::terminate_resident_game(
|
||||
&mut firmware.os,
|
||||
&mut platform,
|
||||
true,
|
||||
)
|
||||
.expect("resident game termination should succeed");
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
ResidentGameTerminationOutcome::Terminated {
|
||||
task_id: stress_task,
|
||||
title: "Stress Console".to_string()
|
||||
}
|
||||
);
|
||||
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());
|
||||
|
||||
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
|
||||
);
|
||||
assert_eq!(
|
||||
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
|
||||
&& event.source == LogSource::Hub
|
||||
&& event.msg == "Resident game killed from Hub: Stress Console"
|
||||
}),
|
||||
"manual Hub kill should emit an INFO log"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_game_switch_returns_home_without_restoring_previous_game() {
|
||||
let root = TestGameRoot::new();
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::firmware::firmware_state::{
|
||||
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
|
||||
};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::firmware::resident_game_termination;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
@ -69,7 +70,11 @@ impl HubHomeStep {
|
||||
return Some(FirmwareState::ResumeResidentGame);
|
||||
}
|
||||
|
||||
if let Err(error) = ctx.os.lifecycle().terminate_resident_game() {
|
||||
if let Err(error) = resident_game_termination::terminate_resident_game(
|
||||
ctx.os,
|
||||
ctx.platform,
|
||||
false,
|
||||
) {
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Hub,
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::firmware::firmware_state::{
|
||||
AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep, ShellRunningStep,
|
||||
};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::firmware::resident_game_termination;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_hal::color::Color;
|
||||
@ -82,7 +83,11 @@ impl LoadCartridgeStep {
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_partial_target {
|
||||
let _ = ctx.os.lifecycle().terminate_resident_game();
|
||||
let _ = resident_game_termination::terminate_resident_game(
|
||||
ctx.os,
|
||||
ctx.platform,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ pub(crate) mod firmware_step_reset;
|
||||
pub(crate) mod firmware_step_shell_running;
|
||||
pub(crate) mod firmware_step_splash_screen;
|
||||
mod prometeu_context;
|
||||
pub(crate) mod resident_game_termination;
|
||||
|
||||
pub use boot_target::BootTarget;
|
||||
pub use firmware::Firmware;
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
use prometeu_hal::RuntimePlatform;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::log::LogSource;
|
||||
use prometeu_system::task::TaskId;
|
||||
use prometeu_system::{LifecycleError, SystemOS};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ResidentGameTerminationOutcome {
|
||||
NoResidentGame,
|
||||
Terminated { task_id: TaskId, title: String },
|
||||
}
|
||||
|
||||
pub(crate) fn terminate_resident_game(
|
||||
os: &mut SystemOS,
|
||||
platform: &mut dyn RuntimePlatform,
|
||||
log_manual_hub_kill: bool,
|
||||
) -> Result<ResidentGameTerminationOutcome, LifecycleError> {
|
||||
let Some(task_id) = os.lifecycle().resident_game_task() else {
|
||||
return Ok(ResidentGameTerminationOutcome::NoResidentGame);
|
||||
};
|
||||
|
||||
let title = os
|
||||
.sessions()
|
||||
.vm_session_for_task(task_id)
|
||||
.map(|session| session.title.clone())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
|
||||
let terminated = os.lifecycle().terminate_resident_game()?;
|
||||
let Some(task_id) = terminated else {
|
||||
return Ok(ResidentGameTerminationOutcome::NoResidentGame);
|
||||
};
|
||||
|
||||
os.vm().transition_render_owner(AppMode::Shell, 0);
|
||||
platform.game2d_frame_composer().unbind_scene();
|
||||
platform.assets_mut().shutdown();
|
||||
|
||||
if log_manual_hub_kill {
|
||||
os.info(LogSource::Hub, format!("Resident game killed from Hub: {title}"));
|
||||
}
|
||||
|
||||
Ok(ResidentGameTerminationOutcome::Terminated { task_id, title })
|
||||
}
|
||||
@ -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":"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":"open","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":"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-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-0164
|
||||
ticket: hub-suspended-game-kill-affordance
|
||||
title: Expose Resident Game Termination Contract
|
||||
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