Hub Suspended Game Kill Affordance

This commit is contained in:
bQUARKz 2026-07-05 09:25:01 +01:00
parent c814e057dc
commit 62573e9574
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
12 changed files with 714 additions and 27 deletions

View File

@ -1858,6 +1858,9 @@ impl AssetManager {
self.glyph_slot_index.clear(); self.glyph_slot_index.clear();
self.sound_slots.write().unwrap().fill(None); self.sound_slots.write().unwrap().fill(None);
self.scene_slots.write().unwrap().fill(None); self.scene_slots.write().unwrap().fill(None);
self.gfx_installer.clear_glyph_banks();
self.sound_installer.clear_sound_banks();
self.scene_installer.clear_scene_banks();
} }
pub fn glyph_asset_slot_index(&self) -> GlyphAssetSlotIndex { pub fn glyph_asset_slot_index(&self) -> GlyphAssetSlotIndex {
@ -2668,6 +2671,51 @@ mod tests {
assert_eq!(am.slot_info(SlotRef::scene(4)).asset_id, Some(2)); assert_eq!(am.slot_info(SlotRef::scene(4)).asset_id, Some(2));
} }
#[test]
fn shutdown_clears_physical_memory_bank_slots() {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let glyph_data = test_glyph_asset_data();
let scene = test_scene();
let scene_data = encode_scene_payload(&scene);
let glyph_entry = test_glyph_asset_entry("preload_glyphs", 16, 16);
let scene_entry = test_scene_asset_entry("preload_scene", &scene_data, &scene);
let mut payload = Vec::new();
payload.extend_from_slice(&glyph_data);
payload.extend_from_slice(&scene_data);
let mut shifted_scene_entry = scene_entry;
shifted_scene_entry.offset = glyph_data.len() as u64;
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
scene_installer,
);
am.initialize_for_cartridge(
vec![glyph_entry, shifted_scene_entry],
vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 2, slot: 0 }],
AssetsPayloadSource::from_bytes(payload),
);
assert!(banks.glyph_bank_slot(0).is_some());
assert!(banks.scene_bank_slot(0).is_some());
assert_eq!(am.slot_info(SlotRef::gfx(0)).asset_id, Some(0));
assert_eq!(am.slot_info(SlotRef::scene(0)).asset_id, Some(2));
am.shutdown();
assert!(banks.glyph_bank_slot(0).is_none());
assert!(banks.scene_bank_slot(0).is_none());
assert_eq!(am.slot_info(SlotRef::gfx(0)).asset_id, None);
assert_eq!(am.slot_info(SlotRef::scene(0)).asset_id, None);
}
#[test] #[test]
fn test_load_returns_asset_not_found() { fn test_load_returns_asset_not_found() {
let banks = Arc::new(MemoryBanks::new()); let banks = Arc::new(MemoryBanks::new());

View File

@ -24,6 +24,8 @@ pub trait GlyphBankPoolAccess: Send + Sync {
pub trait GlyphBankPoolInstaller: Send + Sync { pub trait GlyphBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident GlyphBank in the specified slot. /// Atomically swaps the resident GlyphBank in the specified slot.
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>); fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>);
/// Clears all resident GlyphBank slots.
fn clear_glyph_banks(&self);
} }
/// Non-generic interface for peripherals to access sound banks. /// Non-generic interface for peripherals to access sound banks.
@ -38,6 +40,8 @@ pub trait SoundBankPoolAccess: Send + Sync {
pub trait SoundBankPoolInstaller: Send + Sync { pub trait SoundBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident SoundBank in the specified slot. /// Atomically swaps the resident SoundBank in the specified slot.
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>); fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>);
/// Clears all resident SoundBank slots.
fn clear_sound_banks(&self);
} }
/// Non-generic interface for peripherals to access scene banks. /// Non-generic interface for peripherals to access scene banks.
@ -52,6 +56,8 @@ pub trait SceneBankPoolAccess: Send + Sync {
pub trait SceneBankPoolInstaller: Send + Sync { pub trait SceneBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident SceneBank in the specified slot. /// Atomically swaps the resident SceneBank in the specified slot.
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>); fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>);
/// Clears all resident SceneBank slots.
fn clear_scene_banks(&self);
} }
/// Centralized container for all hardware memory banks. /// Centralized container for all hardware memory banks.
@ -100,6 +106,10 @@ impl GlyphBankPoolInstaller for MemoryBanks {
pool[slot] = Some(bank); pool[slot] = Some(bank);
} }
} }
fn clear_glyph_banks(&self) {
self.glyph_bank_pool.write().unwrap().fill(None);
}
} }
impl SoundBankPoolAccess for MemoryBanks { impl SoundBankPoolAccess for MemoryBanks {
@ -120,6 +130,10 @@ impl SoundBankPoolInstaller for MemoryBanks {
pool[slot] = Some(bank); pool[slot] = Some(bank);
} }
} }
fn clear_sound_banks(&self) {
self.sound_bank_pool.write().unwrap().fill(None);
}
} }
impl SceneBankPoolAccess for MemoryBanks { impl SceneBankPoolAccess for MemoryBanks {
@ -140,6 +154,10 @@ impl SceneBankPoolInstaller for MemoryBanks {
pool[slot] = Some(bank); pool[slot] = Some(bank);
} }
} }
fn clear_scene_banks(&self) {
self.scene_bank_pool.write().unwrap().fill(None);
}
} }
impl HalRenderResourceAccess for MemoryBanks { impl HalRenderResourceAccess for MemoryBanks {

View File

@ -252,9 +252,10 @@ mod tests {
use crate::firmware::firmware_state::HubHomeStep; use crate::firmware::firmware_state::HubHomeStep;
use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::TestPlatform; use prometeu_drivers::{GlyphBankPoolAccess, SceneBankPoolAccess, TestPlatform};
use prometeu_hal::RuntimePlatform; use prometeu_hal::RuntimePlatform;
use prometeu_hal::app_mode::AppMode; use prometeu_hal::app_mode::AppMode;
use prometeu_hal::asset::{BankType, SlotRef};
use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect; use prometeu_hal::primitives::Rect;
@ -851,27 +852,43 @@ mod tests {
}; };
assert_eq!(firmware.os.vm().current_app_id(), 1); assert_eq!(firmware.os.vm().current_app_id(), 1);
assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console"); assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console");
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)
);
firmware.request_home_from_host(); firmware.request_home_from_host();
firmware firmware.tick(&InputSignals::default(), &mut platform);
.os
.lifecycle()
.advance_game_pause_budget(stress_task)
.expect("pause budget should force Stress to suspend");
firmware.os.vm().transition_render_owner(AppMode::Shell, 0);
firmware.change_state(
FirmwareState::HubHome(HubHomeStep),
&InputSignals::default(),
&mut platform,
);
let hub_ownership = firmware.os.vm().active_render_ownership(); let hub_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(hub_ownership.app_mode, AppMode::Shell); assert_eq!(hub_ownership.app_mode, AppMode::Shell);
assert_eq!(hub_ownership.app_id, 0); assert_eq!(hub_ownership.app_id, 0);
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)
);
firmware.tick(&InputSignals::default(), &mut platform); firmware.tick(&InputSignals::default(), &mut platform);
let dummy_click = let dummy_click =
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
firmware.tick(&dummy_click, &mut platform); firmware.tick(&dummy_click, &mut platform);
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_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed)); assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed));
assert_eq!( assert_eq!(
@ -915,6 +932,60 @@ mod tests {
let front = platform.local_hardware().gfx.front_buffer(); let front = platform.local_hardware().gfx.front_buffer();
let center = 240 + 135 * 480; let center = 240 + 135 * 480;
assert_eq!(front[center], Color::GREEN.raw()); assert_eq!(front[center], Color::GREEN.raw());
firmware.request_home_from_host();
firmware.tick(&InputSignals::default(), &mut platform);
let hub_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(hub_ownership.app_mode, AppMode::Shell);
assert_eq!(hub_ownership.app_id, 0);
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);
assert_eq!(firmware.os.lifecycle().task_state(dummy_task), Some(TaskState::Closed));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(dummy_task),
Ok(ProcessState::Stopped)
);
assert_eq!(
firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| session.task_id),
None
);
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 second_stress_task = firmware
.os
.lifecycle()
.resident_game_task()
.expect("Stress should be resident after switching back");
assert_ne!(second_stress_task, stress_task);
assert_ne!(second_stress_task, dummy_task);
firmware.tick(&InputSignals::default(), &mut platform);
match &firmware.state {
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, second_stress_task),
other => panic!("expected second Stress GameRunning state, got {:?}", other),
}
assert_eq!(firmware.os.vm().current_app_id(), 1);
assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console");
firmware.tick(&InputSignals::default(), &mut platform);
let stress_ownership = firmware.os.vm().active_render_ownership();
assert_eq!(stress_ownership.app_mode, AppMode::Game);
assert_eq!(stress_ownership.app_id, 1);
} }
#[test] #[test]

