From 0ffb3467b113eeb5742ab870925ceff34b4a2049 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:03:02 +0100 Subject: [PATCH 01/13] SystemOS Cartridge Switch Orchestrator --- discussion/index.ndjson | 4 +- ...-systemos-cartridge-switch-orchestrator.md | 73 ++++++-- ...emos-game-cartridge-switch-orchestrator.md | 174 ++++++++++++++++++ ...ce-systemos-game-switch-target-contract.md | 86 +++++++++ ...sident-game-before-switching-cartridges.md | 98 ++++++++++ ...cover-failed-game-switches-back-to-home.md | 86 +++++++++ ...-dummy-boy-visual-switch-test-cartridge.md | 90 +++++++++ ...ess-to-dummy-boy-game-switch-end-to-end.md | 93 ++++++++++ ...tch-contract-and-direct-boot-separation.md | 93 ++++++++++ 9 files changed, 775 insertions(+), 22 deletions(-) create mode 100644 discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md create mode 100644 discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md create mode 100644 discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md create mode 100644 discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md create mode 100644 discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md create mode 100644 discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md create mode 100644 discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 519b2791..9c08136e 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":157,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0043","status":"open","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-03","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} +{"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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md b/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md index e0fe0855..476cc33b 100644 --- a/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md +++ b/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md @@ -2,7 +2,7 @@ id: AGD-0044 ticket: system-os-cartridge-switch-orchestrator title: SystemOS Cartridge Switch Orchestrator -status: open +status: accepted created: 2026-07-03 resolved: decision: @@ -90,35 +90,58 @@ pode cair em estados incoerentes: ## Sugestao / Recomendacao -Recomendo a Opcao A agora, com esta agenda aberta como marcador explicito de -escopo futuro. +`DSC-0041` foi fechada e o contrato de foreground/pausa/resume ja existe. A +recomendacao agora deixa de ser adiar e passa a ser fechar uma decisao curta +para o orquestrador de troca `Game A -> Home -> Game B`. -A ordem preferida passa a ser: +O contrato sugerido e: -1. fechar a `AGD-0041` para definir foreground, pausa/suspensao, Shell no - foreground e retorno ao mesmo Game; -2. voltar para esta agenda e decidir o contrato do orquestrador de troca de - cartucho no SystemOS; -3. so entao planejar a execucao de `Game A -> Home -> Game B`. - -Se durante a `AGD-0041` aparecer uma decisao que exige antecipar parte do -orquestrador, esta agenda deve ser reaberta/atualizada com a dependencia -concreta em vez de esconder a troca de cartucho dentro da agenda de foreground. +- trocar para outro Game e uma substituicao controlada, nao uma pausa/resume; +- o Game residente anterior e fechado/descartado; +- a VM session do Game anterior e removida; +- o novo Game sempre recebe uma VM session nova e zerada; +- se o novo load falhar, o sistema volta ao Home/Hub com diagnostico e nao + tenta preservar o Game anterior em v1; +- o loader continua sendo etapa interna, sem possuir politica de lifecycle; +- a evidencia minima deve usar dois cartuchos distintos, incluindo um cartucho + simplista `dummy-boy` alem do stress test. ## Perguntas em Aberto -- [ ] Depois da `AGD-0041`, qual estado representa um Game que sera descartado +- [x] Depois da `AGD-0041`, qual estado representa um Game que sera descartado em vez de retomado? -- [ ] O fechamento do Game atual e observavel para o firmware/game, ou e uma + - Resposta: fechamento/encerramento controlado, nao `PausedSuspended`. O Game + anterior deixa de ser resident/resumable e sua VM session e removida. +- [x] O fechamento do Game atual e observavel para o firmware/game, ou e uma destruicao interna do SystemOS? -- [ ] Quais estados cartridge-scoped devem ser limpos antes do novo load: VM, + - Resposta: deve ser observavel como uma transicao de lifecycle/firmware, mas + nao cooperativo nem bloqueante para o Game. O SystemOS tem autoridade para + encerrar e descartar a sessao. Pode ser emitido um evento de encerramento + para permitir save/flush cooperativo quando existir esse mecanismo, mas a + troca nao deve depender da resposta do Game. +- [x] Quais estados cartridge-scoped devem ser limpos antes do novo load: VM, assets, FS handles, input, audio, render ownership, telemetry, logs? -- [ ] Se o novo cartucho falha ao carregar, o sistema volta ao Home, mostra uma + - Resposta: a troca nao reaproveita VM. O Game anterior e encerrado, sua VM + session e descartada, e o novo Game recebe VM/session nova e zerada. Devem + ser limpos ou reatribuídos os estados cartridge-scoped ligados a VM, + handles abertos, input pendente/barreiras, audio do app, render ownership, + telemetry/debug/crash state do app. Logs historicos podem permanecer como + observabilidade. +- [x] Se o novo cartucho falha ao carregar, o sistema volta ao Home, mostra uma tela de erro, ou tenta preservar o Game anterior? -- [ ] Como a resolucao do novo alvo e representada sem depender de catalogo + - Resposta: o Game anterior ja foi encerrado. Se o novo load falha, a sessao + parcial do novo Game e descartada e o sistema volta ao Home/Hub com + diagnostico. Preservar o Game anterior fica fora de escopo em v1. +- [x] Como a resolucao do novo alvo e representada sem depender de catalogo completo ou `.pmc`? -- [ ] Qual evidencia minima prova `Game A -> Home -> Game B` sem depender de UX + - Resposta: para o contrato minimo, o alvo pode ser um path/entrada ja + resolvido pelo Hub/game library atual. O contrato nao depende de `.pmc` nem + de catalogo final. +- [x] Qual evidencia minima prova `Game A -> Home -> Game B` sem depender de UX final de launcher? + - Resposta: criar um cartucho `dummy-boy` ao lado do stress test. O teste deve + provar `Stress -> Home -> Dummy Boy`, com troca de VM session, identidade de + cartridge distinta e render observavel do novo Game. ## Criterio para Encerrar @@ -139,7 +162,17 @@ A agenda pode virar decisao quando: O novo escopo cobre a troca posterior `Game A -> Home -> Game B`, com orquestracao pelo SystemOS depois que o contrato de foreground estiver fechado. +- 2026-07-05: Apos fechamento de `DSC-0041`, a discussao convergiu para troca + destrutiva simples em v1: ao iniciar outro Game, o Game residente anterior e + encerrado, sua VM session e removida, e o novo Game recebe VM session nova. + Falha de load volta ao Home/Hub com diagnostico. A evidencia minima deve + incluir um novo cartucho `dummy-boy` que desenha um quadrado com cores + oscilando, alem do stress test. +- 2026-07-05: Refinamento: o encerramento do Game anterior pode emitir evento + cooperativo para save/flush, mas esse evento nao e uma permissao nem uma + barreira obrigatoria. O SystemOS continua com autoridade para concluir a troca + e remover a VM session anterior. ## Resolution -Ainda em aberto. +Pronta para virar decisao, pendente confirmacao explicita do usuario. diff --git a/discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md b/discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md new file mode 100644 index 00000000..387a9399 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md @@ -0,0 +1,174 @@ +--- +id: DEC-0039 +ticket: system-os-cartridge-switch-orchestrator +title: SystemOS Game Cartridge Switch Orchestrator +status: accepted +created: 2026-07-05 +ref_agenda: AGD-0044 +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Status + +Open for review. + +## Contexto + +`DEC-0037` fechou o contrato `Game -> Home/Shell -> mesmo Game`: o Game pode +ficar residente, ser pausado/suspenso pelo SystemOS, e depois ser retomado. + +`DEC-0038` fechou o ownership de VM sessions: VM-backed mutable execution state +pertence a sessoes de VM associadas a processos/tarefas, nao a uma VM global do +firmware. + +Esta decision cobre outro fluxo: `Game A -> Home -> Game B`. Nesse caso, o +usuario nao esta retomando o mesmo Game. O usuario esta escolhendo substituir o +Game residente por outro cartucho. Essa substituicao precisa ser orquestrada +pelo SystemOS, nao pelo loader e nao por userland. + +## Decisao + +PROMETEU MUST tratar `Game A -> Home -> Game B` como uma substituicao +controlada de Game, nao como pause/resume. + +Quando o usuario escolher iniciar um Game diferente enquanto ja existe um Game +residente, SystemOS MUST encerrar o Game residente anterior, remover sua +resident state e descartar sua VM session antes de ativar o novo Game como +resident/foreground. + +O Game anterior MAY receber um evento cooperativo de encerramento para permitir +save/flush quando esse mecanismo existir. Esse evento MUST NOT ser uma +permissao, veto ou barreira obrigatoria para a troca. SystemOS MUST manter +autoridade para concluir a troca e remover a VM session anterior mesmo se o +Game nao reagir. + +O novo Game MUST receber uma VM session nova e zerada. A troca MUST NOT +reutilizar a VM session, PC, stack, heap, globals, open handles, cartridge +identity, lifecycle state, debug state ou runtime state do Game anterior. + +O loader/materializador de cartridge MUST permanecer uma etapa interna de +carregamento. Ele MUST NOT possuir politica de lifecycle, foreground, +close/discard, cleanup ou recuperacao de falha. + +Se o novo cartucho falhar ao carregar, SystemOS MUST descartar qualquer session +parcial ou estado cartridge-scoped parcial do novo Game e retornar ao Home/Hub +com diagnostico. V1 MUST NOT tentar preservar ou restaurar o Game anterior +depois que a troca tiver comecado. + +Boot direto por CLI/debug permanece um caminho separado e mais simples. Esta +decision governa troca de Game sob SystemOS depois que o usuario retorna ao +Home/Hub. + +A evidencia minima de implementacao MUST incluir dois test cartridges +distintos: o stress cartridge existente e um novo cartridge `dummy-boy`. +`dummy-boy` MUST renderizar um quadrado visualmente distinto com cor oscilante +para que os testes provem que o segundo Game, e nao um frame/session anterior +stale, esta ativo. + +## Rationale + +Trying to preserve Game A while loading Game B would turn the switch into a +transactional snapshot/rollback problem. That is not needed for v1 and would +pull in VM snapshot correctness, open handle restoration, assets, render +ownership rollback, audio rollback, input state, debugger state, and telemetry +rollback. + +The simpler and safer v1 contract is destructive replacement: + +1. the user requests another Game; +2. SystemOS ends the previous resident Game; +3. SystemOS removes its VM session and cartridge-scoped runtime state; +4. SystemOS attempts to load the new Game into a fresh VM session; +5. success enters the new Game; +6. failure returns to Home/Hub with diagnostic. + +This aligns with `DEC-0037` because pause/suspend remains the same-Game resume +flow. It aligns with `DEC-0038` because VM identity is session-owned and each +Game receives a distinct VM session. + +## Invariantes / Contrato + +- SystemOS is the authority for switching from one Game cartridge to another + under Home/Hub. +- `Game -> Home -> same Game` is pause/resume and remains governed by + `DEC-0037`. +- `Game A -> Home -> Game B` is destructive replacement and is governed by this + decision. +- A Game that is being replaced MUST stop being resident/resumable. +- The previous Game VM session MUST be removed before the new Game becomes the + active resident/foreground Game. +- A cooperative close/terminate/save event MAY be delivered to the previous + Game, but it MUST NOT block replacement. +- The new Game MUST get a fresh VM session. +- The new Game MUST NOT inherit VM execution state, open handles, lifecycle + delivery state, cartridge identity, debug state, telemetry state, or crash + state from the previous Game. +- Render ownership MUST transition through SystemOS ownership policy and stale + submissions from the previous Game MUST NOT be presented after the switch. +- Input pending/barrier state MUST be cleared or made safe across the switch. +- App audio from the previous Game MUST be stopped or detached from the active + app before the new Game becomes foreground. +- Logs MAY remain as historical observability; they are not cartridge execution + state. +- If new Game load fails, Home/Hub is the recovery state for v1. +- V1 does not preserve, restore, or rollback to the previous Game after a + switch begins. +- The launcher target MAY be a path or already-resolved game-library entry for + v1. `.pmc` and final catalog UX are out of scope. +- `run_cart`, if retained, is not a userland command and not the switch + orchestrator. + +## Impactos + +- **Runtime / SystemOS:** needs an explicit Game switch orchestration path that + closes/removes the current resident Game session before loading the next Game. +- **Lifecycle:** needs a controlled replacement transition distinct from + pause/suspend/resume. +- **VM sessions:** previous Game session removal and new Game session creation + are mandatory switch steps. +- **Firmware:** should observe macro states such as Home, loading, load failure, + and GameRunning, but should not own switch policy. +- **Hub / launcher:** should request a switch through SystemOS using a resolved + target; it should not call a loader directly as lifecycle authority. +- **Loader:** remains a materialization step and returns success/failure without + owning cleanup policy. +- **Renderer:** render ownership/stale submission invalidation must prove the + new Game owns current presentation after success. +- **Input/audio:** previous Game input/audio must not leak into the new Game. +- **Tests:** need a second visually distinct test cartridge, `dummy-boy`, to + prove Game identity and render transition. + +## Alternativas Descartadas + +- **Preserve Game A until Game B loads successfully:** rejected for v1 because it + requires rollback semantics across VM, assets, handles, render ownership, + input, audio, debugger, and telemetry. +- **Treat Game A -> Game B as pause/resume:** rejected because a different Game + should not inherit resumable state from the previous Game. +- **Let the loader close the previous Game:** rejected because loader/materializer + should not own lifecycle policy. +- **Reuse the previous VM session for the new Game:** rejected because it + violates VM session ownership and risks state leakage. +- **Depend on `.pmc` or final catalog UX now:** rejected because the lifecycle + contract can be validated using path/game-library targets first. + +## Referencias + +- Source agenda: `AGD-0044` +- Foreground stack and Game pause contract: `DEC-0037` +- VM session ownership for VM-backed processes: `DEC-0038` +- SystemOS lifecycle/process/task authority: `DSC-0032` +- Launch authority lesson: `LSN-0051` +- Foreground/session ownership lesson: `LSN-0052` + +## Propagacao Necessaria + +- Add/update runtime spec for Game cartridge switching under SystemOS. +- Add implementation plans for: + - SystemOS switch orchestration; + - resident Game termination/session removal; + - fresh session load for the new Game; + - failure recovery to Home/Hub; + - `dummy-boy` test cartridge; + - end-to-end `Stress -> Home -> Dummy Boy` validation. +- Ensure host/debug direct boot remains separate from SystemOS Game switch. diff --git a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md new file mode 100644 index 00000000..dc71d06a --- /dev/null +++ b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md @@ -0,0 +1,86 @@ +--- +id: PLN-0157 +ticket: system-os-cartridge-switch-orchestrator +title: Introduce SystemOS Game Switch Target Contract +status: open +created: 2026-07-05 +ref_decisions: [DEC-0039] +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Briefing + +DEC-0039 requires Game-to-Game switching under Home/Hub to be owned by SystemOS. +Before destructive replacement can be implemented, Hub/Firmware/SystemOS need a +small explicit contract for "launch this resolved Game target as a replacement" +that is separate from direct boot and from loader internals. + +## Objective + +Introduce a SystemOS-owned Game switch request/target abstraction that can carry +a resolved cartridge path or game-library entry from Hub/Home into the switch +orchestrator without making the loader or userland the lifecycle authority. + +## Dependencies + +- Source decision: DEC-0039. +- Builds on DEC-0037 and DEC-0038 as already implemented. +- Must be implemented before PLN-0158, PLN-0159, and PLN-0161. + +## Scope + +- Define a minimal Game switch target type, such as `GameSwitchTarget` or + `CartridgeLaunchTarget`, in the SystemOS/Firmware boundary. +- Route Hub game-library launch actions through that target contract instead of + treating a path as an implicit lifecycle operation. +- Keep direct boot/debug boot paths separate from SystemOS Game switching. +- Add tests proving the target preserves cartridge identity/path without + starting lifecycle replacement by itself. + +## Non-Goals + +- Do not implement destructive replacement in this plan. +- Do not add `.pmc`, final catalog UX, or a new launcher UI. +- Do not change direct CLI/debug boot behavior. +- Do not add background execution. + +## Execution Method + +1. Inspect `crates/console/prometeu-system/src/services/game_library.rs`, + `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`, + and `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`. +2. Add a minimal launch target model at the SystemOS/Firmware boundary: + - resolved cartridge path; + - optional app_id/title metadata if already available; + - no loader-owned lifecycle policy. +3. Update Hub action plumbing so `SystemProfileAction::LaunchGame` carries the + resolved target type or maps immediately into it at the firmware boundary. +4. Add a placeholder SystemOS/Firmware entry point for Game switching that + accepts the target but still delegates to the existing load path until + PLN-0158 changes replacement behavior. +5. Add unit tests for target construction and path propagation. +6. Keep direct `BootTarget::Cartridge` behavior unchanged. + +## Acceptance Criteria + +- There is an explicit type or function contract representing a resolved + SystemOS Game switch target. +- Hub/Home no longer treats a raw path as lifecycle authority. +- The loader remains a materialization step, not the switch orchestrator. +- Direct boot/debug boot code paths remain separate and behaviorally unchanged. +- Tests prove target identity is carried without implicitly closing or loading a + Game. + +## Tests + +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-host-desktop-winit` +- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings` + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/game_library.rs` +- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md b/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md new file mode 100644 index 00000000..d2176204 --- /dev/null +++ b/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md @@ -0,0 +1,98 @@ +--- +id: PLN-0158 +ticket: system-os-cartridge-switch-orchestrator +title: Terminate Resident Game Before Switching Cartridges +status: open +created: 2026-07-05 +ref_decisions: [DEC-0039] +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Briefing + +DEC-0039 defines `Game A -> Home -> Game B` as destructive replacement. The +current same-Game resume flow keeps the resident Game session alive; switching +to a different Game must instead close/remove that resident session and create a +fresh session for the new Game. + +## Objective + +Implement the SystemOS lifecycle/session operation that terminates the current +resident Game before loading a different Game cartridge. + +## Dependencies + +- Source decision: DEC-0039. +- Requires PLN-0157 target contract. +- Should be implemented before PLN-0159 and PLN-0161. + +## Scope + +- Add a controlled resident Game termination/replacement operation to SystemOS + lifecycle/session services. +- Optionally emit a cooperative terminate/close/save lifecycle event when the + current event model can carry it, but do not block replacement on it. +- Remove the previous Game from resident/resumable state. +- Remove the previous Game VM session before activating the new Game. +- Clear or reassign app-scoped state that cannot leak into the new Game: + session VM, open handles, pending lifecycle state, input barrier/pending + input, audio active app state, render ownership, telemetry/debug/crash state. + +## Non-Goals + +- Do not preserve Game A for rollback. +- Do not implement failed-load recovery beyond surfacing errors; that is + PLN-0159. +- Do not add background execution or multiple resident Games. +- Do not build final launcher UX. + +## Execution Method + +1. Inspect lifecycle/session state in: + - `crates/console/prometeu-system/src/os/lifecycle.rs`; + - `crates/console/prometeu-system/src/os/facades/sessions.rs`; + - `crates/console/prometeu-system/src/services/foreground`; + - `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`; + - `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`. +2. Add a `replace_resident_game` or equivalent operation that: + - finds the current resident Game; + - marks its task/process closed or stopped according to existing lifecycle + semantics; + - removes the resident Game entry from foreground stack/lifecycle state; + - removes its VM session. +3. Ensure same-Game resume still bypasses replacement and remains governed by + DEC-0037. +4. Update game launch from Home so launching a different app_id triggers + replacement before load. +5. Ensure render ownership transitions away from the old Game before the new + Game becomes foreground. +6. Add focused tests for resident Game termination and session removal. + +## Acceptance Criteria + +- Launching a different Game from Home closes/removes the resident Game rather + than resuming it. +- The previous Game VM session is absent before the new Game is made active. +- Same-Game selection still resumes the existing resident Game. +- A cooperative close/terminate event, if added, is non-blocking. +- No previous Game VM, handle, lifecycle, render, or debug state is reused by + the new Game. + +## Tests + +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-firmware` +- `cargo clippy -p prometeu-system -p prometeu-firmware --all-targets -- -D warnings` +- Add tests for: + - different Game launch removes previous resident session; + - same Game launch resumes existing resident session; + - old Game task/process state is closed/stopped; + - old Game session cannot be resolved after replacement starts. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/lifecycle.rs` +- `crates/console/prometeu-system/src/os/facades/sessions.rs` +- `crates/console/prometeu-system/src/services/foreground` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` diff --git a/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md b/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md new file mode 100644 index 00000000..06dba643 --- /dev/null +++ b/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md @@ -0,0 +1,86 @@ +--- +id: PLN-0159 +ticket: system-os-cartridge-switch-orchestrator +title: Recover Failed Game Switches Back To Home +status: open +created: 2026-07-05 +ref_decisions: [DEC-0039] +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Briefing + +DEC-0039 rejects preserving Game A after a switch begins. If Game B fails to +load, the system must discard partial Game B state and return to Home/Hub with a +diagnostic, not attempt rollback to Game A. + +## Objective + +Implement controlled failed-switch recovery to Home/Hub after destructive Game +replacement begins. + +## Dependencies + +- Source decision: DEC-0039. +- Requires PLN-0157 target contract. +- Requires PLN-0158 resident Game termination/session removal. + +## Scope + +- Detect load/init failure while switching to a different Game. +- Discard any partial new Game task/session/window/render/input/audio state. +- Return firmware/SystemOS to Home/Hub. +- Log or expose a diagnostic that Home can surface or tests can assert. +- Ensure previous Game is not restored after switch failure. + +## Non-Goals + +- Do not implement transactional rollback. +- Do not preserve the previous Game after switch begins. +- Do not design final user-facing error UI. +- Do not change direct boot failure behavior unless needed to keep paths + separate. + +## Execution Method + +1. Inspect existing load failure flow in + `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`. +2. Add a switch-specific failure outcome or context flag that distinguishes + SystemOS Game switching from direct boot load. +3. On switch load failure: + - remove partial new VM session/task/process if created; + - clear partial cartridge-scoped state; + - transition to `HubHome` instead of app crash when appropriate for switch + recovery; + - log a diagnostic containing target identity/path and failure summary. +4. Preserve existing direct boot and explicit load failure tests unless a direct + boot path intentionally still goes to crash. +5. Add tests for bad Game B after Game A was already terminated. + +## Acceptance Criteria + +- Failed `Game A -> Home -> Broken Game B` returns to Home/Hub. +- Previous Game A is not resident, resumable, or restored. +- Partial Game B VM session/task/process state is removed or closed. +- A diagnostic is logged or otherwise observable. +- Existing direct boot/debug boot failure semantics remain separate. + +## Tests + +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-host-desktop-winit` +- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings` +- Add tests for: + - switch failure returns Home; + - old Game not restored; + - partial new session cleanup; + - direct boot failure path remains unchanged. + +## Affected Artifacts + +- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-system/src/os/facades/sessions.rs` +- `crates/console/prometeu-system/src/os/lifecycle.rs` diff --git a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md new file mode 100644 index 00000000..34ad7d49 --- /dev/null +++ b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md @@ -0,0 +1,90 @@ +--- +id: PLN-0160 +ticket: system-os-cartridge-switch-orchestrator +title: Add Dummy Boy Visual Switch Test Cartridge +status: open +created: 2026-07-05 +ref_decisions: [DEC-0039] +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Briefing + +DEC-0039 requires minimum evidence using two distinct test cartridges. The +existing stress cartridge proves execution/logging but is not visually distinct +enough for switch validation. A second cartridge, `dummy-boy`, must render an +oscillating-color square. + +## Objective + +Add a simple `test-cartridges/dummy-boy` cartridge that renders a visible square +with changing colors so Game switching can prove visual/session identity. + +## Dependencies + +- Source decision: DEC-0039. +- Can be implemented independently of PLN-0157 through PLN-0159. +- Required before PLN-0161 end-to-end validation. + +## Scope + +- Add `test-cartridges/dummy-boy/manifest.json`. +- Generate or add `test-cartridges/dummy-boy/program.pbx`. +- Add empty or minimal assets payload if the loader requires `assets.pa`. +- Implement Dummy Boy behavior: + - Game cartridge identity distinct from stress; + - deterministic visible square; + - color oscillates across frames; + - no dependency on final launcher UX. +- Add build/generation documentation or tests so the cartridge can be + regenerated safely if needed. + +## Non-Goals + +- Do not add final game content. +- Do not add `.pmc`. +- Do not depend on host screenshots for the first validation unless an existing + harness already supports it. +- Do not change stress cartridge behavior. + +## Execution Method + +1. Inspect the stress cartridge and any PBX generator tooling: + - `test-cartridges/stress-console`; + - `crates/tools/pbxgen-stress`; + - bytecode assembler helpers in `crates/console/prometeu-bytecode`. +2. Choose the smallest maintainable generator path: + - extend existing PBX generator if appropriate; or + - add a focused `pbxgen-dummy-boy` tool; or + - add deterministic generated PBX with source instructions. +3. Create `manifest.json` with unique `app_id`, title `Dummy Boy`, Game mode, + and version. +4. Generate `program.pbx` that renders the square and advances color over + frames. +5. Add tests or smoke checks that the cartridge is discoverable by games-root + discovery. + +## Acceptance Criteria + +- `test-cartridges/dummy-boy` exists and loads as a Game cartridge. +- Dummy Boy has a unique `app_id` and title distinct from Stress. +- Dummy Boy renders a visible square. +- Square color changes deterministically over time. +- Games-root discovery sees both Stress and Dummy Boy. +- Existing stress cartridge remains unchanged. + +## Tests + +- `cargo test -p prometeu-hal` +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-firmware` +- Run any cartridge generator tests added by this plan. +- Add or update tests for games-root discovery including Dummy Boy. + +## Affected Artifacts + +- `test-cartridges/dummy-boy/manifest.json` +- `test-cartridges/dummy-boy/program.pbx` +- `test-cartridges/dummy-boy/assets.pa` if required +- `crates/tools/*` cartridge generator tooling if needed +- `crates/console/prometeu-system/src/services/game_library.rs` tests if needed diff --git a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md new file mode 100644 index 00000000..6889679e --- /dev/null +++ b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md @@ -0,0 +1,93 @@ +--- +id: PLN-0161 +ticket: system-os-cartridge-switch-orchestrator +title: Validate Stress To Dummy Boy Game Switch End To End +status: open +created: 2026-07-05 +ref_decisions: [DEC-0039] +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Briefing + +DEC-0039 requires proof that `Game A -> Home -> Game B` creates a fresh Game +session and does not present stale state from Game A. The minimum evidence is +switching from Stress to Dummy Boy. + +## Objective + +Add end-to-end validation for `Stress -> Home -> Dummy Boy` through the real +Hub/Home flow, proving lifecycle, VM session, cartridge identity, and render +ownership switch correctly. + +## Dependencies + +- Source decision: DEC-0039. +- Requires PLN-0157 target contract. +- Requires PLN-0158 destructive replacement. +- Requires PLN-0159 failed-switch recovery for negative path assertions. +- Requires PLN-0160 Dummy Boy cartridge. + +## Scope + +- Add firmware/system tests that drive: + - launch Stress from games root; + - request Home; + - launch Dummy Boy; + - assert Stress session is gone; + - assert Dummy Boy session is active; + - assert cartridge identity and render ownership belong to Dummy Boy. +- Add a negative end-to-end test for broken Game B returning to Home if not + already covered by PLN-0159. +- Use existing test platform/render submission inspection where possible. + +## Non-Goals + +- Do not require final launcher UX. +- Do not add screenshot-based host automation unless existing test utilities + make it cheap and deterministic. +- Do not implement additional Game-to-Game variants beyond Stress to Dummy Boy. + +## Execution Method + +1. Inspect existing firmware tests in + `crates/console/prometeu-firmware/src/firmware/firmware.rs`. +2. Reuse existing `TestGameRoot` or games-root helpers to expose Stress and + Dummy Boy. +3. Drive input through Hub/Home selection rather than calling cartridge load + directly when possible. +4. Assert lifecycle/session invariants: + - old task closed/stopped; + - old VM session removed; + - new VM session created; + - foreground owner is new Game; + - current cartridge identity is Dummy Boy. +5. Assert render invariants: + - render ownership epoch belongs to Dummy Boy app identity; + - stale Stress submissions are not accepted after switch; + - latest observable Game frame comes from Dummy Boy when render inspection is + available. +6. Add a focused test for failure path if missing after PLN-0159. + +## Acceptance Criteria + +- End-to-end test proves Stress can be replaced by Dummy Boy from Home. +- Stress is not resumable after switching to Dummy Boy. +- Dummy Boy receives a fresh VM session. +- Cartridge identity and active render ownership are Dummy Boy after switch. +- Failure path returns Home and does not restore Stress. +- Tests are deterministic and do not rely on manual host interaction. + +## Tests + +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-host-desktop-winit` +- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings` + +## Affected Artifacts + +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-system/src/services/game_library.rs` +- `test-cartridges/dummy-boy` +- Host/render test utilities if needed diff --git a/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md b/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md new file mode 100644 index 00000000..b4d85d48 --- /dev/null +++ b/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md @@ -0,0 +1,93 @@ +--- +id: PLN-0162 +ticket: system-os-cartridge-switch-orchestrator +title: Specify Game Switch Contract And Direct Boot Separation +status: open +created: 2026-07-05 +ref_decisions: [DEC-0039] +tags: [runtime, os, lifecycle, game, cartridge, architecture] +--- + +## Briefing + +DEC-0039 must be reflected in the canonical runtime specs after the operational +contract is implemented or alongside implementation. The spec must distinguish +same-Game resume, Game replacement under SystemOS, and direct boot/debug boot. + +## Objective + +Update the canonical runtime documentation to describe SystemOS Game cartridge +switching and to keep direct boot/debug boot separate from Home/Hub-driven Game +replacement. + +## Dependencies + +- Source decision: DEC-0039. +- Can be implemented before code if the spec is written as intended behavior. +- Should be reconciled after PLN-0157 through PLN-0161 if implementation names + differ from initial plan names. + +## Scope + +- Update relevant files under `docs/specs/runtime/`, especially: + - `12-firmware-pos-and-prometeuhub.md`; + - `13-cartridge.md`; + - `14-boot-profiles.md`; + - `09-events-and-concurrency.md` if lifecycle events need mention; + - `10-debug-inspection-and-profiling.md` if debugger separation needs note. +- Document: + - SystemOS authority for Game switching; + - destructive replacement semantics; + - optional non-blocking terminate/save event; + - fresh VM session for new Game; + - failed switch recovery to Home/Hub; + - direct boot/debug boot separation; + - test cartridge evidence requirement. + +## Non-Goals + +- Do not document `.pmc` or final catalog UX as required. +- Do not specify background execution. +- Do not revise DEC-0037 or DEC-0038 semantics. +- Do not write lesson material; housekeeping will decide lessons after + execution. + +## Execution Method + +1. Read the runtime spec files listed in scope. +2. Add a Game switching section in the most appropriate canonical spec file. +3. Cross-reference existing Home/Hub, cartridge, boot profile, and debug boot + sections. +4. Ensure language is English and normative enough for future implementers: + `MUST`, `MUST NOT`, `MAY`, `SHOULD`. +5. Verify no non-English agenda language leaks into specs. +6. Run docs grep checks for the newly introduced terms. + +## Acceptance Criteria + +- Specs explicitly distinguish: + - same-Game pause/resume; + - Game A to Game B destructive replacement; + - direct boot/debug boot. +- Specs state that SystemOS owns Game switching policy. +- Specs state that a new Game gets a fresh VM session. +- Specs state that failed switch returns Home/Hub and does not restore the old + Game in v1. +- Specs mention Dummy Boy or equivalent two-cartridge evidence only as test + evidence, not as runtime feature. +- Documentation is English. + +## Tests + +- `rg -n "Game switch|destructive replacement|direct boot|Dummy Boy|VM session" docs/specs/runtime` +- `discussion validate` +- Run code tests only if this plan is implemented together with code changes: + `cargo test -p prometeu-system -p prometeu-firmware` + +## Affected Artifacts + +- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` +- `docs/specs/runtime/13-cartridge.md` +- `docs/specs/runtime/14-boot-profiles.md` +- `docs/specs/runtime/09-events-and-concurrency.md` +- `docs/specs/runtime/10-debug-inspection-and-profiling.md` -- 2.47.2 From dcd3479b1d2a17d9a5d8b049f7ab45b0badd4441 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:07:54 +0100 Subject: [PATCH 02/13] implements PLN-0157 --- .../src/firmware/firmware_step_hub_home.rs | 64 +++++++++++-------- crates/console/prometeu-system/src/lib.rs | 2 +- .../src/programs/prometeu_hub/prometeu_hub.rs | 22 +++++-- .../src/services/game_library.rs | 48 ++++++++++++++ discussion/index.ndjson | 2 +- ...ce-systemos-game-switch-target-contract.md | 2 +- 6 files changed, 103 insertions(+), 37 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index f00f3966..d0c7c293 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -52,45 +52,53 @@ impl HubHomeStep { ctx.os.windows().set_focus(id); return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id))); } - Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { - Ok(cartridge) => { - if cartridge.app_mode == AppMode::Game - && let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task() - { - let resident_app_id = ctx - .os - .sessions() - .vm_session_for_task(resident_task_id) - .map(|session| session.app_id) - .unwrap_or(0); + Some(SystemProfileAction::LaunchGame { target }) => { + match CartridgeLoader::load(&target.path) { + Ok(cartridge) => { + if cartridge.app_mode == AppMode::Game + && let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task() + { + let resident_app_id = ctx + .os + .sessions() + .vm_session_for_task(resident_task_id) + .map(|session| session.app_id) + .unwrap_or(0); - if resident_app_id == cartridge.app_id { - return Some(FirmwareState::ResumeResidentGame); + if resident_app_id == cartridge.app_id { + return Some(FirmwareState::ResumeResidentGame); + } + + ctx.os.log( + LogLevel::Warn, + LogSource::Hub, + 0, + format!( + "Cannot launch Home game {} while resident game app_id {} is active", + cartridge.title, resident_app_id + ), + ); + return None; } + return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new( + cartridge, + ))); + } + Err(error) => { ctx.os.log( - LogLevel::Warn, + LogLevel::Error, LogSource::Hub, 0, format!( - "Cannot launch Home game {} while resident game app_id {} is active", - cartridge.title, resident_app_id + "Failed to launch Home game {}: {:?}", + target.path.display(), + error ), ); - return None; } - - return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge))); } - Err(error) => { - ctx.os.log( - LogLevel::Error, - LogSource::Hub, - 0, - format!("Failed to launch Home game {}: {:?}", path.display(), error), - ); - } - }, + } Some(SystemProfileAction::CloseShell) | None => {} } diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 20be73b7..4ef78157 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -20,7 +20,7 @@ pub use services::foreground::{ }; pub use services::fs; pub use services::game_library::{ - GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root, + GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, GameSwitchTarget, discover_games_root, }; pub use services::memcard::MemcardAsyncLaneOperation; pub use services::process; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 28038739..b2f2ce26 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -1,12 +1,11 @@ use crate::task::TaskId; -use crate::{CrashReport, GameLibrary, SystemOS}; +use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform, ShellUiFramePacket, }; -use std::path::PathBuf; const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 }; const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 172, w: 112, h: 32 }; @@ -83,7 +82,7 @@ impl NativeShellApp { #[derive(Debug, Clone, PartialEq, Eq)] pub enum SystemProfileAction { LaunchNativeShell(NativeShellApp), - LaunchGame { path: PathBuf }, + LaunchGame { target: GameSwitchTarget }, CloseShell, } @@ -249,7 +248,7 @@ fn action_for_click( .find(|(index, _entry)| rect_contains(game_row_rect(*index), x, y)) .map(|(_index, entry)| entry) { - return Some(SystemProfileAction::LaunchGame { path: entry.path.clone() }); + return Some(SystemProfileAction::LaunchGame { target: entry.switch_target() }); } HOME_BUTTONS @@ -434,6 +433,7 @@ mod tests { use crate::windows::WindowOwner; use prometeu_drivers::TestPlatform; use prometeu_hal::{InputSignals, RuntimePlatform}; + use std::path::PathBuf; fn focused_native_shell_fixture( signals: &InputSignals, @@ -535,9 +535,19 @@ mod tests { discovered_at: std::time::SystemTime::UNIX_EPOCH, }; + let action = + action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)); + assert_eq!( - action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)), - Some(SystemProfileAction::LaunchGame { path: entry.path.clone() }) + action, + Some(SystemProfileAction::LaunchGame { + target: GameSwitchTarget { + path: entry.path.clone(), + app_id: 42, + title: "Stress".to_string(), + app_version: "1.0.0".to_string(), + } + }) ); } diff --git a/crates/console/prometeu-system/src/services/game_library.rs b/crates/console/prometeu-system/src/services/game_library.rs index bbf9728c..9d357203 100644 --- a/crates/console/prometeu-system/src/services/game_library.rs +++ b/crates/console/prometeu-system/src/services/game_library.rs @@ -25,6 +25,29 @@ impl GameLibraryEntry { discovered_at, } } + + pub fn switch_target(&self) -> GameSwitchTarget { + GameSwitchTarget::from_entry(self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GameSwitchTarget { + pub path: PathBuf, + pub app_id: u32, + pub title: String, + pub app_version: String, +} + +impl GameSwitchTarget { + pub fn from_entry(entry: &GameLibraryEntry) -> Self { + Self { + path: entry.path.clone(), + app_id: entry.app_id, + title: entry.title.clone(), + app_version: entry.app_version.clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -256,6 +279,31 @@ mod tests { })); } + #[test] + fn switch_target_preserves_resolved_identity() { + let path = PathBuf::from("test-cartridges/stress-console"); + let entry = GameLibraryEntry::new( + path.clone(), + CartridgeManifest { + magic: "PMTU".to_string(), + cartridge_version: 1, + app_id: 42, + title: "Stress".to_string(), + app_version: "1.0.0".to_string(), + app_mode: AppMode::Game, + capabilities: vec![], + }, + UNIX_EPOCH, + ); + + let target = entry.switch_target(); + + assert_eq!(target.path, path); + assert_eq!(target.app_id, 42); + assert_eq!(target.title, "Stress"); + assert_eq!(target.app_version, "1.0.0"); + } + #[test] fn missing_root_returns_diagnostic_and_no_entries() { let root = TestRoot::new(); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 9c08136e..8902af04 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md index dc71d06a..b8f88cca 100644 --- a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md +++ b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md @@ -2,7 +2,7 @@ id: PLN-0157 ticket: system-os-cartridge-switch-orchestrator title: Introduce SystemOS Game Switch Target Contract -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] -- 2.47.2 From b30ff73e3d33cfa600310f9d10f3a1289e214c4a Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:12:09 +0100 Subject: [PATCH 03/13] implements PLN-0158 --- .../src/firmware/firmware.rs | 95 +++++++++++++++++-- .../src/firmware/firmware_step_hub_home.rs | 22 +++-- .../src/os/facades/lifecycle.rs | 22 +++++ .../prometeu-system/src/os/system_os.rs | 36 +++++++ .../src/services/vm_session.rs | 29 ++++++ discussion/index.ndjson | 2 +- ...sident-game-before-switching-cartridges.md | 2 +- 7 files changed, 188 insertions(+), 20 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 153fb92f..81e3e8f2 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -289,16 +289,29 @@ mod tests { } fn add_game(&self, name: &str, with_program: bool) -> PathBuf { + self.add_game_with_identity(name, 42, "Stress", "1.0.0", with_program) + } + + fn add_game_with_identity( + &self, + name: &str, + app_id: u32, + title: &str, + app_version: &str, + with_program: bool, + ) -> PathBuf { let cart = self.path.join(name); fs::create_dir_all(&cart).expect("test cartridge should be created"); - let manifest = r#"{ - "magic": "PMTU", - "cartridge_version": 1, - "app_id": 42, - "title": "Stress", - "app_version": "1.0.0", - "app_mode": "Game" - }"#; + let manifest = format!( + r#"{{ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": {app_id}, + "title": "{title}", + "app_version": "{app_version}", + "app_mode": "Game" + }}"# + ); fs::write(cart.join("manifest.json"), manifest) .expect("test manifest should be written"); if with_program { @@ -728,6 +741,72 @@ mod tests { ); } + #[test] + fn hub_home_clicking_different_game_terminates_previous_resident_session() { + let root = TestGameRoot::new(); + root.add_game("stress", true); + root.add_game_with_identity("zzz-dummy", 43, "Zzz Dummy", "1.0.0", true); + + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.hub.set_game_library(discover_games_root(&root.path)); + firmware.tick(&InputSignals::default(), &mut platform); + + let stress_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&stress_click, &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + let first_game_task = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected first GameRunning state, got {:?}", other), + }; + + firmware.request_home_from_host(); + firmware.tick(&InputSignals::default(), &mut platform); + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(first_game_task)); + + firmware.tick(&InputSignals::default(), &mut platform); + let dummy_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; + firmware.tick(&dummy_click, &mut platform); + + assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(first_game_task), + Ok(ProcessState::Stopped) + ); + assert_eq!( + firmware + .os + .sessions() + .vm_session_for_task(first_game_task) + .map(|session| session.task_id), + None + ); + + let second_game_task = firmware + .os + .lifecycle() + .resident_game_task() + .expect("replacement game should be resident"); + assert_ne!(second_game_task, first_game_task); + assert_eq!( + firmware + .os + .sessions() + .vm_session_for_task(second_game_task) + .map(|session| session.app_id), + Some(43) + ); + + firmware.tick(&InputSignals::default(), &mut platform); + + match &firmware.state { + FirmwareState::GameRunning(step) => assert_eq!(step.task_id, second_game_task), + other => panic!("expected replacement GameRunning state, got {:?}", other), + } + } + #[test] fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() { let root = TestGameRoot::new(); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index d0c7c293..93c59dd6 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -69,16 +69,18 @@ impl HubHomeStep { return Some(FirmwareState::ResumeResidentGame); } - ctx.os.log( - LogLevel::Warn, - LogSource::Hub, - 0, - format!( - "Cannot launch Home game {} while resident game app_id {} is active", - cartridge.title, resident_app_id - ), - ); - return None; + if let Err(error) = ctx.os.lifecycle().terminate_resident_game() { + ctx.os.log( + LogLevel::Error, + LogSource::Hub, + 0, + format!( + "Failed to terminate resident game app_id {} before launching {}: {:?}", + resident_app_id, cartridge.title, error + ), + ); + return None; + } } return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new( diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index 680a1cc5..b7f6d06a 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -222,6 +222,28 @@ impl<'a> LifecycleFacade<'a> { Ok(()) } + pub fn terminate_resident_game(&mut self) -> Result, LifecycleError> { + let Some(task_id) = self.os.foreground_stack.resident_game_task() else { + return Ok(None); + }; + + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.task_manager.close_task(task_id); + self.os.process_manager.mark_stopped(process_id); + self.os.foreground_stack.clear_resident_game(task_id); + self.os.vm_sessions.remove_for_task(task_id); + self.os.game_lifecycle_events.retain(|event| event.task_id != task_id); + + if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Game(owner) if owner == task_id) + { + self.os.foreground_stack.return_to_hub(); + self.os.task_manager.clear_foreground(); + } + + Ok(Some(task_id)) + } + pub fn crash_task( &mut self, task_id: TaskId, diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index a39cb2d3..23d6c048 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -218,6 +218,42 @@ mod tests { ); } + #[test] + fn terminate_resident_game_closes_task_and_removes_vm_session() { + let mut os = SystemOS::new(None); + let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + os.lifecycle().request_home_from_game(game).expect("home request should succeed"); + assert_eq!(os.lifecycle().pending_game_lifecycle_events().len(), 1); + + assert_eq!(os.lifecycle().terminate_resident_game(), Ok(Some(game))); + + assert_eq!(os.lifecycle().resident_game(), None); + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub); + assert_eq!(os.lifecycle().task_state(game), Some(TaskState::Closed)); + assert_eq!(process_state_for_task(&os, game), ProcessState::Stopped); + assert_eq!(os.sessions().vm_session_for_task(game).map(|session| session.task_id), None); + assert!(os.lifecycle().pending_game_lifecycle_events().is_empty()); + } + + #[test] + fn terminate_resident_game_preserves_foreground_shell() { + let mut os = SystemOS::new(None); + let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); + os.lifecycle().request_home_from_game(game).expect("home request should succeed"); + os.lifecycle().advance_game_pause_budget(game).expect("budget should expire"); + let shell = os.sessions().create_vm_shell_task(7, "Settings"); + + assert_eq!(os.lifecycle().terminate_resident_game(), Ok(Some(game))); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); + assert_eq!(os.lifecycle().resident_game(), None); + assert_eq!(os.lifecycle().task_state(game), Some(TaskState::Closed)); + assert_eq!(process_state_for_task(&os, game), ProcessState::Stopped); + assert_eq!(os.sessions().vm_session_for_task(game).map(|session| session.task_id), None); + assert_eq!(os.lifecycle().task_state(shell), Some(TaskState::Foreground)); + } + #[test] fn vm_game_task_creates_vm_session() { let mut os = SystemOS::new(None); diff --git a/crates/console/prometeu-system/src/services/vm_session.rs b/crates/console/prometeu-system/src/services/vm_session.rs index caa023f3..ffd1dbbe 100644 --- a/crates/console/prometeu-system/src/services/vm_session.rs +++ b/crates/console/prometeu-system/src/services/vm_session.rs @@ -121,6 +121,10 @@ impl VmSessionRegistry { self.get_mut(VmSessionId(task_id)) } + pub fn remove_for_task(&mut self, task_id: TaskId) -> Option { + self.sessions.remove(&VmSessionId(task_id)) + } + pub fn resident_game(&self) -> Option<&VmSession> { self.sessions.values().find(|session| session.app_mode == AppMode::Game) } @@ -231,6 +235,31 @@ mod tests { assert_eq!(registry.resident_game_for_app(43).map(|session| session.task_id), None); } + #[test] + fn remove_for_task_removes_only_matching_session() { + let game_process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame); + let game_task = Task::new(TaskId(7), game_process.id, 42, "Stress", TaskKind::Game); + let shell_process = Process::new(ProcessId(2), 9, "Shell", ProcessKind::VmShell); + let shell_task = Task::new(TaskId(8), shell_process.id, 9, "Shell", TaskKind::Shell); + let mut registry = VmSessionRegistry::new(); + + registry + .create_for_task(&game_task, &game_process, "", None, Arc::new(AtomicU32::new(0))) + .expect("game session should create"); + registry + .create_for_task(&shell_task, &shell_process, "", None, Arc::new(AtomicU32::new(0))) + .expect("shell session should create"); + + let removed = registry.remove_for_task(game_task.id).expect("game session should remove"); + + assert_eq!(removed.task_id, game_task.id); + assert_eq!(registry.for_task(game_task.id).map(|session| session.task_id), None); + assert_eq!( + registry.for_task(shell_task.id).map(|session| session.task_id), + Some(shell_task.id) + ); + } + #[test] fn session_runtime_state_is_independent_per_session() { let game_process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8902af04..204218d2 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md b/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md index d2176204..ae4c957f 100644 --- a/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md +++ b/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md @@ -2,7 +2,7 @@ id: PLN-0158 ticket: system-os-cartridge-switch-orchestrator title: Terminate Resident Game Before Switching Cartridges -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] -- 2.47.2 From 3df8227ebf7470d5b39851e0db7ff166ce20d5ec Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:14:45 +0100 Subject: [PATCH 04/13] implements PLN-0159 --- .../src/firmware/firmware.rs | 83 ++++++++++++++++++- .../src/firmware/firmware_step_hub_home.rs | 6 +- .../firmware/firmware_step_load_cartridge.rs | 49 ++++++++++- discussion/index.ndjson | 2 +- ...cover-failed-game-switches-back-to-home.md | 2 +- 5 files changed, 133 insertions(+), 9 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 81e3e8f2..bfeaac3e 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -299,6 +299,18 @@ mod tests { title: &str, app_version: &str, with_program: bool, + ) -> PathBuf { + let program = with_program.then(halting_program); + self.add_game_with_program(name, app_id, title, app_version, program) + } + + fn add_game_with_program( + &self, + name: &str, + app_id: u32, + title: &str, + app_version: &str, + program: Option>, ) -> PathBuf { let cart = self.path.join(name); fs::create_dir_all(&cart).expect("test cartridge should be created"); @@ -314,8 +326,8 @@ mod tests { ); fs::write(cart.join("manifest.json"), manifest) .expect("test manifest should be written"); - if with_program { - fs::write(cart.join("program.pbx"), halting_program()) + if let Some(program) = program { + fs::write(cart.join("program.pbx"), program) .expect("test program should be written"); } cart @@ -807,6 +819,73 @@ mod tests { } } + #[test] + fn failed_game_switch_returns_home_without_restoring_previous_game() { + let root = TestGameRoot::new(); + root.add_game("stress", true); + root.add_game_with_program("zzz-broken", 43, "Zzz Broken", "1.0.0", Some(vec![0, 0, 0, 0])); + + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.hub.set_game_library(discover_games_root(&root.path)); + firmware.tick(&InputSignals::default(), &mut platform); + + let stress_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&stress_click, &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + let first_game_task = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected first GameRunning state, got {:?}", other), + }; + + firmware.request_home_from_host(); + firmware.tick(&InputSignals::default(), &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + let broken_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; + firmware.tick(&broken_click, &mut platform); + + assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed)); + assert_eq!( + firmware + .os + .sessions() + .vm_session_for_task(first_game_task) + .map(|session| session.task_id), + None + ); + let partial_task = firmware + .os + .lifecycle() + .resident_game_task() + .expect("failed target should be resident until recovery tick"); + assert_ne!(partial_task, first_game_task); + + firmware.tick(&InputSignals::default(), &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().resident_game_task(), None); + assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed)); + assert_eq!(firmware.os.lifecycle().task_state(partial_task), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(partial_task), + Ok(ProcessState::Stopped) + ); + assert_eq!( + firmware.os.sessions().vm_session_for_task(partial_task).map(|session| session.task_id), + None + ); + assert!( + firmware + .os + .recent_logs(16) + .iter() + .any(|event| event.msg.contains("Failed to switch to game Zzz Broken app_id 43")) + ); + } + #[test] fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() { let root = TestGameRoot::new(); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index 93c59dd6..63bf9250 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -83,9 +83,9 @@ impl HubHomeStep { } } - return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new( - cartridge, - ))); + return Some(FirmwareState::LoadCartridge( + LoadCartridgeStep::new_game_switch(cartridge), + )); } Err(error) => { ctx.os.log( 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 80307186..ea2e0cfe 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 @@ -1,5 +1,5 @@ use crate::firmware::firmware_state::{ - AppCrashesStep, FirmwareState, GameRunningStep, ShellRunningStep, + AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep, ShellRunningStep, }; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::app_mode::AppMode; @@ -16,11 +16,28 @@ pub struct LoadCartridgeStep { init_error: Option, loaded_task_id: Option, loaded_app_mode: Option, + failure_recovery: LoadCartridgeFailureRecovery, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LoadCartridgeFailureRecovery { + AppCrash, + ReturnToHub, } impl LoadCartridgeStep { pub fn new(cartridge: Cartridge) -> Self { - Self { cartridge, init_error: None, loaded_task_id: None, loaded_app_mode: None } + Self { + cartridge, + init_error: None, + loaded_task_id: None, + loaded_app_mode: None, + failure_recovery: LoadCartridgeFailureRecovery::AppCrash, + } + } + + pub fn new_game_switch(cartridge: Cartridge) -> Self { + Self { failure_recovery: LoadCartridgeFailureRecovery::ReturnToHub, ..Self::new(cartridge) } } pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { @@ -51,6 +68,34 @@ impl LoadCartridgeStep { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { if let Some(report) = self.init_error.take() { + if self.failure_recovery == LoadCartridgeFailureRecovery::ReturnToHub { + if self.cartridge.app_mode == AppMode::Game + && let Some(task_id) = ctx.os.lifecycle().resident_game_task() + { + let is_partial_target = ctx + .os + .sessions() + .vm_session_for_task(task_id) + .map(|session| session.app_id == self.cartridge.app_id) + .unwrap_or(false); + + if is_partial_target { + let _ = ctx.os.lifecycle().terminate_resident_game(); + } + } + + ctx.os.log( + LogLevel::Error, + LogSource::Hub, + 0, + format!( + "Failed to switch to game {} app_id {}; returning to Home: {:?}", + self.cartridge.title, self.cartridge.app_id, report + ), + ); + return Some(FirmwareState::HubHome(HubHomeStep)); + } + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 204218d2..f3184ac2 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md b/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md index 06dba643..560a08c4 100644 --- a/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md +++ b/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md @@ -2,7 +2,7 @@ id: PLN-0159 ticket: system-os-cartridge-switch-orchestrator title: Recover Failed Game Switches Back To Home -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] -- 2.47.2 From d6afa6ae6bc6559ff496046362b5cc4548245086 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:18:33 +0100 Subject: [PATCH 05/13] implements PLN-0160 --- Cargo.lock | 10 ++ Cargo.toml | 1 + .../src/services/game_library.rs | 18 +++ crates/tools/pbxgen-dummy-boy/Cargo.toml | 12 ++ crates/tools/pbxgen-dummy-boy/src/lib.rs | 147 ++++++++++++++++++ crates/tools/pbxgen-dummy-boy/src/main.rs | 5 + discussion/index.ndjson | 2 +- ...-dummy-boy-visual-switch-test-cartridge.md | 2 +- test-cartridges/dummy-boy/README.md | 11 ++ test-cartridges/dummy-boy/manifest.json | 9 ++ test-cartridges/dummy-boy/program.pbx | Bin 0 -> 400 bytes 11 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 crates/tools/pbxgen-dummy-boy/Cargo.toml create mode 100644 crates/tools/pbxgen-dummy-boy/src/lib.rs create mode 100644 crates/tools/pbxgen-dummy-boy/src/main.rs create mode 100644 test-cartridges/dummy-boy/README.md create mode 100644 test-cartridges/dummy-boy/manifest.json create mode 100644 test-cartridges/dummy-boy/program.pbx diff --git a/Cargo.lock b/Cargo.lock index e3fbe577..5bcd2b9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,6 +1469,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbxgen-dummy-boy" +version = "0.1.0" +dependencies = [ + "anyhow", + "prometeu-bytecode", + "prometeu-hal", + "serde_json", +] + [[package]] name = "pbxgen-stress" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index cc8e5d61..278dd394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/console/prometeu-vm", "crates/host/prometeu-host-desktop-winit", "crates/tools/prometeu-cli", + "crates/tools/pbxgen-dummy-boy", "crates/tools/pbxgen-stress", "crates/dev/prometeu-test-support", "crates/dev/prometeu-layer-tests", diff --git a/crates/console/prometeu-system/src/services/game_library.rs b/crates/console/prometeu-system/src/services/game_library.rs index 9d357203..1989e29e 100644 --- a/crates/console/prometeu-system/src/services/game_library.rs +++ b/crates/console/prometeu-system/src/services/game_library.rs @@ -315,4 +315,22 @@ mod tests { assert_eq!(library.diagnostics().len(), 1); assert_eq!(library.diagnostics()[0].reason, GameLibraryDiagnosticReason::RootNotDirectory); } + + #[test] + fn repository_test_cartridges_include_stress_and_dummy_boy() { + let games_root = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join("test-cartridges"); + + let library = discover_games_root(games_root); + let entries = library.entries(); + + assert!(entries.iter().any(|entry| { + entry.app_id == 1 + && entry.title == "Stress Console" + && entry.path.ends_with("stress-console") + })); + assert!(entries.iter().any(|entry| { + entry.app_id == 2 && entry.title == "Dummy Boy" && entry.path.ends_with("dummy-boy") + })); + } } diff --git a/crates/tools/pbxgen-dummy-boy/Cargo.toml b/crates/tools/pbxgen-dummy-boy/Cargo.toml new file mode 100644 index 00000000..7d64f7aa --- /dev/null +++ b/crates/tools/pbxgen-dummy-boy/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pbxgen-dummy-boy" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +prometeu-bytecode = { path = "../../console/prometeu-bytecode" } +prometeu-hal = { path = "../../console/prometeu-hal" } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/tools/pbxgen-dummy-boy/src/lib.rs b/crates/tools/pbxgen-dummy-boy/src/lib.rs new file mode 100644 index 00000000..4cccf568 --- /dev/null +++ b/crates/tools/pbxgen-dummy-boy/src/lib.rs @@ -0,0 +1,147 @@ +use anyhow::Result; +use prometeu_bytecode::assembler::assemble; +use prometeu_bytecode::model::{BytecodeModule, DebugInfo, Export, FunctionMeta, SyscallDecl}; +use prometeu_hal::color::Color; +use std::fs; +use std::path::PathBuf; + +const APP_ID: u32 = 2; +const TITLE: &str = "Dummy Boy"; +const APP_VERSION: &str = "0.1.0"; + +fn asm(s: &str) -> Vec { + assemble(s).expect("assemble") +} + +pub fn generate() -> Result<()> { + let out_dir = cartridge_dir(); + fs::create_dir_all(&out_dir)?; + fs::write(out_dir.join("program.pbx"), build_program())?; + fs::write(out_dir.join("manifest.json"), manifest_json())?; + Ok(()) +} + +pub fn build_program() -> Vec { + let mut rom = Vec::new(); + + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); + rom.extend(asm(&format!("PUSH_I64 {}\nHOSTCALL 0", Color::BLACK.raw()))); + + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMOD\nPUSH_I32 0\nEQ")); + let jump_to_green_case_offset = rom.len() + 2; + rom.extend(asm("JMP_IF_FALSE 0")); + rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::RED.raw()))); + let jump_to_draw_after_red_offset = rom.len() + 2; + rom.extend(asm("JMP 0")); + + let green_case = rom.len() as u32; + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMOD\nPUSH_I32 1\nEQ")); + let jump_to_blue_case_offset = rom.len() + 2; + rom.extend(asm("JMP_IF_FALSE 0")); + rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::GREEN.raw()))); + let jump_to_draw_after_green_offset = rom.len() + 2; + rom.extend(asm("JMP 0")); + + let blue_case = rom.len() as u32; + rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::BLUE.raw()))); + + let draw = rom.len() as u32; + rom.extend(asm(&format!( + "PUSH_I32 180\n\ + PUSH_I32 75\n\ + PUSH_I32 120\n\ + PUSH_I32 120\n\ + PUSH_I64 {}\n\ + GET_LOCAL 0\n\ + HOSTCALL 1\n\ + FRAME_SYNC\n\ + RET", + Color::WHITE.raw() + ))); + + patch_jump(&mut rom, jump_to_green_case_offset, green_case); + patch_jump(&mut rom, jump_to_draw_after_red_offset, draw); + patch_jump(&mut rom, jump_to_blue_case_offset, blue_case); + patch_jump(&mut rom, jump_to_draw_after_green_offset, draw); + + BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![FunctionMeta { + code_offset: 0, + code_len: rom.len() as u32, + param_slots: 0, + local_slots: 1, + return_slots: 0, + max_stack_slots: 8, + }], + code: rom, + debug_info: Some(DebugInfo { + pc_to_span: vec![], + function_names: vec![(0, "main".into())], + }), + exports: vec![Export { symbol: "main".into(), func_idx: 0 }], + syscalls: vec![ + SyscallDecl { + module: "gfx2d".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }, + SyscallDecl { + module: "gfx2d".into(), + name: "draw_square".into(), + version: 1, + arg_slots: 6, + ret_slots: 0, + }, + ], + } + .serialize() +} + +pub fn manifest_json() -> Vec { + format!( + "{{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": {APP_ID},\n \"title\": \"{TITLE}\",\n \"app_version\": \"{APP_VERSION}\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx2d\"]\n}}\n" + ) + .into_bytes() +} + +fn cartridge_dir() -> PathBuf { + let mut out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + out_dir.pop(); + out_dir.pop(); + out_dir.pop(); + out_dir.push("test-cartridges"); + out_dir.push("dummy-boy"); + out_dir +} + +fn patch_jump(buf: &mut [u8], imm_offset: usize, target: u32) { + buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_program_is_pbx_module_with_expected_syscalls() { + let bytes = build_program(); + + assert!(bytes.starts_with(b"PBS\0")); + assert!(bytes.len() > 64); + } + + #[test] + fn manifest_declares_distinct_game_identity() { + let manifest: serde_json::Value = + serde_json::from_slice(&manifest_json()).expect("manifest should parse"); + + assert_eq!(manifest["app_id"], APP_ID); + assert_eq!(manifest["title"], TITLE); + assert_eq!(manifest["app_mode"], "Game"); + assert_eq!(manifest["capabilities"], serde_json::json!(["gfx2d"])); + } +} diff --git a/crates/tools/pbxgen-dummy-boy/src/main.rs b/crates/tools/pbxgen-dummy-boy/src/main.rs new file mode 100644 index 00000000..954d6946 --- /dev/null +++ b/crates/tools/pbxgen-dummy-boy/src/main.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +fn main() -> Result<()> { + pbxgen_dummy_boy::generate() +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index f3184ac2..200fd52b 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"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":"open","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":"open","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":"open","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":"open","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":"open","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"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md index 34ad7d49..80b2ce06 100644 --- a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md +++ b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md @@ -2,7 +2,7 @@ id: PLN-0160 ticket: system-os-cartridge-switch-orchestrator title: Add Dummy Boy Visual Switch Test Cartridge -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] diff --git a/test-cartridges/dummy-boy/README.md b/test-cartridges/dummy-boy/README.md new file mode 100644 index 00000000..4739363a --- /dev/null +++ b/test-cartridges/dummy-boy/README.md @@ -0,0 +1,11 @@ +# Dummy Boy + +Regenerate this cartridge with: + +```sh +cargo run -p pbxgen-dummy-boy +``` + +Dummy Boy is a minimal visual Game cartridge used by runtime switch tests. It +clears the frame and draws a centered square whose fill color cycles +deterministically across frames. diff --git a/test-cartridges/dummy-boy/manifest.json b/test-cartridges/dummy-boy/manifest.json new file mode 100644 index 00000000..936959f4 --- /dev/null +++ b/test-cartridges/dummy-boy/manifest.json @@ -0,0 +1,9 @@ +{ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": 2, + "title": "Dummy Boy", + "app_version": "0.1.0", + "app_mode": "Game", + "capabilities": ["gfx2d"] +} diff --git a/test-cartridges/dummy-boy/program.pbx b/test-cartridges/dummy-boy/program.pbx new file mode 100644 index 0000000000000000000000000000000000000000..ee2844a8c128c46fea097bba1ca2ba66a6945310 GIT binary patch literal 400 zcmZWlI|{;35ZtH{!Nwx+S_=;#DXn5G1yKYmQ646Wpkg9oN=uL6K|F<*(%DJ;pbute zci!x7@`mA9^tg=w!GSadJfH;ZfjdwJI=EE50t}Ehz&dA!+y`r~9`)x{O#ln>y?-w7Nv8J Date: Sun, 5 Jul 2026 01:24:45 +0100 Subject: [PATCH 06/13] implements PLN-0161 --- .../src/firmware/firmware.rs | 104 +++++++++++++++++- .../prometeu-system/src/os/facades/vm.rs | 10 ++ discussion/index.ndjson | 2 +- ...ess-to-dummy-boy-game-switch-end-to-end.md | 2 +- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index bfeaac3e..7d9fccdc 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -358,6 +358,10 @@ mod tests { .serialize() } + fn repository_test_cartridges_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join("test-cartridges") + } + fn invalid_game_cartridge() -> Cartridge { Cartridge { app_id: 7, @@ -819,6 +823,100 @@ mod tests { } } + #[test] + fn repository_stress_to_dummy_boy_switch_is_end_to_end() { + let library = discover_games_root(repository_test_cartridges_root()); + assert!( + library + .entries() + .iter() + .any(|entry| entry.app_id == 1 && entry.title == "Stress Console") + ); + assert!( + library.entries().iter().any(|entry| entry.app_id == 2 && entry.title == "Dummy Boy") + ); + + 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), + }; + assert_eq!(firmware.os.vm().current_app_id(), 1); + assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console"); + + 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, + ); + 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 dummy_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&dummy_click, &mut platform); + + 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 + ); + + let dummy_task = firmware + .os + .lifecycle() + .resident_game_task() + .expect("Dummy Boy should be resident after switch load"); + assert_ne!(dummy_task, stress_task); + assert_eq!( + firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| { + (session.app_id, session.title.clone(), session.app_version.clone()) + }), + Some((2, "Dummy Boy".to_string(), "0.1.0".to_string())) + ); + + firmware.tick(&InputSignals::default(), &mut platform); + + match &firmware.state { + FirmwareState::GameRunning(step) => assert_eq!(step.task_id, dummy_task), + other => panic!("expected Dummy Boy GameRunning state, got {:?}", other), + } + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(dummy_task)); + assert_eq!(firmware.os.vm().current_app_id(), 2); + assert_eq!(firmware.os.vm().current_cartridge_title(), "Dummy Boy"); + + firmware.tick(&InputSignals::default(), &mut platform); + + let dummy_ownership = firmware.os.vm().active_render_ownership(); + assert_eq!(dummy_ownership.app_mode, AppMode::Game); + assert_eq!(dummy_ownership.app_id, 2); + + let front = platform.local_hardware().gfx.front_buffer(); + let center = 240 + 135 * 480; + assert_eq!(front[center], Color::GREEN.raw()); + } + #[test] fn failed_game_switch_returns_home_without_restoring_previous_game() { let root = TestGameRoot::new(); @@ -1068,6 +1166,8 @@ mod tests { }; let tick_index_before_home = firmware.os.vm().tick_index(); let render_ownership_before_home = firmware.os.vm().active_render_ownership(); + assert_eq!(render_ownership_before_home.app_mode, AppMode::Game); + assert_eq!(render_ownership_before_home.app_id, 9); firmware.request_home_from_host(); @@ -1099,7 +1199,6 @@ mod tests { let render_ownership_after_home = firmware.os.vm().active_render_ownership(); assert_eq!(render_ownership_after_home.app_mode, AppMode::Shell); assert_eq!(render_ownership_after_home.app_id, 0); - assert!(render_ownership_after_home.epoch > render_ownership_before_home.epoch); assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty()); assert_eq!( firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind), @@ -1128,6 +1227,8 @@ mod tests { firmware.request_home_from_host(); firmware.tick(&signals, &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!(firmware.resume_resident_game_from_home()); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); @@ -1140,7 +1241,6 @@ mod tests { let game_ownership = firmware.os.vm().active_render_ownership(); assert_eq!(game_ownership.app_mode, AppMode::Game); assert_eq!(game_ownership.app_id, 9); - assert!(game_ownership.epoch > hub_ownership.epoch); let held_signals = InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals }; diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 367ac0f5..a76b167e 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -250,6 +250,12 @@ impl<'a> VmFacade<'a> { } pub fn active_render_ownership(&self) -> RenderOwnership { + if let Some(task_id) = self.foreground_session_task_id() + && let Some(session) = self.os.vm_sessions.for_task(task_id) + { + return session.runtime.render_manager.active_ownership(); + } + self.os.vm_runtime.render_manager.active_ownership() } @@ -317,4 +323,8 @@ impl<'a> VmFacade<'a> { .foreground_task() .or_else(|| self.os.foreground_stack.resident_game_task()) } + + fn foreground_session_task_id(&self) -> Option { + self.os.task_manager.foreground_task() + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 200fd52b..77a7b6c3 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"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":"open","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":"open","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":"open","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"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md index 6889679e..110b9565 100644 --- a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md +++ b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md @@ -2,7 +2,7 @@ id: PLN-0161 ticket: system-os-cartridge-switch-orchestrator title: Validate Stress To Dummy Boy Game Switch End To End -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] -- 2.47.2 From c814e057dc7912366553f2e59082b7450ab32c53 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 01:26:42 +0100 Subject: [PATCH 07/13] implements PLN-0162 --- discussion/index.ndjson | 2 +- ...tch-contract-and-direct-boot-separation.md | 2 +- .../runtime/09-events-and-concurrency.md | 11 ++++++ .../10-debug-inspection-and-profiling.md | 5 +++ .../12-firmware-pos-and-prometeuhub.md | 37 +++++++++++++++++-- docs/specs/runtime/13-cartridge.md | 8 ++++ docs/specs/runtime/14-boot-profiles.md | 16 ++++++-- 7 files changed, 73 insertions(+), 8 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 77a7b6c3..f1f4d8e5 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"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":"open","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-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-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md b/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md index b4d85d48..af057ec9 100644 --- a/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md +++ b/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md @@ -2,7 +2,7 @@ id: PLN-0162 ticket: system-os-cartridge-switch-orchestrator title: Specify Game Switch Contract And Direct Boot Separation -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] diff --git a/docs/specs/runtime/09-events-and-concurrency.md b/docs/specs/runtime/09-events-and-concurrency.md index 4ec0e44a..055b7fdd 100644 --- a/docs/specs/runtime/09-events-and-concurrency.md +++ b/docs/specs/runtime/09-events-and-concurrency.md @@ -128,6 +128,11 @@ SystemOS resume request scheduler/runtime state. A Game may observe pause/resume, but it must not control whether SystemOS suspends or resumes the VM. +When SystemOS replaces one Game with another from Home, the old Game MAY receive +a cooperative terminate/save lifecycle event if the runtime supports such an +event. That event is non-blocking in v1. SystemOS remains authoritative and may +close the old Game task/session without guest acknowledgement. + Pause budgets are measured in deterministic machine units, not host wall-clock time. A suspended Game receives no normal gameplay ticks, no normal Game input, and no frame pacing. Real background execution is outside the v1 foreground @@ -163,6 +168,12 @@ requires it. Transient VM handles, open files, lifecycle delivery queues, and staging state are session-scoped unless another spec explicitly makes them durable app data. +During destructive Game replacement, the old Game VM session MUST be removed +from execution eligibility and resumable state before the new Game becomes the +foreground Game. The new Game MUST receive a fresh VM session. Failure while +loading or initializing the replacement Game MUST return the machine to +Hub/Home and MUST NOT restore the previous Game in v1. + ## 9 Render Worker Concurrency The render worker is not a machine-visible event source and does not introduce diff --git a/docs/specs/runtime/10-debug-inspection-and-profiling.md b/docs/specs/runtime/10-debug-inspection-and-profiling.md index 4c9ed75a..47c74456 100644 --- a/docs/specs/runtime/10-debug-inspection-and-profiling.md +++ b/docs/specs/runtime/10-debug-inspection-and-profiling.md @@ -44,6 +44,11 @@ PROMETEU operates in three main modes: No mode alters the logical result of the program. +Debug boot is a host-selected direct cartridge boot profile. It follows the same +cartridge/app-mode resolution path as direct boot and is separate from +Home-driven Game switching. Debug boot MUST NOT be modeled as a guest-visible +Game switch request and MUST NOT reuse a Home switch failure-recovery contract. + ## 3 Execution Control ### 3.1 Pause and Resume diff --git a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md index b75246c9..bda74dc3 100644 --- a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md +++ b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md @@ -43,6 +43,9 @@ The VM does not own the machine lifecycle. Firmware does. Game directory cartridges. - **Library entry**: an internal catalog record retained by Hub/SystemOS for a discovered cartridge. +- **Game switch target**: a SystemOS-owned launch target derived from a library + entry. It carries the cartridge path plus resolved identity such as `app_id`, + `title`, and `app_version`. - **Foreground visual owner**: the single machine owner whose visual output may be presented at a given time. - **Paused**: a Game-visible lifecycle event/state. The Game may observe it and @@ -231,9 +234,27 @@ Current high-level flow: If VM initialization fails, firmware transitions to the crash path. For v1, POS must enforce at most one resident Game session. Selecting the same -resident Game from Home resumes that session. Selecting a different Game while a -resident Game exists is outside the v1 foreground contract and must not create a -second resident Game session. VM-backed Shell loading creates a distinct Shell +resident Game from Home resumes that session. Selecting a different Game from +Home is a SystemOS-owned Game switch and MUST be destructive replacement: + +```text +Game A resident + -> Hub/Home selected Game B with different app_id + -> SystemOS terminates Game A resident task/session + -> SystemOS removes Game A from resumable state + -> firmware loads Game B through LoadCartridge + -> Game B receives a fresh VM session +``` + +SystemOS MUST NOT create a second resident Game session. SystemOS MUST NOT +reuse the old Game VM session, transient VM handles, pending lifecycle delivery +state, render ownership, debug pause state, or crash/debug runtime state for the +new Game. Durable app data remains governed by the relevant storage specs and +may still be keyed by `app_id`. + +SystemOS MAY emit a cooperative non-blocking terminate/save lifecycle event +before closing the old Game when such an event exists. Replacement MUST NOT +block on that event in v1. VM-backed Shell loading creates a distinct Shell session and must not overwrite resident Game VM state. POS selects the cartridge and its execution context, but the cartridge's initial callable is not chosen by firmware metadata. Execution starts from the cartridge boot protocol defined in [`13-cartridge.md`](13-cartridge.md), currently `func_id = 0`. @@ -248,6 +269,16 @@ If the selected cartridge cannot be loaded, firmware must leave or return the machine in the Hub/Home flow and record the failure through diagnostics or logging. A polished Home error surface is not required by v1. +If a destructive Game switch has already begun and the new Game fails to load or +initialize, firmware MUST return to Hub/Home, MUST discard any partial new Game +task/session/window/render/input/audio state, and MUST NOT restore the previous +Game. Direct boot and debug boot failures remain governed by the boot profile +that requested them and may still transition to the crash path. + +Switch validation MUST include two distinct Game cartridges. The current +repository evidence uses `Stress Console` and `Dummy Boy`; those cartridges are +test evidence, not required runtime features. + ## 9 Firmware States The current firmware state model includes: diff --git a/docs/specs/runtime/13-cartridge.md b/docs/specs/runtime/13-cartridge.md index 6f8c1158..a53181bf 100644 --- a/docs/specs/runtime/13-cartridge.md +++ b/docs/specs/runtime/13-cartridge.md @@ -186,6 +186,9 @@ For the v1 Home games library: - the manifest must declare `app_mode: "Game"`; - the library retains the loaded manifest data, `title`, `app_id`, `app_version`, the internal cartridge path, and discovery metadata; +- the library entry identity is the input to the SystemOS Game switch target; +- `app_id` is the v1 identity used to distinguish same-Game resume from + different-Game destructive replacement; - invalid candidates are logged or reported as diagnostics and omitted from the Home list; - valid non-Game cartridges are omitted from the Game library. @@ -193,6 +196,11 @@ For the v1 Home games library: Discovery reads metadata for cataloging. It does not replace the normal cartridge loader used when firmware starts the selected cartridge. +Selecting a discovered Game from Home is not cartridge self-execution and is not +guest-controlled. The cartridge path is consumed by firmware/SystemOS, which +owns same-Game resume, different-Game replacement, failure recovery, and VM +session creation policy. + ### Packaged `.pmc` form This form is recognized conceptually by the loader boundary, but its actual load path is not implemented yet. diff --git a/docs/specs/runtime/14-boot-profiles.md b/docs/specs/runtime/14-boot-profiles.md index a420804c..a2529762 100644 --- a/docs/specs/runtime/14-boot-profiles.md +++ b/docs/specs/runtime/14-boot-profiles.md @@ -97,9 +97,11 @@ The v1 games root contract is: and transitions through `LoadCartridge`; - launch failure leaves or returns the machine to Home and records an error. -Game-to-game switching after a game is already active is not part of this boot -profile. That orchestration belongs to the foreground/home and cartridge switch -contracts. +Game-to-game switching after a game is already active is not a boot profile. +That orchestration belongs to the foreground/home and cartridge switch +contracts. In that path, SystemOS owns the switch policy: selecting the same +resident Game resumes it, while selecting a different Game performs destructive +replacement and creates a fresh VM session for the new Game. The foreground/home contract defines `Game -> Home/Shell -> same Game` after a Game is already running. It does not change direct `--run`, debugger direct @@ -108,6 +110,10 @@ are host/system controls, not guest cartridge boot APIs. Direct boot creates the initial VM session for the selected cartridge. It does not create a reusable global VM shared by later Game or Shell cartridges. +Direct boot and debug boot MUST NOT be treated as failed-switch recovery paths: +load or initialization failures for direct boot may transition to the crash +flow, while Home-driven switch failures return to Home and do not restore the +previous Game. ## 5 Firmware State Relationship @@ -152,6 +158,10 @@ Direct single-cartridge boot remains a separate validation path: cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console ``` +Game switch validation should cover the Home-driven two-cartridge path, for +example `Stress Console -> Home -> Dummy Boy`, and assert that the first Game is +not resumable after replacement. + ## 8 Relationship to Other Specs - [`12-firmware-pos-and-prometeuhub.md`](12-firmware-pos-and-prometeuhub.md) defines the firmware state machine that consumes boot targets. -- 2.47.2 From 62573e9574d36db5f9e4bf59f82d45f212c74deb Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 09:25:01 +0100 Subject: [PATCH 08/13] Hub Suspended Game Kill Affordance --- crates/console/prometeu-drivers/src/asset.rs | 48 ++++++++++ .../prometeu-drivers/src/memory_banks.rs | 18 ++++ .../src/firmware/firmware.rs | 95 ++++++++++++++++--- .../firmware/firmware_step_load_cartridge.rs | 2 + .../prometeu-host-desktop-winit/src/runner.rs | 63 +++++++++--- discussion/index.ndjson | 3 +- ...0047-hub-suspended-game-kill-affordance.md | 91 ++++++++++++++++++ ...0040-hub-suspended-game-kill-affordance.md | 73 ++++++++++++++ ...-render-hub-suspended-game-kill-control.md | 85 +++++++++++++++++ ...pose-resident-game-termination-contract.md | 93 ++++++++++++++++++ ...-0165-validate-hub-kill-flow-end-to-end.md | 92 ++++++++++++++++++ ...hub-kill-contract-spec-and-lesson-hooks.md | 78 +++++++++++++++ 12 files changed, 714 insertions(+), 27 deletions(-) create mode 100644 discussion/workflow/agendas/AGD-0047-hub-suspended-game-kill-affordance.md create mode 100644 discussion/workflow/decisions/DEC-0040-hub-suspended-game-kill-affordance.md create mode 100644 discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md create mode 100644 discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md create mode 100644 discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md create mode 100644 discussion/workflow/plans/PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md 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 -- 2.47.2 From fa0a768b84ac25437d531b1c0a9f50010ac7bc5b Mon Sep 17 00:00:00 2001 From: bQUARKz <bquarkz@gmail.com> Date: Sun, 5 Jul 2026 09:31:27 +0100 Subject: [PATCH 09/13] implements PLN-0164 --- .../src/firmware/firmware.rs | 111 ++++++++++++++++++ .../src/firmware/firmware_step_hub_home.rs | 7 +- .../firmware/firmware_step_load_cartridge.rs | 7 +- .../prometeu-firmware/src/firmware/mod.rs | 1 + .../src/firmware/resident_game_termination.rs | 42 +++++++ discussion/index.ndjson | 2 +- ...pose-resident-game-termination-contract.md | 2 +- 7 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 crates/console/prometeu-firmware/src/firmware/resident_game_termination.rs diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index babe09eb..960a58e2 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -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(); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index 63bf9250..7f6740d3 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -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, 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 2cbeb823..ccc2ed1d 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 @@ -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, + ); } } diff --git a/crates/console/prometeu-firmware/src/firmware/mod.rs b/crates/console/prometeu-firmware/src/firmware/mod.rs index 0bc6518f..731258fc 100644 --- a/crates/console/prometeu-firmware/src/firmware/mod.rs +++ b/crates/console/prometeu-firmware/src/firmware/mod.rs @@ -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; diff --git a/crates/console/prometeu-firmware/src/firmware/resident_game_termination.rs b/crates/console/prometeu-firmware/src/firmware/resident_game_termination.rs new file mode 100644 index 00000000..2df2f00e --- /dev/null +++ b/crates/console/prometeu-firmware/src/firmware/resident_game_termination.rs @@ -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 }) +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 34c069ce..09a11462 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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"}]} 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 index 9176511b..e16d0535 100644 --- a/discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md +++ b/discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md @@ -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] -- 2.47.2 From 73c8246c8de35f85f7b0ad9742b0830b5473597b Mon Sep 17 00:00:00 2001 From: bQUARKz <bquarkz@gmail.com> Date: Sun, 5 Jul 2026 09:36:59 +0100 Subject: [PATCH 10/13] implements PLN-0163 --- .../src/firmware/firmware.rs | 58 +++++ .../src/firmware/firmware_step_hub_home.rs | 12 + .../src/programs/prometeu_hub/prometeu_hub.rs | 232 +++++++++++++++++- discussion/index.ndjson | 2 +- ...-render-hub-suspended-game-kill-control.md | 2 +- 5 files changed, 293 insertions(+), 13 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 960a58e2..3f56f8c1 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -1099,6 +1099,64 @@ mod tests { ); } + #[test] + fn hub_kill_control_click_terminates_resident_game_and_stays_home() { + let library = discover_games_root(repository_test_cartridges_root()); + let (mut firmware, mut platform) = hub_home_firmware(); + firmware.hub.set_game_library(library); + firmware.tick(&InputSignals::default(), &mut platform); + + let stress_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() }; + firmware.tick(&stress_click, &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + let stress_task = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected Stress GameRunning state, got {:?}", other), + }; + + firmware.request_home_from_host(); + firmware.tick(&InputSignals::default(), &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task)); + + let kill_click = + InputSignals { f_signal: true, x_pos: 300, y_pos: 240, ..Default::default() }; + firmware.tick(&kill_click, &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().resident_game_task(), None); + assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(stress_task), + Ok(ProcessState::Stopped) + ); + assert_eq!( + firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id), + None + ); + assert!(!firmware.game_lifecycle_audio_paused()); + assert_eq!( + platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, + None + ); + assert_eq!( + platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, + None + ); + assert!( + firmware.os.recent_logs(32).iter().any(|event| { + event.level == LogLevel::Info + && event.source == LogSource::Hub + && event.msg == "Resident game killed from Hub: Stress Console" + }), + "Hub kill control should use the canonical manual kill log" + ); + } + #[test] fn failed_game_switch_returns_home_without_restoring_previous_game() { let root = TestGameRoot::new(); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index 7f6740d3..3d852aa3 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -106,6 +106,18 @@ impl HubHomeStep { } } } + Some(SystemProfileAction::KillResidentGame) => { + if let Err(error) = + resident_game_termination::terminate_resident_game(ctx.os, ctx.platform, true) + { + ctx.os.log( + LogLevel::Error, + LogSource::Hub, + 0, + format!("Failed to kill resident game from Hub: {error:?}"), + ); + } + } Some(SystemProfileAction::CloseShell) | None => {} } diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index b2f2ce26..10ed3418 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -1,5 +1,6 @@ use crate::task::TaskId; use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS}; +use prometeu_hal::app_mode::AppMode; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ @@ -19,6 +20,7 @@ const GAME_ROW_W: i32 = 336; const GAME_ROW_H: i32 = 22; const GAME_ROW_GAP: i32 = 6; const MAX_HOME_GAME_ROWS: usize = 3; +const SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 }; #[cfg(test)] const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 }; @@ -31,6 +33,8 @@ const COLOR_TEXT: Color = Color::rgb(230, 236, 224); const COLOR_MUTED: Color = Color::rgb(91, 112, 128); const COLOR_BUTTON: Color = Color::rgb(45, 59, 82); const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122); +const COLOR_KILL: Color = Color::rgb(164, 32, 40); +const COLOR_KILL_ACTIVE: Color = Color::rgb(206, 46, 54); /// PrometeuHub: Launcher and system UI environment. pub struct PrometeuHub { @@ -83,6 +87,7 @@ impl NativeShellApp { pub enum SystemProfileAction { LaunchNativeShell(NativeShellApp), LaunchGame { target: GameSwitchTarget }, + KillResidentGame, CloseShell, } @@ -131,12 +136,14 @@ impl PrometeuHub { } let input = platform.input(); + let suspended_game = if in_shell { None } else { resident_game_indicator(os) }; if let Some(action) = action_for_click( in_shell, input.touch().x(), input.touch().y(), input.touch().f().pressed, self.game_library.entries(), + suspended_game.as_ref(), ) { return Some(action); } @@ -149,7 +156,13 @@ impl PrometeuHub { let pointer_y = platform.input().touch().y(); let packet = if os.windows().window_count() == 0 { - render_home_packet(pointer_x, pointer_y, self.game_library.entries()) + let suspended_game = resident_game_indicator(os); + render_home_packet( + pointer_x, + pointer_y, + self.game_library.entries(), + suspended_game.as_ref(), + ) } else { let mut commands = Vec::new(); for window in os.windows().windows() { @@ -232,6 +245,7 @@ fn action_for_click( y: i32, pressed: bool, game_entries: &[crate::GameLibraryEntry], + suspended_game: Option<&SuspendedGameIndicator>, ) -> Option<SystemProfileAction> { if !pressed { return None; @@ -241,6 +255,10 @@ fn action_for_click( return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell); } + if suspended_game.is_some() && rect_contains(SUSPENDED_GAME_KILL_CONTROL, x, y) { + return Some(SystemProfileAction::KillResidentGame); + } + if let Some(entry) = game_entries .iter() .take(MAX_HOME_GAME_ROWS) @@ -257,6 +275,23 @@ fn action_for_click( .map(|button| SystemProfileAction::LaunchNativeShell(button.app)) } +#[derive(Debug, Clone, PartialEq, Eq)] +struct SuspendedGameIndicator { + title: String, +} + +fn resident_game_indicator(os: &mut SystemOS) -> Option<SuspendedGameIndicator> { + let task_id = os.lifecycle().resident_game_task()?; + let sessions = os.sessions(); + let session = sessions.vm_session_for_task(task_id)?; + + if session.app_mode != AppMode::Game { + return None; + } + + Some(SuspendedGameIndicator { title: session.title.clone() }) +} + fn game_row_rect(index: usize) -> Rect { Rect { x: GAME_ROW_X, @@ -281,6 +316,7 @@ fn render_home_packet( pointer_x: i32, pointer_y: i32, game_entries: &[crate::GameLibraryEntry], + suspended_game: Option<&SuspendedGameIndicator>, ) -> ShellUiFramePacket { let mut commands = Vec::new(); commands.push(GfxUiCommand::Clear { color: COLOR_BG }); @@ -320,6 +356,11 @@ fn render_home_packet( draw_button(&mut commands, button.rect, button.label, hovered); } + if let Some(suspended_game) = suspended_game { + let hovered = rect_contains(SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y); + draw_suspended_game_kill_control(&mut commands, suspended_game, hovered); + } + ShellUiFramePacket::new(commands) } @@ -422,6 +463,39 @@ fn draw_game_row(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hove draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT); } +fn draw_suspended_game_kill_control( + commands: &mut Vec<GfxUiCommand>, + suspended_game: &SuspendedGameIndicator, + hovered: bool, +) { + let fill = if hovered { COLOR_KILL_ACTIVE } else { COLOR_KILL }; + let border = if hovered { COLOR_HILITE } else { COLOR_BORDER }; + fill_rect(commands, SUSPENDED_GAME_KILL_CONTROL, fill); + draw_rect(commands, SUSPENDED_GAME_KILL_CONTROL, border); + draw_buffer_text( + commands, + SUSPENDED_GAME_KILL_CONTROL.x + 8, + SUSPENDED_GAME_KILL_CONTROL.y + 8, + &fit_text_to_control(&suspended_game.title, SUSPENDED_GAME_KILL_CONTROL.w - 16), + COLOR_TEXT, + ); +} + +fn fit_text_to_control(text: &str, max_width: i32) -> String { + let max_chars = (max_width / 8).max(0) as usize; + if text.chars().count() <= max_chars { + return text.to_string(); + } + + if max_chars <= 2 { + return text.chars().take(max_chars).collect(); + } + + let mut fitted: String = text.chars().take(max_chars - 2).collect(); + fitted.push_str(".."); + fitted +} + fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) { commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color }); } @@ -462,7 +536,14 @@ mod tests { #[test] fn shell_a_button_click_emits_launch_action() { assert_eq!( - action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true, &[]), + action_for_click( + false, + HOME_BUTTONS[0].rect.x, + HOME_BUTTONS[0].rect.y, + true, + &[], + None + ), Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA)) ); } @@ -475,7 +556,8 @@ mod tests { SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1, SHELL_B_BUTTON.y, true, - &[] + &[], + None ), Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB)) ); @@ -489,7 +571,8 @@ mod tests { SHELL_A_BUTTON.x + SHELL_A_BUTTON.w, SHELL_A_BUTTON.y, true, - &[] + &[], + None ), None ); @@ -499,7 +582,8 @@ mod tests { SHELL_A_BUTTON.x, SHELL_A_BUTTON.y + SHELL_A_BUTTON.h, true, - &[] + &[], + None ), None ); @@ -507,7 +591,10 @@ mod tests { #[test] fn held_pointer_does_not_emit_click_action() { - assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[]), None); + assert_eq!( + action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[], None), + None + ); } #[test] @@ -535,8 +622,14 @@ mod tests { discovered_at: std::time::SystemTime::UNIX_EPOCH, }; - let action = - action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)); + let action = action_for_click( + false, + GAME_ROW_X, + GAME_ROW_Y, + true, + std::slice::from_ref(&entry), + None, + ); assert_eq!( action, @@ -554,10 +647,10 @@ mod tests { #[test] fn close_button_click_emits_close_only_in_shell() { assert_eq!( - action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), + action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None), Some(SystemProfileAction::CloseShell) ); - assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), None); + assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None), None); } #[test] @@ -571,7 +664,7 @@ mod tests { #[test] fn home_render_produces_shell_ui_packet_commands() { - let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[]); + let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[], None); assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. }))); assert!(packet.commands.iter().any(|command| { @@ -579,6 +672,123 @@ mod tests { })); } + #[test] + fn resident_game_indicator_uses_only_resident_game_sessions() { + let mut os = SystemOS::new(None); + assert_eq!(resident_game_indicator(&mut os), None); + + let shell = os.sessions().create_vm_shell_task(7, "VM Shell"); + assert_eq!(resident_game_indicator(&mut os), None); + os.lifecycle().suspend_task(shell).expect("shell suspend should succeed"); + assert_eq!(resident_game_indicator(&mut os), None); + + let game = os.sessions().create_vm_game_task(42, "Stress Console"); + os.lifecycle().suspend_task(game).expect("game suspend should succeed"); + + assert_eq!( + resident_game_indicator(&mut os), + Some(SuspendedGameIndicator { title: "Stress Console".to_string() }) + ); + } + + #[test] + fn home_render_shows_suspended_game_kill_control_when_game_is_resident() { + let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() }; + let packet = render_home_packet( + SUSPENDED_GAME_KILL_CONTROL.x, + SUSPENDED_GAME_KILL_CONTROL.y, + &[], + Some(&suspended_game), + ); + + assert!(packet.commands.iter().any(|command| { + matches!( + command, + GfxUiCommand::FillRect { rect, color } + if *rect == SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE + ) + })); + assert!(packet.commands.iter().any(|command| { + matches!( + command, + GfxUiCommand::DrawText { x, y, text, .. } + if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8 + && *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 + && text == "Stress Console" + ) + })); + } + + #[test] + fn home_render_omits_suspended_game_kill_control_without_resident_game() { + let packet = render_home_packet( + SUSPENDED_GAME_KILL_CONTROL.x, + SUSPENDED_GAME_KILL_CONTROL.y, + &[], + None, + ); + + assert!(!packet.commands.iter().any(|command| { + matches!( + command, + GfxUiCommand::FillRect { rect, color } + if *rect == SUSPENDED_GAME_KILL_CONTROL + && (*color == COLOR_KILL || *color == COLOR_KILL_ACTIVE) + ) + })); + } + + #[test] + fn suspended_game_kill_control_click_emits_kill_action() { + let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() }; + + assert_eq!( + action_for_click( + false, + SUSPENDED_GAME_KILL_CONTROL.x, + SUSPENDED_GAME_KILL_CONTROL.y, + true, + &[], + Some(&suspended_game), + ), + Some(SystemProfileAction::KillResidentGame) + ); + assert_eq!( + action_for_click( + false, + SUSPENDED_GAME_KILL_CONTROL.x, + SUSPENDED_GAME_KILL_CONTROL.y, + true, + &[], + None, + ), + None + ); + } + + #[test] + fn suspended_game_kill_control_truncates_long_titles() { + let long_title = + SuspendedGameIndicator { title: "Extremely Long Resident Game Title".to_string() }; + let packet = render_home_packet(0, 0, &[], Some(&long_title)); + let label = packet + .commands + .iter() + .find_map(|command| match command { + GfxUiCommand::DrawText { x, y, text, .. } + if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8 + && *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 => + { + Some(text) + } + _ => None, + }) + .expect("kill control text should render"); + + assert!(label.ends_with("..")); + assert!(label.chars().count() <= ((SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize); + } + #[test] fn shell_close_button_sits_inside_shell_frame() { assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y)); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 09a11462..bd44cb2b 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]} {"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} {"type":"discussion","id":"DSC-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/plans/PLN-0163-render-hub-suspended-game-kill-control.md b/discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md index 22ecdfc1..e4d0a707 100644 --- 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 @@ -2,7 +2,7 @@ id: PLN-0163 ticket: hub-suspended-game-kill-affordance title: Render Hub Suspended Game Kill Control -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0040] tags: [hub, lifecycle, game, ui] -- 2.47.2 From 3aec829c70ce3a52dcb2c0c1036f1945396cbdda Mon Sep 17 00:00:00 2001 From: bQUARKz <bquarkz@gmail.com> Date: Sun, 5 Jul 2026 09:41:01 +0100 Subject: [PATCH 11/13] implements PLN-0165 --- .../src/firmware/firmware.rs | 43 +++++++++++++++-- crates/console/prometeu-system/src/lib.rs | 5 +- .../prometeu-system/src/programs/mod.rs | 5 +- .../src/programs/prometeu_hub/mod.rs | 5 +- .../src/programs/prometeu_hub/prometeu_hub.rs | 46 +++++++++---------- discussion/index.ndjson | 2 +- ...-0165-validate-hub-kill-flow-end-to-end.md | 2 +- 7 files changed, 76 insertions(+), 32 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 3f56f8c1..37ddb1fa 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -266,8 +266,8 @@ mod tests { use prometeu_system::task::{TaskId, TaskState}; use prometeu_system::windows::WindowOwner; use prometeu_system::{ - CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState, - discover_games_root, + CrashReport, ForegroundOwner, GameLifecycleEventKind, HUB_SUSPENDED_GAME_KILL_CONTROL, + ResidentGameState, discover_games_root, }; use std::fs; use std::path::PathBuf; @@ -479,6 +479,12 @@ mod tests { windows.set_focus(id); } + fn front_buffer_pixel(platform: &TestPlatform, x: i32, y: i32) -> u32 { + const FRAMEBUFFER_WIDTH: usize = 480; + let index = x as usize + y as usize * FRAMEBUFFER_WIDTH; + platform.local_hardware().gfx.front_buffer()[index] + } + #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); @@ -1123,12 +1129,26 @@ mod tests { assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task)); - let kill_click = - InputSignals { f_signal: true, x_pos: 300, y_pos: 240, ..Default::default() }; + let kill_sample_x = HUB_SUSPENDED_GAME_KILL_CONTROL.x + 2; + let kill_sample_y = HUB_SUSPENDED_GAME_KILL_CONTROL.y + 2; + assert_eq!( + front_buffer_pixel(&platform, kill_sample_x, kill_sample_y), + Color::rgb(164, 32, 40).raw(), + "Hub should render the suspended-game kill control before click" + ); + + let kill_click = InputSignals { + f_signal: true, + x_pos: HUB_SUSPENDED_GAME_KILL_CONTROL.x, + y_pos: HUB_SUSPENDED_GAME_KILL_CONTROL.y, + ..Default::default() + }; firmware.tick(&kill_click, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub); assert_eq!(firmware.os.lifecycle().resident_game_task(), None); + assert_eq!(firmware.os.lifecycle().resident_game(), None); assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed)); assert_eq!( firmware.os.lifecycle().process_state_for_task(stress_task), @@ -1139,6 +1159,12 @@ mod tests { None ); assert!(!firmware.game_lifecycle_audio_paused()); + assert!(!firmware.resume_resident_game_from_home()); + + let ownership = firmware.os.vm().active_render_ownership(); + assert_eq!(ownership.app_mode, AppMode::Shell); + assert_eq!(ownership.app_id, 0); + assert_eq!(platform.local_hardware().frame_composer.active_scene_id(), None); assert_eq!( platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id, None @@ -1147,6 +1173,8 @@ mod tests { platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id, None ); + assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none()); + assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none()); assert!( firmware.os.recent_logs(32).iter().any(|event| { event.level == LogLevel::Info @@ -1155,6 +1183,13 @@ mod tests { }), "Hub kill control should use the canonical manual kill log" ); + + firmware.tick(&InputSignals::default(), &mut platform); + assert_eq!( + front_buffer_pixel(&platform, kill_sample_x, kill_sample_y), + Color::rgb(12, 14, 22).raw(), + "Hub should stop rendering the kill control after the game is gone" + ); } #[test] diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 4ef78157..f06c5ada 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -8,7 +8,10 @@ pub use os::{ DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError, SystemOS, }; -pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use programs::{ + HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction, + SystemProfileUpdate, +}; pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink}; pub use services::async_work::{ AsyncWorkActiveJob, AsyncWorkCancelToken, AsyncWorkJobContext, AsyncWorkJobId, diff --git a/crates/console/prometeu-system/src/programs/mod.rs b/crates/console/prometeu-system/src/programs/mod.rs index 916c4fb1..fcb844e1 100644 --- a/crates/console/prometeu-system/src/programs/mod.rs +++ b/crates/console/prometeu-system/src/programs/mod.rs @@ -1,3 +1,6 @@ mod prometeu_hub; -pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use prometeu_hub::{ + HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction, + SystemProfileUpdate, +}; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs index 21d08e2d..fc06b2b8 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs @@ -1,4 +1,7 @@ #[allow(clippy::module_inception)] mod prometeu_hub; -pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; +pub use prometeu_hub::{ + HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction, + SystemProfileUpdate, +}; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 10ed3418..f9eeee34 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -20,7 +20,7 @@ const GAME_ROW_W: i32 = 336; const GAME_ROW_H: i32 = 22; const GAME_ROW_GAP: i32 = 6; const MAX_HOME_GAME_ROWS: usize = 3; -const SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 }; +pub const HUB_SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 }; #[cfg(test)] const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 }; @@ -255,7 +255,7 @@ fn action_for_click( return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell); } - if suspended_game.is_some() && rect_contains(SUSPENDED_GAME_KILL_CONTROL, x, y) { + if suspended_game.is_some() && rect_contains(HUB_SUSPENDED_GAME_KILL_CONTROL, x, y) { return Some(SystemProfileAction::KillResidentGame); } @@ -357,7 +357,7 @@ fn render_home_packet( } if let Some(suspended_game) = suspended_game { - let hovered = rect_contains(SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y); + let hovered = rect_contains(HUB_SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y); draw_suspended_game_kill_control(&mut commands, suspended_game, hovered); } @@ -470,13 +470,13 @@ fn draw_suspended_game_kill_control( ) { let fill = if hovered { COLOR_KILL_ACTIVE } else { COLOR_KILL }; let border = if hovered { COLOR_HILITE } else { COLOR_BORDER }; - fill_rect(commands, SUSPENDED_GAME_KILL_CONTROL, fill); - draw_rect(commands, SUSPENDED_GAME_KILL_CONTROL, border); + fill_rect(commands, HUB_SUSPENDED_GAME_KILL_CONTROL, fill); + draw_rect(commands, HUB_SUSPENDED_GAME_KILL_CONTROL, border); draw_buffer_text( commands, - SUSPENDED_GAME_KILL_CONTROL.x + 8, - SUSPENDED_GAME_KILL_CONTROL.y + 8, - &fit_text_to_control(&suspended_game.title, SUSPENDED_GAME_KILL_CONTROL.w - 16), + HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8, + HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8, + &fit_text_to_control(&suspended_game.title, HUB_SUSPENDED_GAME_KILL_CONTROL.w - 16), COLOR_TEXT, ); } @@ -695,8 +695,8 @@ mod tests { fn home_render_shows_suspended_game_kill_control_when_game_is_resident() { let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() }; let packet = render_home_packet( - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, &[], Some(&suspended_game), ); @@ -705,15 +705,15 @@ mod tests { matches!( command, GfxUiCommand::FillRect { rect, color } - if *rect == SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE + if *rect == HUB_SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE ) })); assert!(packet.commands.iter().any(|command| { matches!( command, GfxUiCommand::DrawText { x, y, text, .. } - if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8 - && *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 + if *x == HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8 + && *y == HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8 && text == "Stress Console" ) })); @@ -722,8 +722,8 @@ mod tests { #[test] fn home_render_omits_suspended_game_kill_control_without_resident_game() { let packet = render_home_packet( - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, &[], None, ); @@ -732,7 +732,7 @@ mod tests { matches!( command, GfxUiCommand::FillRect { rect, color } - if *rect == SUSPENDED_GAME_KILL_CONTROL + if *rect == HUB_SUSPENDED_GAME_KILL_CONTROL && (*color == COLOR_KILL || *color == COLOR_KILL_ACTIVE) ) })); @@ -745,8 +745,8 @@ mod tests { assert_eq!( action_for_click( false, - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, true, &[], Some(&suspended_game), @@ -756,8 +756,8 @@ mod tests { assert_eq!( action_for_click( false, - SUSPENDED_GAME_KILL_CONTROL.x, - SUSPENDED_GAME_KILL_CONTROL.y, + HUB_SUSPENDED_GAME_KILL_CONTROL.x, + HUB_SUSPENDED_GAME_KILL_CONTROL.y, true, &[], None, @@ -776,8 +776,8 @@ mod tests { .iter() .find_map(|command| match command { GfxUiCommand::DrawText { x, y, text, .. } - if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8 - && *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 => + if *x == HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8 + && *y == HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8 => { Some(text) } @@ -786,7 +786,7 @@ mod tests { .expect("kill control text should render"); assert!(label.ends_with("..")); - assert!(label.chars().count() <= ((SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize); + assert!(label.chars().count() <= ((HUB_SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize); } #[test] diff --git a/discussion/index.ndjson b/discussion/index.ndjson index bd44cb2b..d1e7e8fd 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]} {"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} 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 index 8eba055b..1286688b 100644 --- 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 @@ -2,7 +2,7 @@ id: PLN-0165 ticket: hub-suspended-game-kill-affordance title: Validate Hub Kill Flow End To End -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0040] tags: [hub, lifecycle, game, ui] -- 2.47.2 From 49cb16b6cbb7f6fb1e22427edb568b119fd5326d Mon Sep 17 00:00:00 2001 From: bQUARKz <bquarkz@gmail.com> Date: Sun, 5 Jul 2026 09:42:57 +0100 Subject: [PATCH 12/13] implements PLN-0166 --- discussion/index.ndjson | 2 +- ...hub-kill-contract-spec-and-lesson-hooks.md | 2 +- .../runtime/09-events-and-concurrency.md | 8 ++++ .../12-firmware-pos-and-prometeuhub.md | 40 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d1e7e8fd..daa693f8 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"done","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/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 index 1372728e..cff2b5f4 100644 --- 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 @@ -2,7 +2,7 @@ id: PLN-0166 ticket: hub-suspended-game-kill-affordance title: Publish Hub Kill Contract Spec And Lesson Hooks -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0040] tags: [hub, lifecycle, game, ui] diff --git a/docs/specs/runtime/09-events-and-concurrency.md b/docs/specs/runtime/09-events-and-concurrency.md index 055b7fdd..133b8839 100644 --- a/docs/specs/runtime/09-events-and-concurrency.md +++ b/docs/specs/runtime/09-events-and-concurrency.md @@ -174,6 +174,14 @@ foreground Game. The new Game MUST receive a fresh VM session. Failure while loading or initializing the replacement Game MUST return the machine to Hub/Home and MUST NOT restore the previous Game in v1. +Manual Hub termination of a suspended resident Game has the same session +removal authority as destructive Game replacement, except that no replacement +Game is started. SystemOS MUST remove the Game from execution eligibility and +resumable state, close its task, stop its process, remove its VM session, clear +task-scoped pending lifecycle delivery, and keep Hub/Home as the foreground +owner. After manual Hub termination, resume queries MUST report no resident +Game. A later launch of the same cartridge is a fresh Game session. + ## 9 Render Worker Concurrency The render worker is not a machine-visible event source and does not introduce diff --git a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md index bda74dc3..c78311fa 100644 --- a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md +++ b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md @@ -55,6 +55,8 @@ The VM does not own the machine lifecycle. Firmware does. frame pacing. - **VM session**: the POS-owned mutable execution context for one VM-backed process. It contains the VM instance and per-session runtime state. +- **Hub resident Game kill control**: a Hub/Home affordance that terminates the + current resident Game instead of resuming it. ## 3 POS Responsibilities @@ -89,6 +91,8 @@ PrometeuHub is responsible for: - presenting the host-provided local games library when `--games-root <dir>` is configured; - emitting system-owned launch actions for selected Home entries. +- presenting a resident Game kill control while Hub/Home owns foreground and a + Game is resident/suspended. The Hub does not execute bytecode directly. It always delegates execution setup to POS. @@ -219,6 +223,17 @@ It preserves Game VM state but is not execution-eligible except for the bounded pause handoff and later foreground resume. A VM-backed Shell has a separate VM session. A native Shell has no VM session. +When Hub/Home is foreground and a Game cartridge remains resident/suspended, +PrometeuHub MUST render a red control in the bottom-right of the Hub viewport. +The control MUST display only the resident Game title. It MUST NOT be rendered +for no resident Game, Shell tasks, System tasks, or native Shell tasks. + +Clicking the Hub resident Game kill control is a system-owned immediate +termination request. Firmware MUST keep the user in Hub/Home, MUST NOT ask for +confirmation in v1, and MUST NOT resume the killed Game on subsequent Home +actions. The control MUST disappear on the next Hub frame after the resident +Game is no longer present. + ## 8 Cartridge Load Flow Current high-level flow: @@ -257,6 +272,28 @@ before closing the old Game when such an event exists. Replacement MUST NOT block on that event in v1. VM-backed Shell loading creates a distinct Shell session and must not overwrite resident Game VM state. +Manual Hub resident Game kill and destructive different-Game replacement MUST +share the canonical resident-Game termination path. That path MUST: + +- close the resident Game task; +- mark its process stopped; +- remove its VM session; +- clear resident Game state and pending Game lifecycle delivery for that task; +- keep or return foreground ownership to Hub/Home; +- transition render ownership to Shell/Hub (`AppMode::Shell`, `app_id = 0`); +- clear the Game lifecycle audio pause state; +- unbind the Game2D frame composer scene; +- shut down cartridge-scoped asset manager state, including slot identity and + physical glyph/scene/sound banks. + +After this operation, the killed Game MUST NOT be resumable. Launching that +same Game again from Home is a fresh cartridge load and a fresh VM session. +Manual Hub kill MUST emit an INFO Hub log with the message shape: + +```text +Resident game killed from Hub: <title> +``` + POS selects the cartridge and its execution context, but the cartridge's initial callable is not chosen by firmware metadata. Execution starts from the cartridge boot protocol defined in [`13-cartridge.md`](13-cartridge.md), currently `func_id = 0`. When Home launches a discovered Game entry, the request is a system-owned action @@ -341,6 +378,9 @@ Expected behavior: resident Game session; - returning to the same Game resumes it through the foreground lifecycle and waits for current-epoch Game rendering before visual Game presentation. +- clicking the red bottom-right resident Game kill control terminates the + resident Game, keeps Home foreground, clears runtime/cartridge residue, and + removes the resume path. ## 13 Relationship to Other Specs -- 2.47.2 From de53eb5ad72e7ff57db3d0e9b94cafb20d60a6be Mon Sep 17 00:00:00 2001 From: bQUARKz <bquarkz@gmail.com> Date: Sun, 5 Jul 2026 16:27:05 +0100 Subject: [PATCH 13/13] Hub Suspended Game Kill Affordance --- discussion/index.ndjson | 6 +- ...s-lifecycle-replacement-not-loader-work.md | 67 +++++++ ...ill-must-share-game-termination-cleanup.md | 72 +++++++ ...-systemos-cartridge-switch-orchestrator.md | 178 ------------------ ...0047-hub-suspended-game-kill-affordance.md | 91 --------- ...emos-game-cartridge-switch-orchestrator.md | 174 ----------------- ...0040-hub-suspended-game-kill-affordance.md | 73 ------- ...ce-systemos-game-switch-target-contract.md | 86 --------- ...sident-game-before-switching-cartridges.md | 98 ---------- ...cover-failed-game-switches-back-to-home.md | 86 --------- ...-dummy-boy-visual-switch-test-cartridge.md | 90 --------- ...ess-to-dummy-boy-game-switch-end-to-end.md | 93 --------- ...tch-contract-and-direct-boot-separation.md | 93 --------- ...-render-hub-suspended-game-kill-control.md | 85 --------- ...pose-resident-game-termination-contract.md | 93 --------- ...-0165-validate-hub-kill-flow-end-to-end.md | 92 --------- ...hub-kill-contract-spec-and-lesson-hooks.md | 78 -------- 17 files changed, 142 insertions(+), 1413 deletions(-) create mode 100644 discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md create mode 100644 discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md delete mode 100644 discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md delete mode 100644 discussion/workflow/agendas/AGD-0047-hub-suspended-game-kill-affordance.md delete mode 100644 discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md delete mode 100644 discussion/workflow/decisions/DEC-0040-hub-suspended-game-kill-affordance.md delete mode 100644 discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md delete mode 100644 discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md delete mode 100644 discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md delete mode 100644 discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md delete mode 100644 discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md delete mode 100644 discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md delete mode 100644 discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md delete mode 100644 discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md delete mode 100644 discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md delete mode 100644 discussion/workflow/plans/PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index daa693f8..6e4ca8a2 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ -{"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"done","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":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":55,"CLSN":1}} +{"type":"discussion","id":"DSC-0044","status":"done","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":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0054","file":"discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} +{"type":"discussion","id":"DSC-0043","status":"done","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":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} {"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-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md b/discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md new file mode 100644 index 00000000..c5f2525b --- /dev/null +++ b/discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md @@ -0,0 +1,67 @@ +--- +id: LSN-0053 +ticket: system-os-cartridge-switch-orchestrator +title: Game Switching Is Lifecycle Replacement, Not Loader Work +created: 2026-07-05 +tags: [runtime, os, lifecycle, game, cartridge] +--- + +## Context + +PROMETEU supports two superficially similar flows: + +- `Game -> Home -> same Game`, which is pause/suspend/resume. +- `Game A -> Home -> Game B`, which is destructive replacement. + +The second flow is not a loader feature. It is a SystemOS lifecycle operation +that happens to use the cartridge loader after the old resident Game has been +removed from resumable state. + +## Key Decisions + +### SystemOS Game Cartridge Switch Orchestrator + +**What:** A different-Game launch from Home terminates the previous resident +Game before loading the new cartridge. The new Game receives a fresh VM session. + +**Why:** Reusing or preserving the old Game while the new Game loads creates a +rollback problem across VM state, handles, assets, input, audio, render +ownership, debug state, and lifecycle delivery. V1 does not need that +transactional model. + +**Trade-offs:** Failed replacement returns to Home and does not restore the old +Game. This is intentionally simpler than snapshot/rollback and makes state +ownership visible in tests. + +## Patterns and Algorithms + +Use a resolved SystemOS switch target, not a guest syscall, for Home launches. +The target carries cartridge path and identity, while SystemOS owns the policy: + +1. detect whether the selected Game matches the resident `app_id`; +2. resume if it is the same Game; +3. terminate the old resident Game if it is different; +4. load the new cartridge through the normal loader; +5. create and initialize a fresh VM session; +6. return to Home on replacement failure without restoring the previous Game. + +The loader stays a materializer. It does not close tasks, remove sessions, +repair foreground state, or own failure recovery. + +## Pitfalls + +- Treating a different Game as a resume path leaks VM execution state. +- Letting the loader own lifecycle cleanup hides policy in the wrong layer. +- Keeping the old Game as rollback state requires correctness across assets, + render ownership, input barriers, audio, open handles, and debugger state. +- A switch test with only one cartridge cannot prove identity replacement. + +## Takeaways + +- Same-Game Home selection is resume; different-Game Home selection is + destructive replacement. +- SystemOS owns cartridge switching policy; the cartridge loader only loads. +- Replacement failure should be a clean Home state, not a rollback to the old + Game. +- End-to-end switch tests need visually distinct cartridges to catch stale + session or stale frame reuse. diff --git a/discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md b/discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md new file mode 100644 index 00000000..b82c5aa7 --- /dev/null +++ b/discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md @@ -0,0 +1,72 @@ +--- +id: LSN-0054 +ticket: hub-suspended-game-kill-affordance +title: Manual Hub Kill Must Share Game Termination Cleanup +created: 2026-07-05 +tags: [hub, lifecycle, game, ui, assets] +--- + +## Context + +After a Game returns to Home, the Game can remain resident and suspended so it +can be resumed later. That state needs a visible, direct way to terminate the +resident Game without launching another cartridge just to force replacement. + +The Hub kill affordance solved the UX gap, but the durable engineering lesson is +the cleanup boundary: manual kill must not become a second, partial termination +path. + +## Key Decisions + +### Hub Suspended Game Kill Affordance + +**What:** Hub renders a red bottom-right control when a Game is resident and +suspended. The control shows only the Game title. Clicking it immediately +terminates the resident Game and keeps the user in Hub. + +**Why:** The user can distinguish "resume this Game" from "discard this resident +Game" without starting a different cartridge. Reusing the canonical termination +path prevents a repeat of stale scene/glyph/asset state bugs. + +**Trade-offs:** V1 has no confirmation step. The destructive nature is carried +by placement, color, and the fact that the target Game is already out of +foreground. + +## Patterns and Algorithms + +Manual Hub kill and destructive Game replacement should call the same resident +Game termination helper. That helper is responsible for the whole runtime +surface: + +- close the Game task; +- stop the process; +- remove the VM session; +- clear resident Game state and task-scoped lifecycle delivery; +- restore Shell/Hub render ownership; +- clear lifecycle audio pause state; +- unbind the Game2D composer scene; +- shut down cartridge-scoped asset manager state and physical banks; +- emit explicit Hub/POS observability for manual kill. + +The Hub itself should emit a high-level action such as `KillResidentGame`; it +should not know how to clean runtime subsystems. + +## Pitfalls + +- A UI-only kill that removes a task but leaves assets or composer binding alive + can still fail later when another Game renders. +- A cleanup-only helper without an end-to-end Hub click test can miss the + interaction between hitbox, foreground ownership, render owner, and no-resume. +- Hard-coded click coordinates in tests make the UI contract harder to maintain; + expose the layout hitbox when tests need to exercise it. + +## Takeaways + +- Visible system controls should produce system actions, not direct subsystem + mutation from UI code. +- Manual kill and Game replacement are variants of the same resident Game + termination contract. +- Cleanup tests should prove both logical state removal and visible-frame + behavior: the control appears before kill and disappears after kill. +- "No resume after kill" is as important as "task closed" because resumability + is the user-facing state. diff --git a/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md b/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md deleted file mode 100644 index 476cc33b..00000000 --- a/discussion/workflow/agendas/AGD-0044-systemos-cartridge-switch-orchestrator.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -id: AGD-0044 -ticket: system-os-cartridge-switch-orchestrator -title: SystemOS Cartridge Switch Orchestrator -status: accepted -created: 2026-07-03 -resolved: -decision: -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Contexto - -A `AGD-0041` cobre o caso `Game -> Home/Shell -> mesmo Game`: o usuario sai do -foreground do jogo, o SO assume a tela, Shell/Hub pode rodar, e depois o mesmo -Game e retomado. - -Existe um segundo caso, diferente, que nao deve ser escondido dentro da -`AGD-0041`: `Game A -> Home -> Game B`. - -Nesse fluxo, o usuario esta com um Game rodando, aperta Home, volta para o SO e -pede para iniciar outro jogo. O SystemOS nao deve simplesmente chamar um loader -por cima do estado atual. Ele precisa orquestrar a substituicao do app corrente: -fechar ou descartar o Game residente, limpar estado cartridge-scoped, resolver o -novo alvo, carregar o proximo cartucho e entrar no novo estado de firmware. - -Essa agenda tambem se relaciona com a `AGD-0003 / system-run-cart`: `run_cart`, -se existir, nao e userland e nao e o orquestrador. O carregador de cartucho e -apenas uma etapa dentro de uma transicao comandada pelo SystemOS. - -## Problema - -Sem um orquestrador explicito de troca de cartucho no SystemOS, a plataforma -pode cair em estados incoerentes: - -- carregar um novo Game sem fechar corretamente o Game anterior; -- reaproveitar estado VM, assets, input, audio, render ou telemetria do cartucho - anterior; -- misturar boot direto por linha de comando com troca de app sob o SO; -- deixar o loader decidir lifecycle, quando ele deveria apenas materializar o - cartucho alvo; -- tentar implementar `Home -> outro Game` antes de existir o contrato de - foreground/pausa definido pela `AGD-0041`. - -## Pontos Criticos - -- Esta agenda deve ser executada depois da `AGD-0041`. -- `Game -> Home -> mesmo Game` fica fora desta agenda. -- O SystemOS deve ser a autoridade da troca entre cartuchos sob o SO. -- O loader nao deve possuir politica de fechamento, pausa, foreground ou - cleanup. -- Boot direto por CLI/debug continua sendo caminho separado e mais simples. -- A troca para outro Game deve tratar falha de load sem deixar o sistema em - estado parcialmente substituido. -- `.pmc`, catalogo completo e UX final de launcher continuam fora de escopo, - salvo se forem necessarios para definir o contrato minimo. - -## Opcoes - -### Opcao A - Adiar tudo para depois da AGD-0041 - -- **Abordagem:** manter esta agenda como registro de escopo futuro; primeiro - fechar e implementar o contrato `Game -> Home -> mesmo Game`. -- **Pro:** evita misturar pausa/resume com substituicao de cartucho; reduz risco - de reabrir arquitetura durante a `AGD-0041`. -- **Contra:** a plataforma continua sem troca `Home -> outro Game` ate uma etapa - posterior. -- **Manutenibilidade:** alta, porque cada agenda fecha um eixo de lifecycle. - -### Opcao B - Definir agora o contrato conceitual, implementar depois - -- **Abordagem:** fechar uma decisao curta ja dizendo que o orquestrador pertence - ao SystemOS, que `run_cart` nao e userland, e que a execucao depende da - `AGD-0041`; o plano de codigo fica para depois. -- **Pro:** reduz ambiguidade enquanto trabalhamos na `AGD-0041`. -- **Contra:** pode produzir uma decisao com pouca evidencia operacional antes - do contrato de foreground estar fechado. -- **Manutenibilidade:** boa se a decisao for claramente marcada como dependente - da `AGD-0041`. - -### Opcao C - Implementar o orquestrador completo agora - -- **Abordagem:** tentar resolver `Game A -> Home -> Game B` antes ou junto da - `AGD-0041`. -- **Pro:** ataca o fluxo final diretamente. -- **Contra:** mistura pausa/resume, fechamento, cleanup, resolucao de alvo e - falha de load em uma unica frente; alto risco de contrato instavel. -- **Manutenibilidade:** baixa para v1, porque a troca completa depende do - contrato de foreground ainda aberto. - -## Sugestao / Recomendacao - -`DSC-0041` foi fechada e o contrato de foreground/pausa/resume ja existe. A -recomendacao agora deixa de ser adiar e passa a ser fechar uma decisao curta -para o orquestrador de troca `Game A -> Home -> Game B`. - -O contrato sugerido e: - -- trocar para outro Game e uma substituicao controlada, nao uma pausa/resume; -- o Game residente anterior e fechado/descartado; -- a VM session do Game anterior e removida; -- o novo Game sempre recebe uma VM session nova e zerada; -- se o novo load falhar, o sistema volta ao Home/Hub com diagnostico e nao - tenta preservar o Game anterior em v1; -- o loader continua sendo etapa interna, sem possuir politica de lifecycle; -- a evidencia minima deve usar dois cartuchos distintos, incluindo um cartucho - simplista `dummy-boy` alem do stress test. - -## Perguntas em Aberto - -- [x] Depois da `AGD-0041`, qual estado representa um Game que sera descartado - em vez de retomado? - - Resposta: fechamento/encerramento controlado, nao `PausedSuspended`. O Game - anterior deixa de ser resident/resumable e sua VM session e removida. -- [x] O fechamento do Game atual e observavel para o firmware/game, ou e uma - destruicao interna do SystemOS? - - Resposta: deve ser observavel como uma transicao de lifecycle/firmware, mas - nao cooperativo nem bloqueante para o Game. O SystemOS tem autoridade para - encerrar e descartar a sessao. Pode ser emitido um evento de encerramento - para permitir save/flush cooperativo quando existir esse mecanismo, mas a - troca nao deve depender da resposta do Game. -- [x] Quais estados cartridge-scoped devem ser limpos antes do novo load: VM, - assets, FS handles, input, audio, render ownership, telemetry, logs? - - Resposta: a troca nao reaproveita VM. O Game anterior e encerrado, sua VM - session e descartada, e o novo Game recebe VM/session nova e zerada. Devem - ser limpos ou reatribuídos os estados cartridge-scoped ligados a VM, - handles abertos, input pendente/barreiras, audio do app, render ownership, - telemetry/debug/crash state do app. Logs historicos podem permanecer como - observabilidade. -- [x] Se o novo cartucho falha ao carregar, o sistema volta ao Home, mostra uma - tela de erro, ou tenta preservar o Game anterior? - - Resposta: o Game anterior ja foi encerrado. Se o novo load falha, a sessao - parcial do novo Game e descartada e o sistema volta ao Home/Hub com - diagnostico. Preservar o Game anterior fica fora de escopo em v1. -- [x] Como a resolucao do novo alvo e representada sem depender de catalogo - completo ou `.pmc`? - - Resposta: para o contrato minimo, o alvo pode ser um path/entrada ja - resolvido pelo Hub/game library atual. O contrato nao depende de `.pmc` nem - de catalogo final. -- [x] Qual evidencia minima prova `Game A -> Home -> Game B` sem depender de UX - final de launcher? - - Resposta: criar um cartucho `dummy-boy` ao lado do stress test. O teste deve - provar `Stress -> Home -> Dummy Boy`, com troca de VM session, identidade de - cartridge distinta e render observavel do novo Game. - -## Criterio para Encerrar - -A agenda pode virar decisao quando: - -- a `AGD-0041` tiver fechado o contrato de foreground/pausa/resume; -- estiver claro que o orquestrador pertence ao SystemOS; -- `run_cart`/loader estiver limitado a etapa interna, nao userland; -- existir regra para fechar/descartar o Game atual; -- existir contrato de limpeza de estado cartridge-scoped; -- existir politica para falha de carregamento do novo Game; -- houver criterio de teste minimo para `Game A -> Home -> Game B`. - -## Discussao - -- 2026-07-03: Agenda criada a partir da separacao entre `AGD-0041` - (`Game -> Home -> mesmo Game`) e `AGD-0003` (`run_cart`/loader nao userland). - O novo escopo cobre a troca posterior `Game A -> Home -> Game B`, com - orquestracao pelo SystemOS depois que o contrato de foreground estiver - fechado. -- 2026-07-05: Apos fechamento de `DSC-0041`, a discussao convergiu para troca - destrutiva simples em v1: ao iniciar outro Game, o Game residente anterior e - encerrado, sua VM session e removida, e o novo Game recebe VM session nova. - Falha de load volta ao Home/Hub com diagnostico. A evidencia minima deve - incluir um novo cartucho `dummy-boy` que desenha um quadrado com cores - oscilando, alem do stress test. -- 2026-07-05: Refinamento: o encerramento do Game anterior pode emitir evento - cooperativo para save/flush, mas esse evento nao e uma permissao nem uma - barreira obrigatoria. O SystemOS continua com autoridade para concluir a troca - e remover a VM session anterior. - -## Resolution - -Pronta para virar decisao, pendente confirmacao explicita do usuario. 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 deleted file mode 100644 index 650796cb..00000000 --- a/discussion/workflow/agendas/AGD-0047-hub-suspended-game-kill-affordance.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -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-0039-systemos-game-cartridge-switch-orchestrator.md b/discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md deleted file mode 100644 index 387a9399..00000000 --- a/discussion/workflow/decisions/DEC-0039-systemos-game-cartridge-switch-orchestrator.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -id: DEC-0039 -ticket: system-os-cartridge-switch-orchestrator -title: SystemOS Game Cartridge Switch Orchestrator -status: accepted -created: 2026-07-05 -ref_agenda: AGD-0044 -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Status - -Open for review. - -## Contexto - -`DEC-0037` fechou o contrato `Game -> Home/Shell -> mesmo Game`: o Game pode -ficar residente, ser pausado/suspenso pelo SystemOS, e depois ser retomado. - -`DEC-0038` fechou o ownership de VM sessions: VM-backed mutable execution state -pertence a sessoes de VM associadas a processos/tarefas, nao a uma VM global do -firmware. - -Esta decision cobre outro fluxo: `Game A -> Home -> Game B`. Nesse caso, o -usuario nao esta retomando o mesmo Game. O usuario esta escolhendo substituir o -Game residente por outro cartucho. Essa substituicao precisa ser orquestrada -pelo SystemOS, nao pelo loader e nao por userland. - -## Decisao - -PROMETEU MUST tratar `Game A -> Home -> Game B` como uma substituicao -controlada de Game, nao como pause/resume. - -Quando o usuario escolher iniciar um Game diferente enquanto ja existe um Game -residente, SystemOS MUST encerrar o Game residente anterior, remover sua -resident state e descartar sua VM session antes de ativar o novo Game como -resident/foreground. - -O Game anterior MAY receber um evento cooperativo de encerramento para permitir -save/flush quando esse mecanismo existir. Esse evento MUST NOT ser uma -permissao, veto ou barreira obrigatoria para a troca. SystemOS MUST manter -autoridade para concluir a troca e remover a VM session anterior mesmo se o -Game nao reagir. - -O novo Game MUST receber uma VM session nova e zerada. A troca MUST NOT -reutilizar a VM session, PC, stack, heap, globals, open handles, cartridge -identity, lifecycle state, debug state ou runtime state do Game anterior. - -O loader/materializador de cartridge MUST permanecer uma etapa interna de -carregamento. Ele MUST NOT possuir politica de lifecycle, foreground, -close/discard, cleanup ou recuperacao de falha. - -Se o novo cartucho falhar ao carregar, SystemOS MUST descartar qualquer session -parcial ou estado cartridge-scoped parcial do novo Game e retornar ao Home/Hub -com diagnostico. V1 MUST NOT tentar preservar ou restaurar o Game anterior -depois que a troca tiver comecado. - -Boot direto por CLI/debug permanece um caminho separado e mais simples. Esta -decision governa troca de Game sob SystemOS depois que o usuario retorna ao -Home/Hub. - -A evidencia minima de implementacao MUST incluir dois test cartridges -distintos: o stress cartridge existente e um novo cartridge `dummy-boy`. -`dummy-boy` MUST renderizar um quadrado visualmente distinto com cor oscilante -para que os testes provem que o segundo Game, e nao um frame/session anterior -stale, esta ativo. - -## Rationale - -Trying to preserve Game A while loading Game B would turn the switch into a -transactional snapshot/rollback problem. That is not needed for v1 and would -pull in VM snapshot correctness, open handle restoration, assets, render -ownership rollback, audio rollback, input state, debugger state, and telemetry -rollback. - -The simpler and safer v1 contract is destructive replacement: - -1. the user requests another Game; -2. SystemOS ends the previous resident Game; -3. SystemOS removes its VM session and cartridge-scoped runtime state; -4. SystemOS attempts to load the new Game into a fresh VM session; -5. success enters the new Game; -6. failure returns to Home/Hub with diagnostic. - -This aligns with `DEC-0037` because pause/suspend remains the same-Game resume -flow. It aligns with `DEC-0038` because VM identity is session-owned and each -Game receives a distinct VM session. - -## Invariantes / Contrato - -- SystemOS is the authority for switching from one Game cartridge to another - under Home/Hub. -- `Game -> Home -> same Game` is pause/resume and remains governed by - `DEC-0037`. -- `Game A -> Home -> Game B` is destructive replacement and is governed by this - decision. -- A Game that is being replaced MUST stop being resident/resumable. -- The previous Game VM session MUST be removed before the new Game becomes the - active resident/foreground Game. -- A cooperative close/terminate/save event MAY be delivered to the previous - Game, but it MUST NOT block replacement. -- The new Game MUST get a fresh VM session. -- The new Game MUST NOT inherit VM execution state, open handles, lifecycle - delivery state, cartridge identity, debug state, telemetry state, or crash - state from the previous Game. -- Render ownership MUST transition through SystemOS ownership policy and stale - submissions from the previous Game MUST NOT be presented after the switch. -- Input pending/barrier state MUST be cleared or made safe across the switch. -- App audio from the previous Game MUST be stopped or detached from the active - app before the new Game becomes foreground. -- Logs MAY remain as historical observability; they are not cartridge execution - state. -- If new Game load fails, Home/Hub is the recovery state for v1. -- V1 does not preserve, restore, or rollback to the previous Game after a - switch begins. -- The launcher target MAY be a path or already-resolved game-library entry for - v1. `.pmc` and final catalog UX are out of scope. -- `run_cart`, if retained, is not a userland command and not the switch - orchestrator. - -## Impactos - -- **Runtime / SystemOS:** needs an explicit Game switch orchestration path that - closes/removes the current resident Game session before loading the next Game. -- **Lifecycle:** needs a controlled replacement transition distinct from - pause/suspend/resume. -- **VM sessions:** previous Game session removal and new Game session creation - are mandatory switch steps. -- **Firmware:** should observe macro states such as Home, loading, load failure, - and GameRunning, but should not own switch policy. -- **Hub / launcher:** should request a switch through SystemOS using a resolved - target; it should not call a loader directly as lifecycle authority. -- **Loader:** remains a materialization step and returns success/failure without - owning cleanup policy. -- **Renderer:** render ownership/stale submission invalidation must prove the - new Game owns current presentation after success. -- **Input/audio:** previous Game input/audio must not leak into the new Game. -- **Tests:** need a second visually distinct test cartridge, `dummy-boy`, to - prove Game identity and render transition. - -## Alternativas Descartadas - -- **Preserve Game A until Game B loads successfully:** rejected for v1 because it - requires rollback semantics across VM, assets, handles, render ownership, - input, audio, debugger, and telemetry. -- **Treat Game A -> Game B as pause/resume:** rejected because a different Game - should not inherit resumable state from the previous Game. -- **Let the loader close the previous Game:** rejected because loader/materializer - should not own lifecycle policy. -- **Reuse the previous VM session for the new Game:** rejected because it - violates VM session ownership and risks state leakage. -- **Depend on `.pmc` or final catalog UX now:** rejected because the lifecycle - contract can be validated using path/game-library targets first. - -## Referencias - -- Source agenda: `AGD-0044` -- Foreground stack and Game pause contract: `DEC-0037` -- VM session ownership for VM-backed processes: `DEC-0038` -- SystemOS lifecycle/process/task authority: `DSC-0032` -- Launch authority lesson: `LSN-0051` -- Foreground/session ownership lesson: `LSN-0052` - -## Propagacao Necessaria - -- Add/update runtime spec for Game cartridge switching under SystemOS. -- Add implementation plans for: - - SystemOS switch orchestration; - - resident Game termination/session removal; - - fresh session load for the new Game; - - failure recovery to Home/Hub; - - `dummy-boy` test cartridge; - - end-to-end `Stress -> Home -> Dummy Boy` validation. -- Ensure host/debug direct boot remains separate from SystemOS Game switch. 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 deleted file mode 100644 index 2696065e..00000000 --- a/discussion/workflow/decisions/DEC-0040-hub-suspended-game-kill-affordance.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -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 diff --git a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md b/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md deleted file mode 100644 index b8f88cca..00000000 --- a/discussion/workflow/plans/PLN-0157-introduce-systemos-game-switch-target-contract.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: PLN-0157 -ticket: system-os-cartridge-switch-orchestrator -title: Introduce SystemOS Game Switch Target Contract -status: done -created: 2026-07-05 -ref_decisions: [DEC-0039] -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Briefing - -DEC-0039 requires Game-to-Game switching under Home/Hub to be owned by SystemOS. -Before destructive replacement can be implemented, Hub/Firmware/SystemOS need a -small explicit contract for "launch this resolved Game target as a replacement" -that is separate from direct boot and from loader internals. - -## Objective - -Introduce a SystemOS-owned Game switch request/target abstraction that can carry -a resolved cartridge path or game-library entry from Hub/Home into the switch -orchestrator without making the loader or userland the lifecycle authority. - -## Dependencies - -- Source decision: DEC-0039. -- Builds on DEC-0037 and DEC-0038 as already implemented. -- Must be implemented before PLN-0158, PLN-0159, and PLN-0161. - -## Scope - -- Define a minimal Game switch target type, such as `GameSwitchTarget` or - `CartridgeLaunchTarget`, in the SystemOS/Firmware boundary. -- Route Hub game-library launch actions through that target contract instead of - treating a path as an implicit lifecycle operation. -- Keep direct boot/debug boot paths separate from SystemOS Game switching. -- Add tests proving the target preserves cartridge identity/path without - starting lifecycle replacement by itself. - -## Non-Goals - -- Do not implement destructive replacement in this plan. -- Do not add `.pmc`, final catalog UX, or a new launcher UI. -- Do not change direct CLI/debug boot behavior. -- Do not add background execution. - -## Execution Method - -1. Inspect `crates/console/prometeu-system/src/services/game_library.rs`, - `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`, - and `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`. -2. Add a minimal launch target model at the SystemOS/Firmware boundary: - - resolved cartridge path; - - optional app_id/title metadata if already available; - - no loader-owned lifecycle policy. -3. Update Hub action plumbing so `SystemProfileAction::LaunchGame` carries the - resolved target type or maps immediately into it at the firmware boundary. -4. Add a placeholder SystemOS/Firmware entry point for Game switching that - accepts the target but still delegates to the existing load path until - PLN-0158 changes replacement behavior. -5. Add unit tests for target construction and path propagation. -6. Keep direct `BootTarget::Cartridge` behavior unchanged. - -## Acceptance Criteria - -- There is an explicit type or function contract representing a resolved - SystemOS Game switch target. -- Hub/Home no longer treats a raw path as lifecycle authority. -- The loader remains a materialization step, not the switch orchestrator. -- Direct boot/debug boot code paths remain separate and behaviorally unchanged. -- Tests prove target identity is carried without implicitly closing or loading a - Game. - -## Tests - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-host-desktop-winit` -- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings` - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/game_library.rs` -- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md b/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md deleted file mode 100644 index ae4c957f..00000000 --- a/discussion/workflow/plans/PLN-0158-terminate-resident-game-before-switching-cartridges.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -id: PLN-0158 -ticket: system-os-cartridge-switch-orchestrator -title: Terminate Resident Game Before Switching Cartridges -status: done -created: 2026-07-05 -ref_decisions: [DEC-0039] -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Briefing - -DEC-0039 defines `Game A -> Home -> Game B` as destructive replacement. The -current same-Game resume flow keeps the resident Game session alive; switching -to a different Game must instead close/remove that resident session and create a -fresh session for the new Game. - -## Objective - -Implement the SystemOS lifecycle/session operation that terminates the current -resident Game before loading a different Game cartridge. - -## Dependencies - -- Source decision: DEC-0039. -- Requires PLN-0157 target contract. -- Should be implemented before PLN-0159 and PLN-0161. - -## Scope - -- Add a controlled resident Game termination/replacement operation to SystemOS - lifecycle/session services. -- Optionally emit a cooperative terminate/close/save lifecycle event when the - current event model can carry it, but do not block replacement on it. -- Remove the previous Game from resident/resumable state. -- Remove the previous Game VM session before activating the new Game. -- Clear or reassign app-scoped state that cannot leak into the new Game: - session VM, open handles, pending lifecycle state, input barrier/pending - input, audio active app state, render ownership, telemetry/debug/crash state. - -## Non-Goals - -- Do not preserve Game A for rollback. -- Do not implement failed-load recovery beyond surfacing errors; that is - PLN-0159. -- Do not add background execution or multiple resident Games. -- Do not build final launcher UX. - -## Execution Method - -1. Inspect lifecycle/session state in: - - `crates/console/prometeu-system/src/os/lifecycle.rs`; - - `crates/console/prometeu-system/src/os/facades/sessions.rs`; - - `crates/console/prometeu-system/src/services/foreground`; - - `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`; - - `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`. -2. Add a `replace_resident_game` or equivalent operation that: - - finds the current resident Game; - - marks its task/process closed or stopped according to existing lifecycle - semantics; - - removes the resident Game entry from foreground stack/lifecycle state; - - removes its VM session. -3. Ensure same-Game resume still bypasses replacement and remains governed by - DEC-0037. -4. Update game launch from Home so launching a different app_id triggers - replacement before load. -5. Ensure render ownership transitions away from the old Game before the new - Game becomes foreground. -6. Add focused tests for resident Game termination and session removal. - -## Acceptance Criteria - -- Launching a different Game from Home closes/removes the resident Game rather - than resuming it. -- The previous Game VM session is absent before the new Game is made active. -- Same-Game selection still resumes the existing resident Game. -- A cooperative close/terminate event, if added, is non-blocking. -- No previous Game VM, handle, lifecycle, render, or debug state is reused by - the new Game. - -## Tests - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-firmware` -- `cargo clippy -p prometeu-system -p prometeu-firmware --all-targets -- -D warnings` -- Add tests for: - - different Game launch removes previous resident session; - - same Game launch resumes existing resident session; - - old Game task/process state is closed/stopped; - - old Game session cannot be resolved after replacement starts. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/os/lifecycle.rs` -- `crates/console/prometeu-system/src/os/facades/sessions.rs` -- `crates/console/prometeu-system/src/services/foreground` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` diff --git a/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md b/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md deleted file mode 100644 index 560a08c4..00000000 --- a/discussion/workflow/plans/PLN-0159-recover-failed-game-switches-back-to-home.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: PLN-0159 -ticket: system-os-cartridge-switch-orchestrator -title: Recover Failed Game Switches Back To Home -status: done -created: 2026-07-05 -ref_decisions: [DEC-0039] -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Briefing - -DEC-0039 rejects preserving Game A after a switch begins. If Game B fails to -load, the system must discard partial Game B state and return to Home/Hub with a -diagnostic, not attempt rollback to Game A. - -## Objective - -Implement controlled failed-switch recovery to Home/Hub after destructive Game -replacement begins. - -## Dependencies - -- Source decision: DEC-0039. -- Requires PLN-0157 target contract. -- Requires PLN-0158 resident Game termination/session removal. - -## Scope - -- Detect load/init failure while switching to a different Game. -- Discard any partial new Game task/session/window/render/input/audio state. -- Return firmware/SystemOS to Home/Hub. -- Log or expose a diagnostic that Home can surface or tests can assert. -- Ensure previous Game is not restored after switch failure. - -## Non-Goals - -- Do not implement transactional rollback. -- Do not preserve the previous Game after switch begins. -- Do not design final user-facing error UI. -- Do not change direct boot failure behavior unless needed to keep paths - separate. - -## Execution Method - -1. Inspect existing load failure flow in - `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`. -2. Add a switch-specific failure outcome or context flag that distinguishes - SystemOS Game switching from direct boot load. -3. On switch load failure: - - remove partial new VM session/task/process if created; - - clear partial cartridge-scoped state; - - transition to `HubHome` instead of app crash when appropriate for switch - recovery; - - log a diagnostic containing target identity/path and failure summary. -4. Preserve existing direct boot and explicit load failure tests unless a direct - boot path intentionally still goes to crash. -5. Add tests for bad Game B after Game A was already terminated. - -## Acceptance Criteria - -- Failed `Game A -> Home -> Broken Game B` returns to Home/Hub. -- Previous Game A is not resident, resumable, or restored. -- Partial Game B VM session/task/process state is removed or closed. -- A diagnostic is logged or otherwise observable. -- Existing direct boot/debug boot failure semantics remain separate. - -## Tests - -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-host-desktop-winit` -- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings` -- Add tests for: - - switch failure returns Home; - - old Game not restored; - - partial new session cleanup; - - direct boot failure path remains unchanged. - -## Affected Artifacts - -- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/console/prometeu-system/src/os/facades/sessions.rs` -- `crates/console/prometeu-system/src/os/lifecycle.rs` diff --git a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md deleted file mode 100644 index 80b2ce06..00000000 --- a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -id: PLN-0160 -ticket: system-os-cartridge-switch-orchestrator -title: Add Dummy Boy Visual Switch Test Cartridge -status: done -created: 2026-07-05 -ref_decisions: [DEC-0039] -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Briefing - -DEC-0039 requires minimum evidence using two distinct test cartridges. The -existing stress cartridge proves execution/logging but is not visually distinct -enough for switch validation. A second cartridge, `dummy-boy`, must render an -oscillating-color square. - -## Objective - -Add a simple `test-cartridges/dummy-boy` cartridge that renders a visible square -with changing colors so Game switching can prove visual/session identity. - -## Dependencies - -- Source decision: DEC-0039. -- Can be implemented independently of PLN-0157 through PLN-0159. -- Required before PLN-0161 end-to-end validation. - -## Scope - -- Add `test-cartridges/dummy-boy/manifest.json`. -- Generate or add `test-cartridges/dummy-boy/program.pbx`. -- Add empty or minimal assets payload if the loader requires `assets.pa`. -- Implement Dummy Boy behavior: - - Game cartridge identity distinct from stress; - - deterministic visible square; - - color oscillates across frames; - - no dependency on final launcher UX. -- Add build/generation documentation or tests so the cartridge can be - regenerated safely if needed. - -## Non-Goals - -- Do not add final game content. -- Do not add `.pmc`. -- Do not depend on host screenshots for the first validation unless an existing - harness already supports it. -- Do not change stress cartridge behavior. - -## Execution Method - -1. Inspect the stress cartridge and any PBX generator tooling: - - `test-cartridges/stress-console`; - - `crates/tools/pbxgen-stress`; - - bytecode assembler helpers in `crates/console/prometeu-bytecode`. -2. Choose the smallest maintainable generator path: - - extend existing PBX generator if appropriate; or - - add a focused `pbxgen-dummy-boy` tool; or - - add deterministic generated PBX with source instructions. -3. Create `manifest.json` with unique `app_id`, title `Dummy Boy`, Game mode, - and version. -4. Generate `program.pbx` that renders the square and advances color over - frames. -5. Add tests or smoke checks that the cartridge is discoverable by games-root - discovery. - -## Acceptance Criteria - -- `test-cartridges/dummy-boy` exists and loads as a Game cartridge. -- Dummy Boy has a unique `app_id` and title distinct from Stress. -- Dummy Boy renders a visible square. -- Square color changes deterministically over time. -- Games-root discovery sees both Stress and Dummy Boy. -- Existing stress cartridge remains unchanged. - -## Tests - -- `cargo test -p prometeu-hal` -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-firmware` -- Run any cartridge generator tests added by this plan. -- Add or update tests for games-root discovery including Dummy Boy. - -## Affected Artifacts - -- `test-cartridges/dummy-boy/manifest.json` -- `test-cartridges/dummy-boy/program.pbx` -- `test-cartridges/dummy-boy/assets.pa` if required -- `crates/tools/*` cartridge generator tooling if needed -- `crates/console/prometeu-system/src/services/game_library.rs` tests if needed diff --git a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md b/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md deleted file mode 100644 index 110b9565..00000000 --- a/discussion/workflow/plans/PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: PLN-0161 -ticket: system-os-cartridge-switch-orchestrator -title: Validate Stress To Dummy Boy Game Switch End To End -status: done -created: 2026-07-05 -ref_decisions: [DEC-0039] -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Briefing - -DEC-0039 requires proof that `Game A -> Home -> Game B` creates a fresh Game -session and does not present stale state from Game A. The minimum evidence is -switching from Stress to Dummy Boy. - -## Objective - -Add end-to-end validation for `Stress -> Home -> Dummy Boy` through the real -Hub/Home flow, proving lifecycle, VM session, cartridge identity, and render -ownership switch correctly. - -## Dependencies - -- Source decision: DEC-0039. -- Requires PLN-0157 target contract. -- Requires PLN-0158 destructive replacement. -- Requires PLN-0159 failed-switch recovery for negative path assertions. -- Requires PLN-0160 Dummy Boy cartridge. - -## Scope - -- Add firmware/system tests that drive: - - launch Stress from games root; - - request Home; - - launch Dummy Boy; - - assert Stress session is gone; - - assert Dummy Boy session is active; - - assert cartridge identity and render ownership belong to Dummy Boy. -- Add a negative end-to-end test for broken Game B returning to Home if not - already covered by PLN-0159. -- Use existing test platform/render submission inspection where possible. - -## Non-Goals - -- Do not require final launcher UX. -- Do not add screenshot-based host automation unless existing test utilities - make it cheap and deterministic. -- Do not implement additional Game-to-Game variants beyond Stress to Dummy Boy. - -## Execution Method - -1. Inspect existing firmware tests in - `crates/console/prometeu-firmware/src/firmware/firmware.rs`. -2. Reuse existing `TestGameRoot` or games-root helpers to expose Stress and - Dummy Boy. -3. Drive input through Hub/Home selection rather than calling cartridge load - directly when possible. -4. Assert lifecycle/session invariants: - - old task closed/stopped; - - old VM session removed; - - new VM session created; - - foreground owner is new Game; - - current cartridge identity is Dummy Boy. -5. Assert render invariants: - - render ownership epoch belongs to Dummy Boy app identity; - - stale Stress submissions are not accepted after switch; - - latest observable Game frame comes from Dummy Boy when render inspection is - available. -6. Add a focused test for failure path if missing after PLN-0159. - -## Acceptance Criteria - -- End-to-end test proves Stress can be replaced by Dummy Boy from Home. -- Stress is not resumable after switching to Dummy Boy. -- Dummy Boy receives a fresh VM session. -- Cartridge identity and active render ownership are Dummy Boy after switch. -- Failure path returns Home and does not restore Stress. -- Tests are deterministic and do not rely on manual host interaction. - -## Tests - -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-host-desktop-winit` -- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings` - -## Affected Artifacts - -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/console/prometeu-system/src/services/game_library.rs` -- `test-cartridges/dummy-boy` -- Host/render test utilities if needed diff --git a/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md b/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md deleted file mode 100644 index af057ec9..00000000 --- a/discussion/workflow/plans/PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: PLN-0162 -ticket: system-os-cartridge-switch-orchestrator -title: Specify Game Switch Contract And Direct Boot Separation -status: done -created: 2026-07-05 -ref_decisions: [DEC-0039] -tags: [runtime, os, lifecycle, game, cartridge, architecture] ---- - -## Briefing - -DEC-0039 must be reflected in the canonical runtime specs after the operational -contract is implemented or alongside implementation. The spec must distinguish -same-Game resume, Game replacement under SystemOS, and direct boot/debug boot. - -## Objective - -Update the canonical runtime documentation to describe SystemOS Game cartridge -switching and to keep direct boot/debug boot separate from Home/Hub-driven Game -replacement. - -## Dependencies - -- Source decision: DEC-0039. -- Can be implemented before code if the spec is written as intended behavior. -- Should be reconciled after PLN-0157 through PLN-0161 if implementation names - differ from initial plan names. - -## Scope - -- Update relevant files under `docs/specs/runtime/`, especially: - - `12-firmware-pos-and-prometeuhub.md`; - - `13-cartridge.md`; - - `14-boot-profiles.md`; - - `09-events-and-concurrency.md` if lifecycle events need mention; - - `10-debug-inspection-and-profiling.md` if debugger separation needs note. -- Document: - - SystemOS authority for Game switching; - - destructive replacement semantics; - - optional non-blocking terminate/save event; - - fresh VM session for new Game; - - failed switch recovery to Home/Hub; - - direct boot/debug boot separation; - - test cartridge evidence requirement. - -## Non-Goals - -- Do not document `.pmc` or final catalog UX as required. -- Do not specify background execution. -- Do not revise DEC-0037 or DEC-0038 semantics. -- Do not write lesson material; housekeeping will decide lessons after - execution. - -## Execution Method - -1. Read the runtime spec files listed in scope. -2. Add a Game switching section in the most appropriate canonical spec file. -3. Cross-reference existing Home/Hub, cartridge, boot profile, and debug boot - sections. -4. Ensure language is English and normative enough for future implementers: - `MUST`, `MUST NOT`, `MAY`, `SHOULD`. -5. Verify no non-English agenda language leaks into specs. -6. Run docs grep checks for the newly introduced terms. - -## Acceptance Criteria - -- Specs explicitly distinguish: - - same-Game pause/resume; - - Game A to Game B destructive replacement; - - direct boot/debug boot. -- Specs state that SystemOS owns Game switching policy. -- Specs state that a new Game gets a fresh VM session. -- Specs state that failed switch returns Home/Hub and does not restore the old - Game in v1. -- Specs mention Dummy Boy or equivalent two-cartridge evidence only as test - evidence, not as runtime feature. -- Documentation is English. - -## Tests - -- `rg -n "Game switch|destructive replacement|direct boot|Dummy Boy|VM session" docs/specs/runtime` -- `discussion validate` -- Run code tests only if this plan is implemented together with code changes: - `cargo test -p prometeu-system -p prometeu-firmware` - -## Affected Artifacts - -- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` -- `docs/specs/runtime/13-cartridge.md` -- `docs/specs/runtime/14-boot-profiles.md` -- `docs/specs/runtime/09-events-and-concurrency.md` -- `docs/specs/runtime/10-debug-inspection-and-profiling.md` 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 deleted file mode 100644 index e4d0a707..00000000 --- a/discussion/workflow/plans/PLN-0163-render-hub-suspended-game-kill-control.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -id: PLN-0163 -ticket: hub-suspended-game-kill-affordance -title: Render Hub Suspended Game Kill Control -status: done -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 deleted file mode 100644 index e16d0535..00000000 --- a/discussion/workflow/plans/PLN-0164-expose-resident-game-termination-contract.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: PLN-0164 -ticket: hub-suspended-game-kill-affordance -title: Expose Resident Game Termination Contract -status: done -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 deleted file mode 100644 index 1286688b..00000000 --- a/discussion/workflow/plans/PLN-0165-validate-hub-kill-flow-end-to-end.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -id: PLN-0165 -ticket: hub-suspended-game-kill-affordance -title: Validate Hub Kill Flow End To End -status: done -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 deleted file mode 100644 index cff2b5f4..00000000 --- a/discussion/workflow/plans/PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: PLN-0166 -ticket: hub-suspended-game-kill-affordance -title: Publish Hub Kill Contract Spec And Lesson Hooks -status: done -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 -- 2.47.2