diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index b80ddff2..e846d8ec 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -1858,6 +1858,9 @@ impl AssetManager { self.glyph_slot_index.clear(); self.sound_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 { @@ -2668,6 +2671,51 @@ mod tests { 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; + let sound_installer = Arc::clone(&banks) as Arc; + let scene_installer = Arc::clone(&banks) as Arc; + + 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] fn test_load_returns_asset_not_found() { let banks = Arc::new(MemoryBanks::new()); diff --git a/crates/console/prometeu-drivers/src/memory_banks.rs b/crates/console/prometeu-drivers/src/memory_banks.rs index a245a512..1eef36e6 100644 --- a/crates/console/prometeu-drivers/src/memory_banks.rs +++ b/crates/console/prometeu-drivers/src/memory_banks.rs @@ -24,6 +24,8 @@ pub trait GlyphBankPoolAccess: Send + Sync { pub trait GlyphBankPoolInstaller: Send + Sync { /// Atomically swaps the resident GlyphBank in the specified slot. fn install_glyph_bank(&self, slot: usize, bank: Arc); + /// Clears all resident GlyphBank slots. + fn clear_glyph_banks(&self); } /// Non-generic interface for peripherals to access sound banks. @@ -38,6 +40,8 @@ pub trait SoundBankPoolAccess: Send + Sync { pub trait SoundBankPoolInstaller: Send + Sync { /// Atomically swaps the resident SoundBank in the specified slot. fn install_sound_bank(&self, slot: usize, bank: Arc); + /// Clears all resident SoundBank slots. + fn clear_sound_banks(&self); } /// Non-generic interface for peripherals to access scene banks. @@ -52,6 +56,8 @@ pub trait SceneBankPoolAccess: Send + Sync { pub trait SceneBankPoolInstaller: Send + Sync { /// Atomically swaps the resident SceneBank in the specified slot. fn install_scene_bank(&self, slot: usize, bank: Arc); + /// Clears all resident SceneBank slots. + fn clear_scene_banks(&self); } /// Centralized container for all hardware memory banks. @@ -100,6 +106,10 @@ impl GlyphBankPoolInstaller for MemoryBanks { pool[slot] = Some(bank); } } + + fn clear_glyph_banks(&self) { + self.glyph_bank_pool.write().unwrap().fill(None); + } } impl SoundBankPoolAccess for MemoryBanks { @@ -120,6 +130,10 @@ impl SoundBankPoolInstaller for MemoryBanks { pool[slot] = Some(bank); } } + + fn clear_sound_banks(&self) { + self.sound_bank_pool.write().unwrap().fill(None); + } } impl SceneBankPoolAccess for MemoryBanks { @@ -140,6 +154,10 @@ impl SceneBankPoolInstaller for MemoryBanks { pool[slot] = Some(bank); } } + + fn clear_scene_banks(&self) { + self.scene_bank_pool.write().unwrap().fill(None); + } } impl HalRenderResourceAccess for MemoryBanks { diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 7d9fccdc..babe09eb 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -252,9 +252,10 @@ mod tests { use crate::firmware::firmware_state::HubHomeStep; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; - use prometeu_drivers::TestPlatform; + use prometeu_drivers::{GlyphBankPoolAccess, SceneBankPoolAccess, TestPlatform}; use prometeu_hal::RuntimePlatform; use prometeu_hal::app_mode::AppMode; + use prometeu_hal::asset::{BankType, SlotRef}; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; 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_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 - .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, - ); + 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); + 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); let dummy_click = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; 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!( @@ -915,6 +932,60 @@ mod tests { let front = platform.local_hardware().gfx.front_buffer(); let center = 240 + 135 * 480; 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] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs index ea2e0cfe..2cbeb823 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs @@ -48,6 +48,8 @@ impl LoadCartridgeStep { format!("Loading cartridge: {}", self.cartridge.title), ); + ctx.platform.game2d_frame_composer().unbind_scene(); + // Initialize Asset Manager ctx.platform.assets_mut().initialize_for_cartridge( self.cartridge.asset_table.clone(), diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index 0bb7611e..9161e07e 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -26,16 +26,22 @@ const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100); #[derive(Debug, Clone)] struct PresentationState { - latest_published_frame: u64, - last_presented_frame: Option, + latest_published_frame: Option, + last_presented_frame: Option, host_invalidated: bool, redraw_requested: bool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct PresentationFrameMarker { + ownership: RenderOwnership, + frame_id: u64, +} + impl Default for PresentationState { fn default() -> Self { Self { - latest_published_frame: 0, + latest_published_frame: None, last_presented_frame: None, host_invalidated: true, redraw_requested: false, @@ -44,9 +50,10 @@ impl Default for PresentationState { } impl PresentationState { - fn note_published_frame(&mut self, frame_index: u64) { - if frame_index > self.latest_published_frame { - self.latest_published_frame = frame_index; + fn note_published_frame(&mut self, ownership: RenderOwnership, frame_id: u64) { + let marker = PresentationFrameMarker { ownership, frame_id }; + if self.latest_published_frame != Some(marker) { + self.latest_published_frame = Some(marker); } } @@ -55,7 +62,7 @@ impl PresentationState { } 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 { @@ -70,7 +77,7 @@ impl PresentationState { fn mark_presented(&mut self) { self.redraw_requested = 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() && 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 { - 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() @@ -505,14 +514,37 @@ mod tests { #[test] fn presentation_state_requests_redraw_for_new_frame_publication() { + let ownership = RenderOwnership::new(1, AppMode::Game, 1); let mut state = PresentationState::default(); state.mark_presented(); - state.note_published_frame(1); + state.note_published_frame(ownership, 1); assert!(state.should_request_redraw()); 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] @@ -608,11 +640,14 @@ mod tests { let mut presentation = PresentationState::default(); presentation.mark_presented(); 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()); 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] diff --git a/discussion/index.ndjson b/discussion/index.ndjson index f1f4d8e5..34c069ce 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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-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"}]} diff --git a/discussion/workflow/agendas/AGD-0047-hub-suspended-game-kill-affordance.md b/discussion/workflow/agendas/AGD-0047-hub-suspended-game-kill-affordance.md new file mode 100644 index 00000000..650796cb --- /dev/null +++ b/discussion/workflow/agendas/AGD-0047-hub-suspended-game-kill-affordance.md @@ -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. diff --git a/discussion/workflow/decisions/DEC-0040-hub-suspended-game-kill-affordance.md b/discussion/workflow/decisions/DEC-0040-hub-suspended-game-kill-affordance.md new file mode 100644 index 00000000..2696065e --- /dev/null +++ b/discussion/workflow/decisions/DEC-0040-hub-suspended-game-kill-affordance.md @@ -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: `. + +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 diff --git a/discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md b/discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md new file mode 100644 index 00000000..22ecdfc1 --- /dev/null +++ b/discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md @@ -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 diff --git a/discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md b/discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md new file mode 100644 index 00000000..9176511b --- /dev/null +++ b/discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md @@ -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 diff --git a/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md b/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md new file mode 100644 index 00000000..8eba055b --- /dev/null +++ b/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md @@ -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 diff --git a/discussion/workflow/plans/PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md b/discussion/workflow/plans/PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md new file mode 100644 index 00000000..1372728e --- /dev/null +++ b/discussion/workflow/plans/PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md @@ -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