View File

@ -48,6 +48,8 @@ impl LoadCartridgeStep {
format!("Loading cartridge: {}", self.cartridge.title), format!("Loading cartridge: {}", self.cartridge.title),
); );
ctx.platform.game2d_frame_composer().unbind_scene();
// Initialize Asset Manager // Initialize Asset Manager
ctx.platform.assets_mut().initialize_for_cartridge( ctx.platform.assets_mut().initialize_for_cartridge(
self.cartridge.asset_table.clone(), self.cartridge.asset_table.clone(),

View File

@ -26,16 +26,22 @@ const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct PresentationState { struct PresentationState {
latest_published_frame: u64, latest_published_frame: Option<PresentationFrameMarker>,
last_presented_frame: Option<u64>, last_presented_frame: Option<PresentationFrameMarker>,
host_invalidated: bool, host_invalidated: bool,
redraw_requested: bool, redraw_requested: bool,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PresentationFrameMarker {
ownership: RenderOwnership,
frame_id: u64,
}
impl Default for PresentationState { impl Default for PresentationState {
fn default() -> Self { fn default() -> Self {
Self { Self {
latest_published_frame: 0, latest_published_frame: None,
last_presented_frame: None, last_presented_frame: None,
host_invalidated: true, host_invalidated: true,
redraw_requested: false, redraw_requested: false,
@ -44,9 +50,10 @@ impl Default for PresentationState {
} }
impl PresentationState { impl PresentationState {
fn note_published_frame(&mut self, frame_index: u64) { fn note_published_frame(&mut self, ownership: RenderOwnership, frame_id: u64) {
if frame_index > self.latest_published_frame { let marker = PresentationFrameMarker { ownership, frame_id };
self.latest_published_frame = frame_index; if self.latest_published_frame != Some(marker) {
self.latest_published_frame = Some(marker);
} }
} }
@ -55,7 +62,7 @@ impl PresentationState {
} }
fn needs_redraw(&self) -> bool { fn needs_redraw(&self) -> bool {
self.host_invalidated || self.last_presented_frame != Some(self.latest_published_frame) self.host_invalidated || self.last_presented_frame != self.latest_published_frame
} }
fn should_request_redraw(&mut self) -> bool { fn should_request_redraw(&mut self) -> bool {
@ -70,7 +77,7 @@ impl PresentationState {
fn mark_presented(&mut self) { fn mark_presented(&mut self) {
self.redraw_requested = false; self.redraw_requested = false;
self.host_invalidated = false; self.host_invalidated = false;
self.last_presented_frame = Some(self.latest_published_frame); self.last_presented_frame = self.latest_published_frame;
} }
} }
@ -437,9 +444,11 @@ impl ApplicationHandler for HostRunner {
if let Some(worker_frame) = self.render_frame_store.latest_frame() if let Some(worker_frame) = self.render_frame_store.latest_frame()
&& should_present_worker_frame(&self.firmware.state, &worker_frame, active_ownership) && should_present_worker_frame(&self.firmware.state, &worker_frame, active_ownership)
{ {
self.presentation.note_published_frame(worker_frame.frame_id.get()); self.presentation
.note_published_frame(worker_frame.ownership, worker_frame.frame_id.get());
} else { } else {
self.presentation.note_published_frame(self.firmware.os.frame_index()); self.presentation
.note_published_frame(active_ownership, self.firmware.os.frame_index());
} }
if was_debugger_connected != self.debugger.stream.is_some() if was_debugger_connected != self.debugger.stream.is_some()
@ -505,14 +514,37 @@ mod tests {
#[test] #[test]
fn presentation_state_requests_redraw_for_new_frame_publication() { fn presentation_state_requests_redraw_for_new_frame_publication() {
let ownership = RenderOwnership::new(1, AppMode::Game, 1);
let mut state = PresentationState::default(); let mut state = PresentationState::default();
state.mark_presented(); state.mark_presented();
state.note_published_frame(1); state.note_published_frame(ownership, 1);
assert!(state.should_request_redraw()); assert!(state.should_request_redraw());
state.mark_presented(); state.mark_presented();
assert_eq!(state.last_presented_frame, Some(1)); assert_eq!(
state.last_presented_frame,
Some(PresentationFrameMarker { ownership, frame_id: 1 })
);
}
#[test]
fn presentation_state_requests_redraw_when_ownership_changes_with_lower_frame_id() {
let first = RenderOwnership::new(3, AppMode::Game, 1);
let next = RenderOwnership::new(4, AppMode::Game, 2);
let mut state = PresentationState::default();
state.note_published_frame(first, 120);
state.mark_presented();
state.note_published_frame(next, 0);
assert!(state.should_request_redraw());
state.mark_presented();
assert_eq!(
state.last_presented_frame,
Some(PresentationFrameMarker { ownership: next, frame_id: 0 })
);
} }
#[test] #[test]
@ -608,11 +640,14 @@ mod tests {
let mut presentation = PresentationState::default(); let mut presentation = PresentationState::default();
presentation.mark_presented(); presentation.mark_presented();
let published = store.latest_frame().expect("published frame"); let published = store.latest_frame().expect("published frame");
presentation.note_published_frame(published.frame_id.get()); presentation.note_published_frame(published.ownership, published.frame_id.get());
assert!(presentation.should_request_redraw()); assert!(presentation.should_request_redraw());
presentation.mark_presented(); presentation.mark_presented();
assert_eq!(presentation.last_presented_frame, Some(9)); assert_eq!(
presentation.last_presented_frame,
Some(PresentationFrameMarker { ownership: published.ownership, frame_id: 9 })
);
} }
#[test] #[test]

View File

@ -1,4 +1,5 @@
{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"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":"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-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

@ -0,0 +1,91 @@
---
id: AGD-0047
ticket: hub-suspended-game-kill-affordance
title: Hub Suspended Game Kill Affordance
status: accepted
created: 2026-07-05
resolved:
decision:
tags: [hub, lifecycle, game, ui]
---
## Contexto
A troca entre cartridges agora mata a VM do jogo anterior quando outro jogo e carregado. Ainda assim, voltar para o Hub via Home/ESC pode deixar um jogo residente/suspenso, pronto para retomar pelo fluxo existente.
Hoje o Hub nao comunica esse estado de forma clara. O usuario pode estar no Hub sem saber que ha um jogo suspenso mantendo task, sessao de VM, estado de pausa/audio e possivelmente recursos associados. A proposta e exibir o nome do jogo suspenso no canto inferior direito, dentro de uma caixa vermelha, e permitir que um clique nessa caixa feche/mate esse jogo.
## Problema
Precisamos decidir se esse elemento e apenas um indicador visual ou tambem um controle destrutivo de lifecycle. Se ele mata o jogo, a acao precisa ter contrato explicito: qual entidade e encerrada, quais estados sao limpos, como o Hub atualiza a UI, e se ha confirmacao para evitar kill acidental.
## Pontos Criticos
- Fato: o jogo suspenso/residente e uma entidade de lifecycle, nao apenas uma linha visual no Hub.
- Fato: iniciar outro jogo ja mata a VM anterior; esta proposta adiciona um caminho manual para a mesma intencao sem carregar outro cartridge.
- Risco: um clique acidental em uma caixa vermelha pode perder progresso/estado sem aviso.
- Risco: matar somente a task sem limpar sessao/render/audio/assets deixaria estado residual parecido com os bugs recentes de troca de jogo.
- Tradeoff: confirmacao protege o usuario, mas adiciona friccao a um controle que parece operacional/debug.
- Hipotese: limitar o controle a jogos `Game` residentes/suspensos evita misturar shell/system apps no mesmo affordance.
## Opcoes
### Opcao A - Indicador com kill imediato
- **Abordagem:** mostrar uma caixa vermelha no canto inferior direito somente quando houver jogo residente/suspenso; um clique mata imediatamente esse jogo e remove o indicador.
- **Pro:** simples, rapido, combina com a proposta original.
- **Contra:** risco de perda acidental de estado.
- **Manutencao:** baixa complexidade, mas exige testes fortes de cleanup.
### Opcao B - Indicador com confirmacao em dois passos
- **Abordagem:** primeiro clique arma a acao ou muda o texto/estado visual; segundo clique confirma kill.
- **Pro:** reduz kill acidental sem exigir modal.
- **Contra:** mais estado no Hub e precisa de feedback visual claro.
- **Manutencao:** moderada; adiciona timeout/estado armado se quisermos evitar confirmacao persistente.
### Opcao C - Apenas indicador, sem kill
- **Abordagem:** mostrar o jogo suspenso, mas matar continua ocorrendo apenas ao carregar outro jogo ou por controles futuros.
- **Pro:** menor risco destrutivo.
- **Contra:** nao resolve a necessidade declarada de fechar/matar o jogo pelo Hub.
- **Manutencao:** baixa, mas provavelmente incompleta.
## Sugestao / Recomendacao
Seguir com a Opcao A se aceitarmos que o Hub atual e uma superficie operacional/dev e que a caixa vermelha comunica suficientemente a natureza destrutiva da acao. O contrato recomendado:
- aparece somente para jogo `Game` residente/suspenso;
- mostra somente o nome do jogo;
- fica no canto inferior direito;
- usa vermelho como indicativo de encerramento;
- clique mata a task/sessao residente e limpa o estado relacionado;
- apos o kill, o Hub permanece no Hub e o indicador desaparece.
Se a prioridade for evitar perda acidental de estado, promover para Opcao B antes da decision.
## Perguntas em Aberto
- [ ] O indicador deve aparecer somente para jogos `Game` residentes/suspensos?
- [ ] O clique vermelho deve matar imediatamente ou exigir confirmacao?
- [ ] O kill manual deve reutilizar exatamente o mesmo caminho de encerramento usado antes de carregar outro cartridge?
- [ ] O kill deve limpar assets/compositor imediatamente ou apenas task/session/render/audio?
- [ ] Deve haver evento/log explicito para "resident game killed from Hub"?
## Criterio para Encerrar
A agenda pode virar decision quando concordarmos em:
- quando o indicador aparece;
- qual texto e posicao minima ele usa;
- se a acao e imediata ou confirmada;
- qual contrato de cleanup o kill manual deve cumprir;
- quais testes provam que o jogo suspenso foi morto e nao pode ser retomado.
## Resolution
Ainda em aberto.
## Next Step
Responder as perguntas em aberto e entao aceitar a agenda para escrever a decision.

View File

@ -0,0 +1,73 @@
---
id: DEC-0040
ticket: hub-suspended-game-kill-affordance
title: Hub Suspended Game Kill Affordance
status: accepted
created: 2026-07-05
ref_agenda: AGD-0047
tags: [hub, lifecycle, game, ui]
---
## Context
O Hub pode ficar ativo enquanto existe um jogo `Game` residente/suspenso apos Home/ESC. Esse estado e intencional para retomada, mas hoje nao fica visivel o suficiente e nao ha uma acao direta no Hub para matar o jogo sem iniciar outro cartridge.
A troca de cartridges ja encerra a VM anterior antes de carregar o proximo jogo. Esta decision define o affordance manual equivalente no Hub.
## Decision
O Hub MUST exibir um indicador de jogo suspenso quando houver exatamente um jogo `Game` residente/suspenso associado ao lifecycle do SystemOS.
O indicador MUST:
- aparecer somente para jogos do perfil `Game`;
- mostrar somente o nome/titulo do jogo suspenso;
- ficar no canto inferior direito do Hub;
- ser apresentado como uma caixa vermelha;
- funcionar como controle destrutivo: um clique MUST matar imediatamente o jogo suspenso;
- desaparecer no mesmo fluxo em que o jogo deixa de ser residente.
O clique no indicador MUST reutilizar o mesmo caminho de encerramento de jogo residente usado pelo cartridge switching. A implementacao MUST NOT criar um segundo caminho parcial de cleanup.
Ao matar o jogo pelo Hub, o runtime MUST limpar o estado pertencente ao jogo residente:
- task/process lifecycle;
- VM session;
- resident game state;
- render ownership retornando ao Shell/Hub;
- game audio pause/lifecycle state;
- Game2D composer binding/cache state;
- cartridge asset manager state and physical memory banks associated with the resident game.
O Hub MUST permanecer no Hub apos a acao. O jogo morto MUST NOT ser retomavel depois do kill. A acao MUST emitir um log `INFO` explicito, com source `HUB` ou `POS`, equivalente a `Resident game killed from Hub: <title>`.
Nao havera confirmacao ou segundo clique nesta versao. A cor vermelha e a posicao dedicada sao o affordance de acao destrutiva.
## Rationale
O fluxo aprovado privilegia simplicidade operacional. A intencao do controle e fechar uma VM suspensa rapidamente, sem iniciar outro jogo apenas para forcar o encerramento do anterior.
Limitar a UI a jogos `Game` evita misturar semantics de shell/system apps com cartridge runtime. Reusar o caminho de termination existente reduz risco de divergencia entre "kill por troca de cartridge" e "kill manual pelo Hub", especialmente apos os bugs recentes envolvendo scene/glyph/assets residuais.
O kill imediato e aceitavel porque o controle e vermelho, posicionado como acao especifica, e opera sobre um jogo que ja esta fora do primeiro plano. Se futuramente houver save-state persistente ou risco maior de perda de progresso, uma nova decision pode revisar este contrato para confirmacao em dois passos.
## Implications
- O Hub precisa consultar estado de jogo residente/suspenso de forma explicita.
- A camada de lifecycle precisa expor uma operacao clara de matar jogo residente sem carregar outro cartridge.
- O cleanup manual precisa ter a mesma abrangencia do cleanup usado por switch.
- O render/presentation state nao pode continuar apontando para ownership/frame do jogo morto.
- Testes devem provar que o indicador aparece, que o clique mata, e que o jogo nao pode ser retomado depois.
## Propagation Targets
- specs: atualizar contrato de Hub/lifecycle se houver spec canonica aplicavel.
- plans: criar plano de execucao para UI do Hub, facade/lifecycle kill e testes.
- code: `prometeu_hub`, firmware/lifecycle facade, runtime cleanup, render/audio/assets cleanup conforme necessario.
- tests: Hub rendering/click hitbox, lifecycle termination, switch-vs-manual cleanup equivalence, no-resume-after-kill.
- docs: registrar lesson apenas apos execucao publicada.
## References
- Agenda: AGD-0047
- Related: DSC-0043 / DEC-0039 cartridge switch orchestrator

View File

@ -0,0 +1,85 @@
---
id: PLN-0163
ticket: hub-suspended-game-kill-affordance
title: Render Hub Suspended Game Kill Control
status: open
created: 2026-07-05
ref_decisions: [DEC-0040]
tags: [hub, lifecycle, game, ui]
---
## Briefing
DEC-0040 requires the Hub to show a red bottom-right control when a `Game` cartridge is resident/suspended, displaying only the game title and killing the game immediately on click.
## Objective
Render and handle the Hub suspended-game kill control using the lifecycle operation from PLN-0164.
## Dependencies
- Source decision: DEC-0040.
- Depends on PLN-0164 for the callable resident-game termination operation.
## Scope
- Show a red box in the bottom-right of the Hub only when a `Game` is resident/suspended.
- Render only the suspended game's title in that box.
- Register a click/hitbox for the box.
- On click, call the canonical resident-game termination operation.
- Keep the Hub active after the click.
- Remove the indicator immediately after the resident game is no longer present.
## Non-Goals
- Do not add confirmation or two-click arming.
- Do not show shell/system apps in this control.
- Do not redesign the Hub layout beyond the minimal bottom-right control.
- Do not add save-state or suspend-state management.
## Execution Method
1. Identify the Hub rendering and input code.
- Primary target: `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`.
- Determine how Hub cards/buttons currently compute layout and hit testing.
2. Add resident-game status to the Hub view model.
- Read only `Game` resident/suspended state.
- Include the game title needed for display.
- Keep the value absent for shell/system apps and for no resident game.
3. Render the kill control.
- Position it in the bottom-right of the internal viewport.
- Use a red filled box with restrained dimensions.
- Draw only the game title.
- Keep the text inside the box for long titles by truncating or fitting according to existing Hub text conventions.
4. Add click handling.
- Use the box hit region.
- On click, call the resident-game termination operation from PLN-0164.
- Do not navigate away from Hub.
- Ensure the next Hub frame no longer renders the control when the resident game is gone.
## Acceptance Criteria
- [ ] The control appears in Hub only while a `Game` resident/suspended game exists.
- [ ] The control displays only the game title.
- [ ] The control is visually red and placed in the bottom-right of the Hub.
- [ ] Clicking the control kills the resident game immediately.
- [ ] The Hub remains in Hub after the click.
- [ ] The control disappears after the game is killed.
- [ ] Shell/system apps do not appear in this control.
## Tests
- Hub render/unit test for presence when a resident `Game` exists.
- Hub render/unit test for absence when no resident game exists.
- Hub render/unit test for absence for non-Game app modes.
- Hub click/hitbox test proving the termination operation is invoked.
- Test for long title handling if existing Hub layout tests cover text bounds.
## Affected Artifacts
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`
- Any Hub facade/view-model modules used by the program
- Hub rendering/input tests

View File

@ -0,0 +1,93 @@
---
id: PLN-0164
ticket: hub-suspended-game-kill-affordance
title: Expose Resident Game Termination Contract
status: open
created: 2026-07-05
ref_decisions: [DEC-0040]
tags: [hub, lifecycle, game, ui]
---
## Briefing
DEC-0040 requires the Hub kill affordance to reuse the same resident-game termination path used by cartridge switching. This plan creates the lifecycle/runtime contract that the Hub UI can call without duplicating cleanup logic.
## Objective
Expose a single SystemOS/Firmware operation that terminates the current resident `Game` cartridge from Hub context and performs the complete cleanup required by DEC-0040.
## Dependencies
- Source decision: DEC-0040.
- Related lifecycle behavior: DSC-0043 / DEC-0039 cartridge switch orchestrator.
- Must be completed before PLN-0163 can wire the Hub UI click.
## Scope
- Add or reuse a resident-game termination operation in SystemOS lifecycle/facade code.
- Ensure the operation closes task/process lifecycle state.
- Ensure the operation removes the VM session and resident game state.
- Ensure render ownership returns to Shell/Hub.
- Ensure game audio pause/lifecycle state is cleared.
- Ensure Game2D composer scene binding/cache state is cleared.
- Ensure cartridge asset manager state and physical memory banks are cleared.
- Emit an explicit `INFO` log for manual Hub kill.
## Non-Goals
- Do not change cartridge switching semantics except to reuse the same termination path.
- Do not add Hub UI rendering or hit testing; that belongs to PLN-0163.
- Do not add confirmation semantics; DEC-0040 explicitly rejects confirmation for this version.
- Do not support shell/system app killing through this affordance.
## Execution Method
1. Locate the current game switch termination path in firmware/SystemOS lifecycle code.
- Files: `crates/console/prometeu-firmware/src/firmware/`, `crates/console/prometeu-system/src/`.
- Confirm which function currently closes the resident game before switching cartridges.
2. Extract or expose a named resident-game termination operation.
- The operation must be callable from Hub context without a target cartridge.
- It must return an explicit status: no resident game, terminated game, or failure if the lifecycle cannot close cleanly.
- It must preserve existing switch behavior by making switch call the same operation or a shared internal helper.
3. Wire complete cleanup into that operation.
- Close task/process lifecycle state.
- Remove the VM session and resident game state.
- Transition render ownership to Shell/Hub.
- Clear game lifecycle audio pause state.
- Unbind `Game2D` scene/composer state.
- Shutdown cartridge assets and clear physical memory banks.
4. Emit the manual kill log.
- Source must be `HUB` or `POS`.
- Level must be `INFO`.
- Message must include the killed title, equivalent to `Resident game killed from Hub: <title>`.
## Acceptance Criteria
- [ ] There is one canonical resident-game termination path used by both cartridge switching and Hub manual kill.
- [ ] Calling the operation with no resident game is harmless and does not mutate unrelated Hub state.
- [ ] Calling the operation with a suspended `Game` closes the task/process and removes the VM session.
- [ ] The runtime cannot resume the killed game after the operation.
- [ ] Render ownership is Shell/Hub after the operation.
- [ ] Game audio pause/lifecycle state is not left active after the operation.
- [ ] Composer and cartridge asset/bank state are cleared.
- [ ] An explicit INFO log is emitted for Hub manual kill.
## Tests
- Unit or integration test around the lifecycle operation:
- no resident game is a no-op;
- resident game is terminated;
- VM session lookup returns none after kill;
- resident game state returns none after kill;
- render owner is Shell/Hub after kill.
- Regression test proving cartridge switch still terminates through the same cleanup behavior.
- Asset/composer cleanup assertions should mirror the recent Stress/Dummy switch tests where applicable.
## Affected Artifacts
- `crates/console/prometeu-system/src/`
- `crates/console/prometeu-firmware/src/`
- Existing switch tests under firmware/SystemOS test modules

View File

@ -0,0 +1,92 @@
---
id: PLN-0165
ticket: hub-suspended-game-kill-affordance
title: Validate Hub Kill Flow End To End
status: open
created: 2026-07-05
ref_decisions: [DEC-0040]
tags: [hub, lifecycle, game, ui]
---
## Briefing
DEC-0040 requires that the Hub kill affordance fully terminates the suspended game and prevents resume. PLN-0164 and PLN-0163 provide the operation and UI; this plan validates the full flow end to end.
## Objective
Add deterministic tests that exercise Home -> suspended game indicator -> Hub kill -> no resume/no residue.
## Dependencies
- Source decision: DEC-0040.
- Depends on PLN-0164 for the termination operation.
- Depends on PLN-0163 for Hub UI and click behavior.
## Scope
- Cover a real game launch, Home/ESC return to Hub, indicator visibility, click-to-kill, and post-kill cleanup.
- Verify task/session/resident state are gone.
- Verify render ownership and Hub state are correct.
- Verify assets/composer cleanup where observable.
- Verify log emission.
## Non-Goals
- Do not test OS-native window manager shell app closure here.
- Do not create new cartridges unless existing Stress/Dummy cartridges cannot cover the scenario.
- Do not use timing sleeps or visual/manual assertions as primary evidence.
## Execution Method
1. Extend firmware/SystemOS end-to-end tests.
- Primary file: `crates/console/prometeu-firmware/src/firmware/firmware.rs` or a focused SystemOS runtime test if the existing test harness fits better.
- Launch `Stress Console` or a simple test cartridge.
- Return to Hub through the real Home path.
- Assert the resident game remains suspended before kill.
2. Simulate Hub click on the bottom-right kill control.
- Use deterministic coordinates from the Hub layout.
- Avoid hard-coded magic numbers if the Hub exposes layout constants/helpers.
3. Assert lifecycle cleanup.
- Resident game task is closed.
- Process is stopped.
- VM session lookup returns none.
- Resident game state returns none.
- Foreground owner remains Hub/Shell.
- Render owner is Shell/Hub.
4. Assert resource cleanup.
- Composer is unbound.
- Cartridge asset slots and physical banks are cleared where existing platform test access permits.
- Audio lifecycle pause is not active.
5. Assert no resume path remains.
- Clicking the old resume area or querying resident state must not restore the killed game.
- The Hub control must be absent on the next frame.
6. Assert log evidence.
- Recent logs contain an INFO event equivalent to `Resident game killed from Hub: <title>`.
## Acceptance Criteria
- [ ] End-to-end test launches a game and returns to Hub with resident game suspended.
- [ ] Test confirms the kill control is present before click.
- [ ] Test clicks the control and remains in Hub.
- [ ] Test proves the game cannot be resumed after kill.
- [ ] Test proves task/session/resident/render/audio/assets/composer cleanup.
- [ ] Test proves the explicit INFO log exists.
- [ ] Existing cartridge switch tests still pass.
## Tests
- `cargo test -p prometeu-firmware`
- `cargo test -p prometeu-system`
- `cargo test -p prometeu-host-desktop-winit` if host presentation behavior is touched by implementation.
- `cargo clippy -p prometeu-system -p prometeu-firmware --all-targets -- -D warnings`
## Affected Artifacts
- Firmware/SystemOS end-to-end tests
- Hub tests
- Lifecycle/runtime tests

View File

@ -0,0 +1,78 @@
---
id: PLN-0166
ticket: hub-suspended-game-kill-affordance
title: Publish Hub Kill Contract Spec And Lesson Hooks
status: open
created: 2026-07-05
ref_decisions: [DEC-0040]
tags: [hub, lifecycle, game, ui]
---
## Briefing
DEC-0040 introduces a user-visible Hub lifecycle control. Any canonical spec covering Hub, lifecycle, resident games, or cartridge switching must reflect the accepted contract before implementation is considered complete.
## Objective
Publish the Hub suspended-game kill contract in the appropriate canonical spec location and prepare the discussion thread for post-execution lessons.
## Dependencies
- Source decision: DEC-0040.
- Should run after PLN-0164, PLN-0163, and PLN-0165 identify the exact final module boundaries.
## Scope
- Locate canonical specs for Hub behavior, SystemOS lifecycle, resident game state, and cartridge switching.
- Add English normative text for the red bottom-right kill affordance.
- Document the cleanup contract at the spec level.
- Document required log/event behavior.
- Leave lesson creation for post-execution housekeeping.
## Non-Goals
- Do not create lessons before implementation is complete.
- Do not move discussion artifacts into legacy docs paths.
- Do not reopen DEC-0040 semantics unless implementation proves the contract insufficient.
## Execution Method
1. Locate canonical specs.
- Search existing `specs/`, runtime docs, or repository-approved spec paths.
- Do not write new normative text under `discussion/lessons/`.
2. Update Hub spec text.
- State that Hub shows a bottom-right red control when a `Game` resident/suspended cartridge exists.
- State that the control displays only the game title.
- State that click kills immediately and keeps the user in Hub.
3. Update lifecycle/spec text.
- State that manual Hub kill uses the same resident-game termination path as cartridge switching.
- State cleanup obligations: task/process, VM session, resident state, render ownership, audio lifecycle, composer, assets, physical banks.
- State that the killed game cannot be resumed.
4. Update logging/spec text if a logging contract exists.
- Record the required INFO event and message shape.
5. Prepare housekeeping note.
- After implementation and validation, run discussion housekeeping for DSC-0044 with an English lesson if the implementation yields durable knowledge.
## Acceptance Criteria
- [ ] Canonical spec text exists for the Hub kill affordance.
- [ ] Spec text is in English.
- [ ] Spec text includes visibility, layout, click behavior, cleanup, no-resume behavior, and log event requirements.
- [ ] No normative text is added to `discussion/lessons/`.
- [ ] `discussion validate` passes after artifact/spec updates.
## Tests
- `discussion validate`
- Documentation/spec review against DEC-0040
- Any repository doc lint/check if present
## Affected Artifacts
- Canonical specs location, to be identified during execution
- `discussion/workflow/plans/`
- Later `discussion/lessons/` only through housekeeping after implementation