From 126bffa37d7d4e0b4079bf18f9af9d16702ba6f8 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Jul 2026 22:52:08 +0100 Subject: [PATCH 01/29] Foreground Stack, Game Pause, and VM-Backed Shell Coexistence --- discussion/index.ndjson | 6 +- ...51-launch-authority-belongs-to-systemos.md | 118 ++++++++++ .../agendas/AGD-0003-system-run-cart.md | 202 ------------------ ...ground-stack-game-pause-shell-vm-backed.md | 78 +++++-- ...-systemos-library-root-home-game-launch.md | 161 -------------- ...ystem-run-cart-and-preserve-direct-boot.md | 136 ------------ ...ystemos-games-root-and-home-game-launch.md | 156 -------------- ...oreground-stack-and-game-pause-contract.md | 191 +++++++++++++++++ ...29-remove-userland-run-cart-abi-surface.md | 90 -------- ...0-remove-runtime-run-cart-stub-dispatch.md | 79 ------- ...preserve-direct-cartridge-boot-coverage.md | 83 ------- ...specify-games-root-home-launch-contract.md | 103 --------- ...-implement-games-root-library-discovery.md | 118 ---------- ...LN-0134-wire-home-game-list-launch-flow.md | 104 --------- ...5-validate-games-root-launch-end-to-end.md | 99 --------- ...te-desktop-home-key-outside-guest-input.md | 96 +++++++++ ...plement-systemos-foreground-stack-state.md | 116 ++++++++++ ...cify-foreground-pause-and-home-contract.md | 114 ++++++++++ ...alidate-game-home-shell-game-end-to-end.md | 106 +++++++++ ...eliver-game-pause-resume-and-suspension.md | 110 ++++++++++ ...render-audio-and-input-pause-boundaries.md | 104 +++++++++ 21 files changed, 1023 insertions(+), 1347 deletions(-) create mode 100644 discussion/lessons/DSC-0004-system-run-cart/LSN-0051-launch-authority-belongs-to-systemos.md delete mode 100644 discussion/workflow/agendas/AGD-0003-system-run-cart.md delete mode 100644 discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md delete mode 100644 discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md delete mode 100644 discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md create mode 100644 discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md delete mode 100644 discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md delete mode 100644 discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md delete mode 100644 discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md delete mode 100644 discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md delete mode 100644 discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md delete mode 100644 discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md delete mode 100644 discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md create mode 100644 discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md create mode 100644 discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md create mode 100644 discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md create mode 100644 discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md create mode 100644 discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md create mode 100644 discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 48b236ba..1779eddc 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":37,"PLN":136,"LSN":51,"CLSN":1}} +{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":38,"PLN":142,"LSN":52,"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":"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":"open","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-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} @@ -14,7 +14,7 @@ {"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} {"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]} {"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0004","status":"done","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0051","file":"discussion/lessons/DSC-0004-system-run-cart/LSN-0051-launch-authority-belongs-to-systemos.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03"}]} {"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} diff --git a/discussion/lessons/DSC-0004-system-run-cart/LSN-0051-launch-authority-belongs-to-systemos.md b/discussion/lessons/DSC-0004-system-run-cart/LSN-0051-launch-authority-belongs-to-systemos.md new file mode 100644 index 00000000..d88d2d7f --- /dev/null +++ b/discussion/lessons/DSC-0004-system-run-cart/LSN-0051-launch-authority-belongs-to-systemos.md @@ -0,0 +1,118 @@ +--- +id: LSN-0051 +ticket: system-run-cart +title: Launch Authority Belongs to SystemOS +created: 2026-07-03 +tags: [runtime, systemos, cartridge, launch, abi, home] +--- + +## Context + +The `system.run_cart` workflow resolved two related questions: whether a guest +should have an app-callable cartridge launch syscall, and how Home should start +games from a local library. + +The final model removes `system.run_cart` from the userland ABI while preserving +direct host-controlled cartridge boot. Home launch is restored as a +system-owned path: the host may provide `--games-root `, SystemOS/Home +discovers valid Game directory cartridges, and selecting an entry routes through +firmware into `LoadCartridge`. + +The important distinction is authority. A launch can be a valid product action +without being a guest capability. + +## Key Decisions + +### Remove Guest Cartridge Launch + +**What:** Prometeu removed `system.run_cart` from the public userland syscall +surface, generated guest metadata, and runtime dispatch. + +**Why:** A guest syscall made cartridge launch look like app-owned navigation. +That gave userland implied authority over target resolution, lifecycle cleanup, +failure policy, and system navigation. + +**Trade-offs:** Obsolete guest code cannot keep a compatibility stub. The +runtime instead keeps direct boot as a host/system entrypoint and lets stale +guest attempts fail through the invalid syscall path. + +### Preserve Direct Boot Outside The Guest ABI + +**What:** Host, CLI, debugger, tests, and single-game flows continue to boot a +selected cartridge directly. + +**Why:** Direct boot is still essential for development, automation, and future +single-game distribution. Those flows start with a host-selected target before +VM execution, so they do not need a guest syscall. + +**Trade-offs:** The loader remains reusable, but it is not lifecycle +orchestration authority. Higher-level transitions must be owned by firmware or +SystemOS. + +### Model Home Launch As A System Action + +**What:** `--games-root ` configures a local games library for Home. V1 +discovery scans immediate child directories, keeps only valid Game cartridges, +and exposes enough internal metadata to launch a selected entry. + +**Why:** Home needs a product path for "choose a game and start it", but that +action belongs to the system profile. SystemOS can present the library and ask +firmware to enter `LoadCartridge` without reintroducing guest-controlled launch. + +**Trade-offs:** V1 intentionally excludes recursive discovery, `.pmc`, non-game +apps, rich catalog metadata, return-to-Home, and game-to-game switching. Those +need separate lifecycle and orchestration decisions. + +## Patterns and Algorithms + +### Authority-Based API Classification + +Before exposing an operation to guest/userland code, classify what authority it +implies. If the operation selects another executable target, changes process +lifecycle, or controls navigation between system-owned modes, it is probably a +SystemOS or firmware action rather than an app syscall. + +### Separate Selection From Loading + +Home owns user selection and presentation. SystemOS owns the launch request. +Firmware owns the transition into `LoadCartridge`. The cartridge loader remains +the mechanism that validates and materializes a cartridge, not the authority +that decides when navigation is allowed. + +### Expandable Internal Catalog Records + +Home can render a small v1 list while the internal entry retains manifest data, +title, app id, app version, cartridge path, and discovery metadata. This keeps +the UI simple without reducing the model to a display-only DTO. + +## Pitfalls + +### Compatibility Stubs Preserve False Contracts + +A syscall that returns success without doing real lifecycle work is worse than +an absent syscall. It teaches callers that the platform supports app-driven +launch while hiding that no complete transition happened. + +### A Product Flow Is Not Automatically A Guest API + +"Home can launch a game" and "a guest can launch a cartridge" are different +claims. Product behavior should be mapped to the component with the correct +authority instead of mirrored into the nearest callable ABI. + +### Generic Library Roots Prematurely Expand Scope + +The first launcher path is a games library, not a marketplace or full app +catalog. Shell apps, System apps, packages, icons, search, and recursive layouts +carry separate contracts and should not be smuggled into the v1 games-root path. + +## Takeaways + +- Cartridge launch is system navigation, not a userland syscall. +- Direct boot can remain host-controlled without appearing in guest metadata. +- Home launch should emit a system action and let firmware enter + `LoadCartridge`. +- Loader reuse does not imply loader ownership of lifecycle policy. +- Keep v1 discovery narrow: immediate child directory cartridges with + `app_mode: Game`. +- Preserve internal catalog metadata even when the Home UI renders only a small + subset. diff --git a/discussion/workflow/agendas/AGD-0003-system-run-cart.md b/discussion/workflow/agendas/AGD-0003-system-run-cart.md deleted file mode 100644 index b0e1b2ad..00000000 --- a/discussion/workflow/agendas/AGD-0003-system-run-cart.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -id: AGD-0003 -ticket: system-run-cart -title: Agenda - System Run Cart -status: accepted -created: 2026-03-27 -resolved: -decision: -tags: [] ---- - -# Agenda - System Run Cart - -## Problema - -`system.run_cart` existe na ABI pública, mas hoje não produz efeito real no runtime. -Isso expõe duas questões: a superfície atual é um placebo e, mesmo quando houver -fluxo real, rodar cartuchos não deve ser uma operação userland. A autoridade -para iniciar cartuchos deve pertencer ao sistema/firmware. - -Isso cria uma falsa promessa de plataforma: - -- o programa consegue declarar e chamar uma operação que aparenta iniciar cartuchos; -- o loader aceita sua existência; -- o runtime retorna sucesso vazio; -- nenhuma troca de cartucho, transição de firmware, validação de alvo ou política de segurança ocorre. - -## Dor - -- A ABI expõe uma capability de sistema que, na prática, nao existe. -- Mesmo se implementada, a operação não deve ser uma syscall userland comum: - apps não devem iniciar cartuchos diretamente. -- O guest pode acreditar que solicitou uma troca de app quando nada aconteceu. -- Isso corrói a confiabilidade da plataforma: a interface pública deixa de ser contrato e vira placebo. -- Qualquer ferramenta, SDK ou documentação construída sobre essa syscall passa a modelar um comportamento inexistente. - -## Alvo da Discussao - -Definir o contrato real da troca de cartucho iniciada pelo sistema do ponto de vista: - -- do guest; -- do runtime; -- do firmware; -- do host/cartridge loader. - -Ao final da discussão, deve ficar claro se a superfície atual: - -- permanece apenas como mecanismo interno do sistema e sai da ABI userland; ou -- permanece na ABI somente como superfície privilegiada impossível de chamar por apps comuns; ou -- sai temporariamente da ABI até existir fluxo fechado. - -Esta discussao nao deve depender do fechamento previo do formato `.pmc`. -Para o primeiro fluxo fechado, `system.run_cart` pode resolver um alvo por -um loader ja disponivel, incluindo cartucho em diretorio ou fixture de teste. -O suporte ao artefato empacotado `.pmc` permanece responsabilidade da -`DSC-0003 / packed-cartridge-loader-pmc` e nao deve bloquear a prova inicial -de troca `Shell/Hub -> Game`. - -Tambem precisa ficar claro que `run_cart`, se existir, nao e o orquestrador da -troca. No boot direto por linha de comando, carregar um cartucho e simples -porque o runtime nasce ja apontado para aquele jogo. Com o SO em execucao, o -caso real e diferente: pode haver um Game residente/rodando, o usuario volta -para Home, e o sistema decide iniciar outro Game. Nesse caso o SO precisa -orquestrar fechamento/suspensao descartada do app atual, limpeza de estado, -resolucao do novo alvo, carregamento e transicao para o proximo estado de -firmware. O carregador de cartucho e apenas uma etapa desse fluxo. - -A direcao preferida e que a orquestracao de troca de cartucho viva no -SystemOS. Porem, essa agenda nao deve capturar o caso `Game -> Home -> mesmo -Game`: esse e o escopo da `AGD-0041`, que precisa fechar foreground, -pausa/suspensao e retorno ao mesmo Game. A troca `Home -> outro Game`, com -fechamento do jogo atual e carregamento de outro cartucho, pode merecer uma -agenda propria depois da `AGD-0041`, derivada do contrato de foreground que ela -fechar. - -## O Que Precisa Ser Definido - -1. Semântica da operação. - O que significa "run cart" quando existe SO: trocar imediatamente, - agendar para o próximo frame, iniciar uma transicao orquestrada pelo - sistema, ou apenas carregar bytes de cartucho como etapa interna? - -2. Origem do alvo. - Como o guest identifica o cartucho alvo: id numérico, nome canônico, path virtual, handle, manifesto, ou nenhum argumento nesta primeira versão? - Para v1, uma opcao aceitavel e limitar a resolucao a alvos conhecidos pelo host/test harness e carregaveis pelo loader de diretorio, deixando `.pmc` fora do caminho critico. - -3. Modelo de segurança. - Nenhum app/userland deve chamar `run_cart`. O sistema/firmware e o caminho - host/CLI/debug devem ser as unicas autoridades para iniciar cartuchos. - Se apps precisarem solicitar navegacao no futuro, isso deve ser uma - superficie nova e mediada pelo sistema. - -4. Ponto de integração. - Onde a transição vive: `VirtualMachineRuntime`, firmware, host, SystemOS, - ou combinação dos três? Para esta agenda, a decisao minima e separar o boot - direto host/CLI/debug da ABI guest. A orquestracao `Home -> outro Game` - pertence a `DSC-0043 / AGD-0044`. - -5. Efeitos observáveis. - O que acontece quando um guest tenta chamar a superficie removida, e como - ferramentas/boot direto continuam carregando cartuchos sem depender de uma - syscall userland? - -6. Contrato de erro. - Remocao da ABI significa erro de build/declaracao, syscall desconhecida, - ou bloqueio em registro/capability ate a limpeza completa das declaracoes? - -## O Que Necessita Para Resolver - -- remocao de `system.run_cart` da ABI userland; -- definicao de que boot direto continua por caminho host/CLI/debug, nao por - syscall guest; -- separacao explicita entre boot direto, loader interno e orquestracao - `Home -> outro Game` da `DSC-0043 / AGD-0044`; -- testes garantindo que guest/userland nao pode declarar/chamar `run_cart`; -- testes garantindo que boot direto de cartucho continua funcional. - -## Fora de Escopo - -- catálogo completo de apps instalados; -- UX final do launcher/hub; -- resolução remota ou download de cartuchos; -- política de marketplace/distribuição. -- obrigatoriedade de boot a partir de `.pmc` no primeiro fluxo funcional. - -## Sugestao / Recomendacao - -Recomendo remover `system.run_cart` da ABI userland para v1. Nao ha necessidade -de compatibilidade com a superficie atual: ela nao deve permanecer como stub, -syscall reservada chamavel por guest, nem sucesso vazio. - -Rodar cartuchos deve ser uma operacao interna do SystemOS/firmware ou do -caminho host/CLI/debug, nunca uma syscall de app. O boot direto deve continuar -existindo porque e o fluxo essencial para testar cartuchos em desenvolvimento e -tambem representa o modo futuro de executar um jogo unico, como em uma -integracao tipo Steam. - -Recomendo tambem separar dois conceitos: - -- **boot direto:** usado por linha de comando/debug, inicia o runtime ja com um - cartucho alvo e pode continuar simples; -- **troca via SO:** fluxo orquestrado pelo SystemOS, que fecha o app - atual quando necessario, limpa estado cartridge-scoped, resolve o proximo - alvo e so entao chama o carregador. - -Essa agenda nao deve implementar o fluxo `Game -> Home -> mesmo Game`; esse -trabalho pertence a `AGD-0041`. Depois dela, a troca `Home -> outro Game` deve -ser aberta ou refinada como discussao propria, porque adiciona fechamento do -jogo atual, limpeza cartridge-scoped, resolucao de novo alvo e falhas de load. - -Se a plataforma precisar de uma ação solicitável por apps no futuro, ela deve -ser modelada separadamente como pedido de navegação mediado pelo sistema, com -nome e erros que não prometam boot direto. - -## Respostas Atuais - -- `system.run_cart` deve ser removido da ABI userland. -- Nao precisamos manter compatibilidade com a superficie atual. -- Nao deve existir syscall publica para app iniciar cartucho. -- Boot direto por linha de comando, host, debug ou fluxo equivalente de jogo - unico deve permanecer. -- O carregamento interno pode continuar usando loader ja disponivel, incluindo - cartucho em diretorio/fixture. -- `.pmc`, catalogo, UX final de launcher e troca `Game A -> Home -> Game B` - ficam fora desta agenda. -- Se no futuro apps precisarem pedir navegacao, isso deve ser uma nova - superficie mediada pelo sistema, nao a volta de `run_cart` userland. - -## Critério de Saida Desta Agenda - -Esta agenda só pode virar PR de implementação quando existir decisão escrita para: - -- retirada da superfície userland atual; -- preservacao explicita do boot direto host/CLI/debug; -- comportamento esperado para declaracao/chamada guest obsoleta; -- fronteira com `AGD-0041` e `AGD-0044`; -- cobertura minima de testes para remocao userland e boot direto. - -## Discussao - -- 2026-07-03: A agenda deve ser aberta antes da `AGD-0041` como habilitadora - de validacao do ciclo `Shell/Hub -> Game`. Ficou definido como premissa de - agenda que o primeiro `system.run_cart` testavel nao depende de `.pmc`; ele - pode usar cartucho em diretorio ou fixture resolvida pelo host/test harness. -- 2026-07-03: Direcao ajustada: `run_cart` nao deve ser uma operacao userland. - A autoridade de iniciar cartuchos deve pertencer ao sistema/firmware; apps - podem, no maximo, solicitar navegacao por uma superficie separada e mediada. -- 2026-07-03: Direcao refinada: `run_cart` simples nao resolve o caso com SO. - Boot direto por linha de comando continua sendo um caminho simples, mas - iniciar outro jogo a partir do Home exige um orquestrador do SO/firmware que - fecha o app atual, limpa estado e so entao carrega o novo cartucho. -- 2026-07-03: Escopo refinado: `Game -> Home -> mesmo Game` pertence a - `AGD-0041`. A agenda atual nao deve absorver esse trabalho. Depois da - `AGD-0041`, a troca `Home -> outro Game` pode ser aberta ou refinada como - agenda propria para o orquestrador de troca de cartucho no SystemOS. -- 2026-07-03: A troca `Game A -> Home -> Game B` foi separada para - `DSC-0043 / AGD-0044`, mantendo esta agenda focada na superficie - `run_cart`/loader e sua remocao ou restricao como operacao userland. -- 2026-07-03: Direcao aceita para esta agenda: remover `system.run_cart` da ABI - userland sem necessidade de compatibilidade, manter boot direto para - CLI/debug/teste de cartuchos e futuro modo de jogo unico, e deixar navegacao - mediada por apps para uma superficie futura separada se ela vier a existir. diff --git a/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md b/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md index 973f74e2..65520de4 100644 --- a/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md +++ b/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md @@ -2,10 +2,10 @@ id: AGD-0041 ticket: foreground-stack-game-pause-shell-vm-backed title: Foreground Stack, Game Pause, and VM-Backed Shell Coexistence -status: open +status: accepted created: 2026-06-05 -resolved: -decision: +resolved: 2026-07-03 +decision: DEC-0037 tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] --- @@ -99,16 +99,49 @@ Isso preserva o ciclo `Game -> Home/Shell -> Game` sem transformar o runtime em ## Perguntas em Aberto -- [ ] Qual e o nome canonico dos estados de Game fora do foreground: `Paused`, `Suspended`, ambos, ou outro? -- [ ] Ao apertar Home, o Game deve receber um evento/interrupt/trap de pausa antes de parar de executar? -- [ ] O Hub e um Shell especial sempre presente ou uma task Shell comum? -- [ ] Um app Shell VM-backed pode substituir o Hub no foreground ou o Hub permanece como owner raiz? -- [ ] Quando o Shell VM-backed fecha, o retorno vai sempre para Hub ou pode voltar diretamente ao Game? -- [ ] O input do Game e drenado/limpo ao pausar e ao retomar? -- [ ] Audio do Game pausa junto com a VM ou tem politica propria? -- [ ] Existe limite de memoria/residencia para manter Game pausado enquanto Shell app roda? -- [ ] O retorno ao Game exige novo frame antes de trocar a superficie, ou a tela pode manter Hub/Shell ate a primeira submission valida? -- [ ] Quais eventos de lifecycle precisam ser visiveis para firmware, OS, VM e host? +- [x] Qual e o nome canonico dos estados de Game fora do foreground: `Paused`, `Suspended`, ambos, ou outro? +- [x] Ao apertar Home, o Game deve receber um evento/interrupt/trap de pausa antes de parar de executar? +- [x] O Hub e um Shell especial sempre presente ou uma task Shell comum? +- [x] Um app Shell VM-backed pode substituir o Hub no foreground ou o Hub permanece como owner raiz? +- [x] Quando o Shell VM-backed fecha, o retorno vai sempre para Hub ou pode voltar diretamente ao Game? +- [x] O input do Game e drenado/limpo ao pausar e ao retomar? +- [x] Audio do Game pausa junto com a VM ou tem politica propria? +- [x] Existe limite de memoria/residencia para manter Game pausado enquanto Shell app roda? +- [x] O retorno ao Game exige novo frame antes de trocar a superficie, ou a tela pode manter Hub/Shell ate a primeira submission valida? +- [x] Quais eventos de lifecycle precisam ser visiveis para firmware, OS, VM e host? + +## Resolucao + +Fica aceito o modelo `Game -> Home/Shell -> mesmo Game` com foreground unico e +residencia limitada: + +- somente um Game pode estar residente por vez; +- somente um Shell pode ser foreground por vez; +- Hub e o Shell root/especial e permanece owner raiz/back-stack; +- um Shell VM-backed pode assumir o foreground visual, mas segue lifecycle de + Shell e nao herda politica de game loop; +- fechar um Shell VM-backed retorna ao Hub em v1; +- background execution real fica fora de escopo; +- no host desktop, `Esc` e a tecla principal para solicitar Home/SystemOS e a + tecla fisica `Home` pode existir como alias; +- a acao Home/SystemOS e interceptada pelo host/SystemOS e nao entra no pad + logico visivel ao Game; +- `Paused` e o estado/evento cooperativo visivel ao Game; +- `Suspended` e o estado operacional imposto pelo OS/scheduler; +- ao sair do foreground, o OS notifica o Game para pausar e concede um budget + curto de reacao; +- se o Game nao reagir ao pause dentro do budget, o OS suspende a VM mesmo + assim; +- ao retornar, o OS reativa a VM, notifica o Game para resume/foreground + restore e permite que o Game sincronize antes de voltar ao gameplay; +- input pendente do Game e drenado/limpo nas fronteiras de pause/resume; +- audio do Game pausa junto com a VM em v1; +- retorno ao Game deve manter Hub/Shell visivel ate a primeira render submission + valida do Game retomado; +- render ownership muda junto com o foreground owner e invalida submissions + antigas via epoch/generation; +- firmware observa transicoes macro; OS observa lifecycle completo; VM observa + pause/resume; host observa owner/render epoch e estado de apresentacao. ## Criterio para Encerrar @@ -124,3 +157,22 @@ A agenda pode ser encerrada quando tivermos uma decisao clara sobre: ## Discussao - 2026-06-05: Agenda criada a partir da `DSC-0040`, quando o caso de Shell VM-backed aberto sobre um Game pausado mostrou que a discussao pertence ao lifecycle/SystemOS, nao apenas ao render worker. +- 2026-07-03: Direcao provisoria aceita para as perguntas 2-10: + Game deve receber evento de pausa com budget curto antes da suspensao; Hub e + Shell root/especial; Shell VM-backed pode assumir foreground visual, mas Hub + permanece como owner raiz/back-stack; fechar Shell VM-backed retorna ao Hub em + v1; input do Game deve ser drenado/limpo em pause/resume; audio do Game pausa + junto com VM em v1; apenas um Game pausado residente e permitido; retorno ao + Game deve manter Hub/Shell visivel ate a primeira submission valida; firmware + ve transicoes macro, OS ve lifecycle completo, VM ve pause/resume, host ve + owner/render epoch e estado de apresentacao. +- 2026-07-03: Pergunta 1 permanece aberta para esclarecer se `Paused` e + `Suspended` sao estados distintos, nomes para camadas diferentes, ou se v1 + deve expor somente um deles. +- 2026-07-03: Direcao proposta para pergunta 1: `Paused` e o estado/evento + cooperativo visivel ao Game; `Suspended` e o estado operacional imposto pelo + OS. Ao sair do foreground, o OS notifica o Game para pausar e concede um + budget curto de reacao; se o Game nao reagir, o OS suspende a VM mesmo assim. + Ao retornar, o OS notifica o Game antes de recoloca-lo no foreground visual, + permitindo sincronizacao/revalidacao. O Game nao controla a suspensao, mas + pode observar `pause`/`resume` para ajustar seu estado interno. diff --git a/discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md b/discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md deleted file mode 100644 index 3f1980f7..00000000 --- a/discussion/workflow/agendas/AGD-0045-systemos-library-root-home-game-launch.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -id: AGD-0045 -ticket: system-run-cart -title: SystemOS Library Root and Home Game Launch -status: accepted -created: 2026-07-03 -resolved: -decision: -tags: [runtime, os, hub, cartridge, launcher, library, game, architecture] ---- - -## Contexto - -A `DEC-0035` fechou que `system.run_cart` nao deve existir como syscall -userland e que boot direto por host/CLI/debug deve permanecer. - -Isso resolveu a autoridade negativa, mas ainda nao resolveu a experiencia -desejada para o SO: apontar o runtime para um diretorio raiz de biblioteca, -fazer a Home listar os cartuchos encontrados ali e permitir que o usuario clique -em um jogo para carrega-lo. - -O objetivo pratico e um "Switch feio": uma lista simples na Home, sem UX final, -sem marketplace e sem catalogo rico, mas capaz de provar que o SO, e nao o -guest, escolhe e carrega um cartucho a partir de uma biblioteca local. - -## Problema - -Hoje temos dois caminhos separados: - -- boot direto por `--run ` funciona e carrega um cartucho unico; -- Hub/Home existe, mas mostra acoes fixas de Shell nativo e nao lista jogos de - uma biblioteca real. - -Sem uma biblioteca de cartuchos visivel para o SO, o usuario nao consegue -iniciar jogos a partir da Home. Isso deixa a agenda `system-run-cart` incompleta -do ponto de vista de produto: removemos o `run_cart` userland, mas ainda falta a -alternativa correta, que e o SO iniciar cartuchos por acao de usuario. - -## Pontos Criticos - -- O root da biblioteca deve ser fornecido pelo host como `--games-root `. -- O scan inicial deve cobrir subdiretorios imediatos com `manifest.json`. -- O loader de diretorio existente deve continuar sendo a autoridade para validar - cartucho completo; a biblioteca pode ler metadados minimos para listagem. -- A Home pode mostrar uma lista feia/clicavel; polimento visual fica fora. -- O clique em um Game deve iniciar `LoadCartridge` pelo firmware/SystemOS, nao - por syscall guest. -- V1 nao precisa tratar retorno de um Game para Home, porque isso pertence a - `AGD-0041`. -- `.pmc`, outros tipos de app, catalogo remoto, marketplace, icones, busca e - metadados ricos ficam fora. - -## Opcoes - -### Opcao A - Biblioteca apenas para listagem - -- **Abordagem:** adicionar `--games-root`, escanear manifestos e mostrar jogos - na Home, mas sem permitir launch ainda. -- **Pro:** baixa complexidade; prova descoberta/catalogo local. -- **Contra:** nao entrega o objetivo principal de carregar jogo pela Home. -- **Manutenibilidade:** boa, mas valor funcional limitado. - -### Opcao B - Home lista biblioteca e carrega Game quando nao ha Game ativo - -- **Abordagem:** adicionar `--games-root`, escanear cartuchos locais, renderizar - lista simples na Home e permitir clique para carregar um Game via - `LoadCartridge`. A primeira versao cobre Home inicial/sem Game residente. -- **Pro:** entrega o fluxo desejado sem abrir troca completa entre jogos. -- **Contra:** exige passar biblioteca/acao selecionada entre host, Hub, - firmware e loader. -- **Manutenibilidade:** boa se a fronteira ficar clara: biblioteca descobre, - Hub escolhe, firmware carrega. - -### Opcao C - Biblioteca mais troca completa entre jogos - -- **Abordagem:** Home lista biblioteca e tambem permite `Game A -> Home -> - Game B` ja no primeiro passo. -- **Pro:** cobre a experiencia final. -- **Contra:** mistura biblioteca com pausa, fechamento, cleanup e falha de load; - depende de `AGD-0041` e `AGD-0044`. -- **Manutenibilidade:** baixa para v1. - -## Sugestao / Recomendacao - -Recomendo a Opcao B. - -O v1 deve permitir: - -- host recebe `--games-root `; -- o SO/Hub recebe uma lista de cartuchos descobertos; -- a Home mostra uma lista simples/clicavel de jogos; -- clicar em um Game carrega o cartucho via firmware `LoadCartridge`; -- boot direto por `--run ` continua separado e intacto; -- nenhum guest/app chama `run_cart`; -- o escopo v1 acontece na Home inicial; retorno de Game para Home fica para - `AGD-0041`, e troca `Game A -> Home -> Game B` fica para `AGD-0044`. - -Detalhes aceitos para v1: - -- scan apenas de subdiretorios imediatos; -- somente cartuchos `app_mode: Game`; -- `.pmc` fica para futuro; -- cartuchos invalidos sao logados e omitidos da lista; -- o modelo interno deve ser uma entry de biblioteca expansivel, nao apenas um - summary de UI. A entry pode manter o manifest carregado em memoria, path - interno necessario para launch e metadados operacionais como instante de - descoberta/instalacao. A UI v1 mostra apenas `title`, `app_id` e - `app_version`, mas futuros metadados ingeridos pela biblioteca poderao ser - usados pelo catalogo e pela propria UI; -- clique carrega imediatamente, sem confirmacao; -- falha de load volta para Home e registra log/erro. - -Essa agenda deve produzir a alternativa correta ao `run_cart` userland: uma -acao de sistema, iniciada pela Home, que carrega cartucho escolhido pelo usuario -a partir de uma biblioteca local. - -## Perguntas em Aberto - -- [x] O argumento deve se chamar `--games-root`. -- [x] A biblioteca deve escanear apenas subdiretorios imediatos. -- [x] A listagem v1 deve incluir apenas `app_mode: Game`. -- [x] O scan deve logar cartuchos invalidos e omiti-los da lista. -- [x] O Hub/SystemOS deve receber uma entry de biblioteca expansivel, capaz de - manter o manifest carregado, path interno para launch e metadados - operacionais como timestamp de descoberta/instalacao. A UI v1 so precisa - mostrar `title`, `app_id` e `app_version`. -- [x] O clique deve carregar imediatamente, sem confirmacao. -- [x] Se um cartucho falha ao carregar, o sistema volta para Home e registra - erro/log. -- [x] Retorno de Game para Home nao pertence a esta agenda; fica para - `AGD-0041`. -- [x] O teste minimo deve provar o fluxo `games-root -> Home list -> click -> - LoadCartridge -> GameRunning`? - -## Criterio para Encerrar - -A agenda pode virar decisao quando estiver claro: - -- nome e semantica do argumento de host para root da biblioteca; -- escopo do scan e metadados minimos; -- fronteira entre biblioteca, Hub, firmware e loader; -- politica v1 para cartuchos invalidos; -- regra v1 para launch quando nao ha Game ativo; -- fronteira explicita com `AGD-0041` e `AGD-0044`; -- testes minimos para discovery e launch pela Home. - -## Discussao - -- 2026-07-03: Agenda criada como continuacao da `DSC-0004` apos a `DEC-0035`. - A decisao anterior removeu `run_cart` userland; esta agenda define o caminho - correto para o SO/Home carregar jogos a partir de uma biblioteca local. -- 2026-07-03: Respostas aceitas: usar `--games-root`, escanear apenas - subdiretorios imediatos, listar somente Games, deixar `.pmc` e outros apps - para depois, logar/omitir invalidos, usar summary com `title`, `app_id` e - `app_version`, carregar imediatamente no clique, e deixar retorno para Home - sob responsabilidade da `AGD-0041`. -- 2026-07-03: Modelo de dados refinado: a biblioteca deve produzir entries - expansíveis, nao somente summaries de UI. Cada entry pode manter manifest em - memoria, path interno de launch e metadados operacionais como timestamp de - descoberta/instalacao; a UI v1 escolhe quais campos mostrar, mas o modelo - deve permitir que metadados futuros sejam ingeridos e usados pelo catalogo/UI. diff --git a/discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md b/discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md deleted file mode 100644 index 5e212b26..00000000 --- a/discussion/workflow/decisions/DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -id: DEC-0035 -ticket: system-run-cart -title: Remove Userland system.run_cart and Preserve Direct Boot -status: accepted -created: 2026-07-03 -ref_agenda: AGD-0003 -tags: [] ---- - -## Status - -Accepted. - -## Contexto - -`system.run_cart` existe na ABI publica, mas nao possui comportamento real de -troca de cartucho. Isso cria uma falsa promessa: um guest pode declarar/chamar -uma operacao que aparenta iniciar outro cartucho, enquanto o runtime retorna -sucesso vazio ou nao executa uma transicao completa. - -A discussao fechou tres separacoes: - -- boot direto por host/CLI/debug e necessario e deve permanecer; -- apps/userland nao devem iniciar cartuchos diretamente; -- troca `Game A -> Home -> Game B` exige um orquestrador de SystemOS proprio, - separado em `DSC-0043 / AGD-0044`. - -`AGD-0041` permanece responsavel por `Game -> Home -> mesmo Game`, foreground, -pausa/suspensao e retorno ao mesmo Game. - -## Decisao - -`system.run_cart` MUST be removed from the userland guest ABI. - -No app, cartridge, Shell app, or other userland guest code MAY directly request -cartridge boot through `system.run_cart`. - -The platform MUST NOT preserve compatibility for the current userland -`system.run_cart` surface. It MUST NOT remain as a callable stub, reserved -guest syscall that returns success, or capability-gated userland escape hatch. - -Direct boot MUST remain supported through host/CLI/debug or equivalent -single-game launch paths. This direct boot path is a host/system entrypoint, -not a guest syscall. - -Internal cartridge loading MAY continue to use the existing loader path, -including directory cartridges and test fixtures. `.pmc` support is not required -by this decision. - -If apps ever need to request navigation in the future, that MUST be designed as -a separate SystemOS-mediated surface with different naming and semantics. It -MUST NOT reintroduce userland `run_cart` as direct cartridge boot. - -## Rationale - -Direct cartridge boot is essential for development, debugging, automated tests, -and future distribution modes where the runtime launches as a single game. That -use case does not require a guest-visible syscall. - -Userland cartridge boot would give app code authority over system navigation, -lifecycle, cleanup, target resolution, and failure policy. Those concerns belong -to SystemOS/firmware and later to the cartridge switch orchestrator, not to a VM -guest syscall. - -Keeping a stub or compatibility surface would preserve the current ambiguity. -The public ABI should not advertise an operation that either does nothing or -looks like direct app-controlled navigation. - -## Invariantes / Contrato - -- Guest/userland MUST NOT expose or call `system.run_cart`. -- The public syscall registry, declarations, generated bindings, and docs MUST - stop presenting `system.run_cart` as an app-callable operation. -- Booting a cartridge directly MUST remain available from host/CLI/debug flows. -- Direct boot MUST enter the runtime with a selected cartridge target; it MUST - NOT depend on a guest syscall issued after VM startup. -- Loader code MAY remain reusable by host/firmware/SystemOS internals. -- Loader code MUST NOT become lifecycle orchestration authority. -- `Game -> Home -> same Game` remains owned by `AGD-0041`. -- `Game A -> Home -> Game B` remains owned by `DSC-0043 / AGD-0044`. -- `.pmc`, app catalog, launcher UX, marketplace/distribution, and full cartridge - replacement under Home are outside this decision. - -## Impactos - -- **Spec / ABI:** remove or retire `system.run_cart` from the userland syscall - surface. Any syscall table, PBS declarations, generated metadata, or docs - must stop advertising it to apps. -- **Runtime / VM:** guest dispatch must no longer treat `system.run_cart` as a - valid userland operation. If obsolete bytecode or declarations still reach the - runtime during migration, they must fail explicitly rather than report success. -- **Firmware / Host:** direct boot remains supported for command-line, - debugger, test harness, and future single-game launch flows. -- **SystemOS:** no new cart-switch orchestrator is required by this decision. - That work is deferred to `DSC-0043 / AGD-0044`. -- **Tooling:** tools must continue to support direct cartridge launch for - development and test workflows, without relying on a guest syscall. -- **Tests:** coverage must prove both sides: userland cannot declare/call - `system.run_cart`, and direct boot of a cartridge still works. - -## Alternativas Descartadas - -- **Implement `system.run_cart` as a normal syscall:** rejected because it gives - userland direct navigation authority and mixes guest execution with system - lifecycle. -- **Keep a compatibility stub:** rejected because no compatibility is required - and a stub preserves the false contract. -- **Make `system.run_cart` privileged but still guest-visible:** rejected for v1 - because it keeps the ambiguous idea that some guest profile may directly boot - cartuchos. -- **Implement full `Home -> another Game` now:** rejected for this decision; it - requires SystemOS orchestration after the `AGD-0041` foreground contract. - -## Referencias - -- Agenda: `AGD-0003` -- Foreground/pause scope: `AGD-0041` -- Future cartridge switch orchestrator: `DSC-0043 / AGD-0044` -- Related lessons: `LSN-0041` SystemOS lifecycle authority, `LSN-0042` - SystemOS service ownership boundary, `LSN-0043` SystemOS domain facades. - -## Propagacao Necessaria - -- Remove `SystemRunCart` from userland ABI declarations and generated syscall - metadata. -- Remove or rewrite runtime dispatch paths that return success for - `system.run_cart`. -- Preserve host/CLI/debug direct boot paths and add regression tests for them. -- Add negative tests for guest/userland declaration or call attempts. -- Update any documentation/spec text that describes `system.run_cart` as - app-callable. - -## Revision Log - -- 2026-07-03: Initial draft from `AGD-0003`. diff --git a/discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md b/discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md deleted file mode 100644 index ed7eda2c..00000000 --- a/discussion/workflow/decisions/DEC-0036-systemos-games-root-and-home-game-launch.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -id: DEC-0036 -ticket: system-run-cart -title: SystemOS Games Root and Home Game Launch -status: accepted -created: 2026-07-03 -ref_agenda: AGD-0045 -tags: [] ---- - -## Status - -Accepted. - -## Contexto - -`DEC-0035` removed `system.run_cart` from the userland guest ABI and preserved -direct host/CLI/debug cartridge boot. That decision intentionally did not -deliver the Home/SO product flow: pointing PROMETEU at a local games directory, -listing available games in Home, and launching a selected game by user action. - -This decision defines the first system-owned replacement for userland -`run_cart`: a local games-root library discovered by the host/SystemOS and -exposed through a simple Home UI. - -## Decisao - -PROMETEU MUST support a host-provided games library root through -`--games-root `. - -The games root is a directory containing game cartridge directories as immediate -children. V1 MUST scan only immediate child directories. Recursive discovery is -out of scope. - -The v1 games library MUST include only cartridges whose manifest declares -`app_mode: Game`. Shell/System apps, `.pmc` packages, remote catalogs, -marketplace/distribution metadata, rich icons, search, and non-game apps are out -of scope. - -The library MUST produce expandable internal entries, not UI-only summaries. -Each entry MUST retain enough internal data to launch the cartridge and to grow -as a catalog model. At minimum, each entry MUST include: - -- loaded manifest data; -- `title`; -- `app_id`; -- `app_version`; -- internal cartridge path; -- operational discovery metadata, including a timestamp or equivalent discovery - marker. - -The Home UI v1 SHOULD display only `title`, `app_id`, and `app_version`. It MAY -hide path and operational metadata. - -Invalid cartridge candidates MUST be logged and omitted from the Home game list. - -Selecting a listed Game in Home MUST immediately request a system-owned launch -through firmware/SystemOS, resulting in `LoadCartridge` for that cartridge. This -MUST NOT use a guest syscall and MUST NOT reintroduce userland `run_cart`. - -If launch fails, the machine MUST return to or remain in Home and record an -error/log. A polished error UI is out of scope. - -Booting directly with `--run ` MUST remain separate and unchanged. - -Returning from a running Game to Home is outside this decision and belongs to -`AGD-0041`. Switching from `Game A -> Home -> Game B` after a game is already -active belongs to `DSC-0043 / AGD-0044`. - -## Rationale - -The previous `run_cart` surface was wrong because it made cartridge launch look -like an app/userland capability. The desired behavior is still valid, but the -authority must live in the system: Home presents available games and the -firmware/SystemOS loads the selected cartridge. - -Using `--games-root` keeps the v1 product model honest: this is a local game -library, not a general app root. Other app types can be introduced later without -forcing the first launcher model to pretend it is a full marketplace. - -Immediate child scanning keeps discovery deterministic and testable. `.pmc` and -recursive layouts can be added after the directory-cartridge path is proven. - -An expandable entry model avoids prematurely freezing the UI shape as the data -model. The UI is intentionally simple, but the catalog model needs room for -future metadata such as package origin, installation/discovery timestamps, rich -presentation fields, and packaged cartridge details. - -## Invariantes / Contrato - -- `--games-root ` is the v1 host argument for a local games library. -- The games root is Game-only in v1. -- Discovery scans immediate child directories only. -- Directory candidates without valid Game cartridge metadata are logged and - omitted. -- Library entries are internal catalog records, not direct UI DTOs. -- The UI v1 displays a simple clickable list and may show only `title`, - `app_id`, and `app_version`. -- The internal entry retains cartridge path and loaded manifest data for launch. -- Home selection is a system action that enters firmware/SystemOS loading. It is - not a guest syscall. -- `--run ` direct boot remains independent from `--games-root`. -- Return-to-Home and game-to-game switching are not implemented by this - decision. - -## Impactos - -- **Host:** add a `--games-root ` argument and pass the resolved root into - runtime/firmware/SystemOS initialization. -- **SystemOS / Hub:** add a local game library model and expose entries to Home. - Home must render a minimal clickable list and emit a system-owned launch - action for a selected entry. -- **Firmware:** consume the Home launch action and transition to - `LoadCartridge` with the selected cartridge path. -- **Loader:** continue to validate complete directory cartridges. Discovery may - read manifest metadata, but full launch still uses the existing loader. -- **Runtime / VM:** no userland ABI is added. No guest syscall participates in - launch. -- **Specs / Docs:** document `--games-root`, Game-only v1 discovery, and the - distinction between direct boot, games library launch, and future game - switching. -- **Tests:** cover discovery, invalid omission/logging, Home action emission, - and the end-to-end path from games root selection to `GameRunning`. - -## Alternativas Descartadas - -- **List only, no launch:** rejected because it does not satisfy the product - goal of starting games from Home. -- **General app root:** rejected for v1 because Shell/System apps need separate - lifecycle and UI contracts. -- **Recursive discovery:** rejected for v1 to keep semantics deterministic. -- **`.pmc` discovery:** rejected for v1 because packed cartridge loading is a - separate open agenda. -- **Game-to-game switch now:** rejected because it depends on foreground and - cartridge-switch orchestration work in `AGD-0041` and `AGD-0044`. - -## Referencias - -- Source agenda: `AGD-0045` -- Userland run-cart removal: `DEC-0035` -- Foreground/Home return scope: `AGD-0041` -- Future cartridge switch orchestration: `DSC-0043 / AGD-0044` - -## Propagacao Necessaria - -- Add or update runtime specs for games-root discovery and Home launch. -- Add host CLI argument plumbing for `--games-root`. -- Add a catalog/library entry model. -- Add Hub/Home list rendering and click handling. -- Add firmware/SystemOS action routing to `LoadCartridge`. -- Add tests for discovery, invalid candidates, action emission, and - `games-root -> Home click -> GameRunning`. - -## Revision Log - -- 2026-07-03: Initial draft from `AGD-0045`. diff --git a/discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md b/discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md new file mode 100644 index 00000000..5e2d8765 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md @@ -0,0 +1,191 @@ +--- +id: DEC-0037 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Foreground Stack and Game Pause Contract +status: accepted +created: 2026-07-03 +ref_agenda: AGD-0041 +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Status + +Accepted. + +## Contexto + +`AGD-0041` defines the minimum lifecycle contract for `Game -> Home/Shell -> +same Game`. + +The issue is not only rendering. The render worker needs a single valid visual +owner, but SystemOS must decide which VMs may exist, which owner receives input, +which owner may tick, what happens to a Game when Home takes foreground, and how +a VM-backed Shell app coexists with a paused Game. + +This decision builds on: + +- `DSC-0031`: runtime mode separation between Game and System/Shell; +- `DSC-0032`: SystemOS lifecycle/process/task authority; +- `DSC-0035`: task-owned Shell windows and Shell liveness; +- `DSC-0036`: Hub/Shell as a lifecycle proving surface; +- `DSC-0040`: render ownership/epoch and stale submission invalidation. + +## Decisao + +PROMETEU MUST use a single-foreground-owner model for v1. + +At most one Game MAY be resident at a time. At most one Shell app MAY be the +foreground visual owner at a time. + +The Hub MUST be treated as the root/special Shell owner for Home. A VM-backed +Shell app MAY replace the Hub as the foreground visual owner, but the Hub +remains the root/back-stack controller. In v1, closing a VM-backed Shell app +MUST return to Hub/Home, not directly to the Game. + +When the user requests Home while a Game is running, SystemOS MUST start a +cooperative pause transition: + +- the Game receives a pause lifecycle notification; +- the Game is given a short bounded budget to react; +- after that budget, SystemOS MAY suspend the Game VM even if the Game did not + cooperate; +- the Game remains resident but MUST NOT receive normal gameplay ticks, normal + game input, or frame pacing while suspended; +- the Shell/Hub foreground owner MAY execute according to Shell lifecycle rules. + +`Paused` is the Game-visible lifecycle state/event. `Suspended` is the +OS/scheduler operational state. A Game may observe pause/resume, but it MUST NOT +control whether the OS suspends or resumes its VM. + +When returning to the same Game, SystemOS MUST reactivate the Game VM and send a +resume or foreground-restore lifecycle notification before restoring visual +foreground. The Game MAY remain internally paused while it synchronizes. The +system MUST keep Hub/Shell visible until the resumed Game produces a valid new +render submission for the current ownership epoch. + +Input pending for the Game MUST be drained or cleared across pause/resume +boundaries. Game audio MUST pause with the Game VM in v1. Independent background +audio policy is out of scope. + +Render ownership MUST move with the foreground owner. Foreground transitions +MUST invalidate stale render submissions through ownership epoch/generation so +old Game frames cannot be presented after Shell takes foreground, and old Shell +frames cannot be presented after the Game resumes. + +On the desktop host, `Esc` MUST be the primary keyboard request for Home/SystemOS. +The physical `Home` key MAY be supported as an alias. This Home request is a +host/SystemOS control action and MUST NOT be exposed as a normal guest pad +button or userland input capability. + +Real background execution, multiple resident Games, multiple foreground Shells, +Game-to-Game switching, Shell app persistence policy, memory eviction policy, +and rich multitasking are out of scope for this decision. + +## Rationale + +The chosen model preserves the expected console-like flow: a Game can be +paused, Home can run, Shell apps can open, and the same Game can resume without +turning the runtime into a general multitasking scheduler. + +Separating `Paused` from `Suspended` keeps authority clear. `Paused` is the +contract the Game can observe and react to. `Suspended` is the OS mechanism that +prevents the paused Game from continuing to consume ticks, input, audio, and +render ownership while another owner is foreground. + +Keeping Hub as root/back-stack prevents VM-backed Shell apps from becoming +independent lifecycle roots. Returning to Hub before resuming Game keeps v1 +navigation deterministic and leaves more complex direct-return behavior for a +future decision. + +Keeping Hub/Shell visible until the Game submits a new valid frame avoids +presenting stale pixels from the previous Game epoch. This aligns lifecycle +state with the render ownership model from `DSC-0040`. + +Using `Esc` as desktop Home makes the control available on compact keyboards. +Supporting the physical `Home` key as an alias is acceptable, but the portable +contract cannot depend on it. + +## Invariantes / Contrato + +- There is exactly one foreground visual owner at a time. +- V1 supports at most one resident Game. +- V1 supports at most one foreground Shell app. +- Hub/Home is the root Shell owner and back-stack controller. +- VM-backed Shell apps follow Shell lifecycle, not Game loop lifecycle. +- A paused Game may remain resident, but a suspended Game does not receive + normal gameplay ticks, normal game input, or frame pacing. +- Pause/resume are cooperative Game-visible lifecycle events. +- Suspend/resume scheduling authority belongs to SystemOS. +- SystemOS may suspend a Game after the pause budget even if the Game does not + cooperate. +- Input is cleared or barriered across Game pause/resume boundaries. +- Game audio pauses with the Game VM in v1. +- Foreground transitions advance render ownership epoch/generation. +- Visual foreground returns to Game only after a valid current-epoch Game render + submission exists. +- Desktop `Esc` is a host/SystemOS Home request; physical `Home` may alias it. +- Home/SystemOS is not a guest pad button and is not userland input. +- Background execution and general multitasking are explicitly excluded. + +## Impactos + +- **Spec:** runtime lifecycle, firmware/Hub, input, and render ownership specs + must document foreground ownership, pause/suspend semantics, Home key mapping, + and stale frame invalidation. +- **Runtime / SystemOS:** SystemOS needs explicit foreground owner, Hub root, + Shell foreground, Game paused, and Game suspended state. +- **VM:** VM execution must support pause/resume lifecycle delivery and OS-owned + suspension of ticks. +- **Firmware:** firmware should observe macro transitions such as GameRunning, + Hub/Home, Shell foreground, pause request, and resume request without owning + every internal OS detail. +- **Host:** host must route desktop `Esc` and optional physical `Home` to a + SystemOS Home request instead of guest pad input. +- **Input:** Game input must be drained or barriered during pause/resume and + Home/SystemOS must stay outside the guest-visible pad ABI. +- **Audio:** v1 audio follows Game pause/suspend and does not continue as + independent background audio. +- **Renderer:** render publication must reject stale submissions after ownership + epoch changes and keep previous foreground visible until the resumed Game + publishes a valid current-epoch frame. +- **Tests:** coverage must prove pause budget behavior, non-foreground VM + non-execution, input clearing, host Home routing, Shell return-to-Hub, and + stale render rejection. + +## Alternativas Descartadas + +- **Terminate Game when entering Shell:** rejected because it makes + `Game -> Shell -> Game` a reload rather than a pause/resume flow. +- **Real background execution now:** rejected because it pulls in scheduler, + budget, energy, audio, input, IO, memory, and persistence policy beyond v1. +- **Multiple resident Games:** rejected for v1 because Game-to-Game switching is + owned by `DSC-0043 / AGD-0044`. +- **Expose Home as guest input:** rejected because Home changes system + foreground authority and must not be handled as ordinary game input. +- **Return directly from Shell app to Game in v1:** rejected to keep Hub as the + deterministic root/back-stack owner. + +## Referencias + +- Source agenda: `AGD-0041` +- Runtime mode separation: `DSC-0031` +- SystemOS lifecycle authority: `DSC-0032` +- Shell task/window lifecycle: `DSC-0035` +- Hub UI direction: `DSC-0036` +- Render ownership and stale submission handling: `DSC-0040` +- Future Game A -> Home -> Game B switching: `DSC-0043 / AGD-0044` + +## Propagacao Necessaria + +- Update canonical runtime specs for foreground ownership and pause/suspend. +- Specify desktop Home request mapping in host/input documentation. +- Add plan(s) for SystemOS foreground stack state and Game pause/suspend. +- Add plan(s) for VM lifecycle event delivery and forced suspension budget. +- Add plan(s) for host `Esc`/`Home` routing outside guest pad input. +- Add plan(s) for render epoch integration during Game/Shell transitions. +- Add tests covering lifecycle, input, host routing, and render publication + invariants. + +## Revision Log + +- 2026-07-03: Initial decision from `AGD-0041`. diff --git a/discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md b/discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md deleted file mode 100644 index 52c071ae..00000000 --- a/discussion/workflow/plans/PLN-0129-remove-userland-run-cart-abi-surface.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -id: PLN-0129 -ticket: system-run-cart -title: Remove Userland Run Cart ABI Surface -status: done -created: 2026-07-03 -ref_decisions: [DEC-0035] -tags: [] ---- - -## Briefing - -`DEC-0035` removes `system.run_cart` from the userland guest ABI. This plan -covers the public ABI, syscall registry/declaration surface, generated metadata, -and documentation/spec propagation. It does not change runtime dispatch behavior -or direct boot tests; those are covered by `PLN-0130` and `PLN-0131`. - -## Objective - -Remove `system.run_cart` from every app-callable/userland ABI surface while -preserving non-guest direct boot entrypoints. - -## Dependencies - -- Source decision: `DEC-0035`. -- Must stay scoped away from `AGD-0041` and `AGD-0044`. -- Coordinate with `PLN-0130` so runtime dispatch does not retain a callable - success path after the declaration is removed. - -## Scope - -- Remove or retire `Syscall::SystemRunCart` from userland-visible syscall - declarations in `crates/console/prometeu-hal/src/syscalls.rs`. -- Remove the `system.run_cart` registry entry from - `crates/console/prometeu-hal/src/syscalls/domains/system.rs` and - `crates/console/prometeu-hal/src/syscalls/registry.rs`, unless implementation - discovers an internal-only registry distinct from guest ABI. -- Remove generated/static bytecode or VM-facing declarations that expose - `system.run_cart`, including known references in - `crates/console/prometeu-bytecode/src/model.rs` and - `crates/console/prometeu-vm/src/virtual_machine.rs` if they include it. -- Update docs/specs that mention `system.run_cart` as app-callable. -- Update `AGD-0004` references only if they become factually misleading after - removal; do not close or reinterpret that agenda. - -## Non-Goals - -- Do not implement `Home -> another Game`. -- Do not add a replacement app navigation syscall. -- Do not implement `.pmc`. -- Do not change direct host/CLI/debug boot behavior. -- Do not add compatibility stubs for obsolete guest code. - -## Execution Method - -1. Search for `SystemRunCart`, `run_cart`, and `"system", "run_cart"` across - `crates/`, `docs/`, and `discussion/`. -2. Remove `system.run_cart` from userland syscall domain declarations and - lookup/name registries. -3. Remove generated/static guest-facing metadata entries that advertise - `system.run_cart`. -4. Update docs/specs to state that cartridges are booted by host/system paths, - not by guest syscall. -5. Compile the workspace or targeted crates to find stale references. - -## Acceptance Criteria - -- No userland ABI declaration advertises `system.run_cart`. -- No generated guest metadata exposes `run_cart` as callable. -- Documentation/spec text no longer describes app-callable cartridge boot. -- Any retained code path is clearly internal-only and is not reachable through - guest syscall lookup. - -## Tests - -- Add or update syscall registry tests to assert `system.run_cart` is absent - from the public/userland registry. -- Run targeted tests for `prometeu-hal`, `prometeu-bytecode`, and - `prometeu-vm` if available. -- Run repository validation commands used for syscall declaration consistency. - -## Affected Artifacts - -- `crates/console/prometeu-hal/src/syscalls.rs` -- `crates/console/prometeu-hal/src/syscalls/domains/system.rs` -- `crates/console/prometeu-hal/src/syscalls/registry.rs` -- `crates/console/prometeu-bytecode/src/model.rs` -- `crates/console/prometeu-vm/src/virtual_machine.rs` -- `docs/specs/runtime/` -- `discussion/workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md` diff --git a/discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md b/discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md deleted file mode 100644 index 71be6a70..00000000 --- a/discussion/workflow/plans/PLN-0130-remove-runtime-run-cart-stub-dispatch.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: PLN-0130 -ticket: system-run-cart -title: Remove Runtime Run Cart Stub Dispatch -status: done -created: 2026-07-03 -ref_decisions: [DEC-0035] -tags: [] ---- - -## Briefing - -`DEC-0035` forbids a callable userland `system.run_cart` stub. This plan covers -runtime and VM dispatch cleanup so obsolete calls cannot silently succeed. -Public ABI removal is handled by `PLN-0129`; direct boot preservation is handled -by `PLN-0131`. - -## Objective - -Remove the runtime no-op dispatch path for `SystemRunCart` and ensure stale or -obsolete userland attempts fail explicitly rather than returning success. - -## Dependencies - -- Source decision: `DEC-0035`. -- Depends on the ABI direction in `PLN-0129`, but can be implemented in the same - PR if the public enum/registry removal and dispatch cleanup move together. - -## Scope - -- Remove the `Syscall::SystemRunCart => return Ok(())` path from - `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs`. -- Update any match arms made unreachable or obsolete by removing - `SystemRunCart`. -- Ensure stale numeric syscall ids or stale bytecode declarations fail with the - repository's existing unknown/invalid syscall behavior. -- Add negative coverage for userland attempts to invoke the removed operation, - using the closest existing VM runtime syscall tests. - -## Non-Goals - -- Do not implement a replacement navigation operation. -- Do not implement the `Home -> another Game` orchestrator. -- Do not alter direct host/CLI/debug cartridge boot paths. -- Do not change `system.has_cart` behavior unless required by compile fallout. - -## Execution Method - -1. Inspect `dispatch.rs` for all `SystemRunCart` match arms. -2. Remove the no-op success path and any unreachable arms tied only to - `SystemRunCart`. -3. If the public enum variant is removed by `PLN-0129`, update exhaustiveness - and registry mapping accordingly. -4. Add or update VM runtime tests that demonstrate `system.run_cart` is not - callable from guest/userland. -5. Run targeted runtime tests and compile checks. - -## Acceptance Criteria - -- There is no runtime path where a guest `system.run_cart` call returns `Ok(())`. -- Stale/obsolete attempts fail explicitly through existing invalid syscall or - declaration validation behavior. -- `system.has_cart` and unrelated system syscalls keep their current behavior. -- Runtime dispatch remains exhaustive and clear after removing `SystemRunCart`. - -## Tests - -- Add a negative test in `crates/console/prometeu-system/src/services/vm_runtime` - for stale userland `run_cart` attempts, if the test harness can express it. -- Run `cargo test` for `prometeu-system` and any impacted syscall crates. -- Run a workspace compile/test command if targeted tests reveal cross-crate enum - fallout. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs` -- `crates/console/prometeu-system/src/services/vm_runtime/tests.rs` -- `crates/console/prometeu-hal/src/syscalls.rs` -- `crates/console/prometeu-hal/src/syscalls/registry.rs` diff --git a/discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md b/discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md deleted file mode 100644 index 75660a8c..00000000 --- a/discussion/workflow/plans/PLN-0131-preserve-direct-cartridge-boot-coverage.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -id: PLN-0131 -ticket: system-run-cart -title: Preserve Direct Cartridge Boot Coverage -status: done -created: 2026-07-03 -ref_decisions: [DEC-0035] -tags: [] ---- - -## Briefing - -`DEC-0035` removes userland `system.run_cart`, but direct cartridge boot remains -mandatory for development, debugging, automated tests, and future single-game -launch scenarios. This plan protects that path while the guest syscall is -removed. - -## Objective - -Preserve and test host/CLI/debug direct cartridge boot without relying on a -guest syscall. - -## Dependencies - -- Source decision: `DEC-0035`. -- Can be implemented after or alongside `PLN-0129` and `PLN-0130`. -- Must not depend on `.pmc`, app catalog, launcher UX, `AGD-0041`, or - `AGD-0044`. - -## Scope - -- Identify direct boot entrypoints in `crates/tools/prometeu-cli/src/main.rs`, - `crates/host/prometeu-host-desktop-winit/src/lib.rs`, - `crates/host/prometeu-host-desktop-winit/src/debugger.rs`, and firmware - cartridge loading code. -- Add or preserve tests showing a cartridge can be loaded directly into - firmware/runtime from host or CLI paths. -- Ensure these paths do not call or require `system.run_cart`. -- Document the distinction between direct boot and guest navigation where - runtime specs describe boot behavior. - -## Non-Goals - -- Do not implement `Home -> another Game`. -- Do not add app-callable navigation. -- Do not require `.pmc`. -- Do not redesign cartridge loader internals beyond what is required to keep - direct boot working. - -## Execution Method - -1. Trace the CLI/debug direct boot path from cartridge path argument through - loader, firmware `load_cartridge`, and runtime initialization. -2. Add regression tests at the lowest practical layer proving direct boot still - reaches `GameRunning` for a valid game cartridge. -3. Add a guard or assertion that direct boot does not depend on `system.run_cart` - metadata. -4. Update docs/specs to describe direct boot as host/system-controlled. -5. Run targeted CLI/firmware/host tests. - -## Acceptance Criteria - -- A valid game cartridge can still boot directly from host/CLI/debug paths. -- Direct boot does not depend on `system.run_cart` being in the guest syscall - registry. -- Existing debugger/handshake metadata that reports cartridge info still works. -- The direct boot behavior is documented as host/system entry, not guest ABI. - -## Tests - -- Add or preserve firmware tests around `load_cartridge` and `GameRunning`. -- Add or preserve host/CLI tests for cartridge path boot when practical. -- Run `cargo test` for `prometeu-firmware`, `prometeu-host-desktop-winit`, and - `prometeu-cli` where available. - -## Affected Artifacts - -- `crates/tools/prometeu-cli/src/main.rs` -- `crates/host/prometeu-host-desktop-winit/src/lib.rs` -- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` -- `docs/specs/runtime/` diff --git a/discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md b/discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md deleted file mode 100644 index 7bd714c1..00000000 --- a/discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -id: PLN-0132 -ticket: system-run-cart -title: Specify Games Root Home Launch Contract -status: done -created: 2026-07-03 -ref_decisions: [DEC-0036] -tags: [] ---- - -## Briefing - -`DEC-0036` establishes a new system-owned game library launch path: -the host receives `--games-root `, SystemOS/Home lists valid Game -directory cartridges from that root, and selecting one starts the cartridge -through firmware loading rather than a guest syscall. - -This plan updates the normative specs before code is changed. - -## Objective - -Document the v1 games-root and Home launch contract in the canonical runtime -specs so implementation plans cannot drift back into userland `run_cart` or -general app-library semantics. - -## Dependencies - -- Source decision: `DEC-0036`. -- Prior decision: `DEC-0035`, which removed userland `system.run_cart` and - preserved direct `--run` boot. -- This plan must be completed before `PLN-0133`, `PLN-0134`, and `PLN-0135`. - -## Scope - -- Update the runtime specs to define `--games-root ` as the v1 local games - library root. -- Specify that discovery scans only immediate child directories. -- Specify that v1 library discovery includes only valid directory cartridges - with `app_mode: Game`. -- Specify that `.pmc`, recursive discovery, Shell/System apps, marketplace - metadata, rich presentation, search, and non-game apps are out of scope. -- Specify the internal library entry contract: loaded manifest data, `title`, - `app_id`, `app_version`, internal cartridge path, and discovery metadata. -- Specify that Home v1 may display only `title`, `app_id`, and `app_version`. -- Specify that selecting a Home game emits a system-owned launch request that - transitions through firmware/SystemOS into `LoadCartridge`. -- Specify that invalid candidates are logged and omitted. -- Specify that launch failure remains or returns to Home and logs the failure. -- Reaffirm that direct `--run ` boot remains independent and unchanged. - -## Non-Goals - -- No Rust implementation. -- No Home UI changes. -- No `.pmc` discovery or packaged-cartridge spec changes. -- No recursive library layout. -- No return-to-Home behavior for a running Game. -- No Game A -> Home -> Game B orchestration. -- No guest syscall or userland ABI addition. - -## Execution Method - -1. Update [docs/specs/runtime/14-boot-profiles.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/14-boot-profiles.md) with: - - direct `--run` boot remaining a single-cartridge boot profile; - - `--games-root ` as a SystemOS/Home library profile; - - the distinction between direct boot, Home launch, and future game-switch - orchestration. -2. Update [docs/specs/runtime/12-firmware-pos-and-prometeuhub.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md) with: - - Home's responsibility to display the local games library; - - the system-owned launch action boundary; - - failure handling that leaves the machine in Home and records an error. -3. Update [docs/specs/runtime/13-cartridge.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/13-cartridge.md) with: - - directory-cartridge discovery requirements for library candidates; - - Game-only filtering by manifest `app_mode`; - - invalid candidate omission and diagnostics. -4. Update [docs/specs/runtime/16-host-abi-and-syscalls.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/docs/specs/runtime/16-host-abi-and-syscalls.md) only if needed to reinforce that no guest syscall owns cartridge launch. -5. Keep all spec text in English. - -## Acceptance Criteria - -- The specs define `--games-root ` and do not describe it as a generic app - root. -- The specs state that v1 scans immediate child directories only. -- The specs state that only valid Game directory cartridges enter the Home list. -- The specs state that Home launch is system-owned and enters `LoadCartridge`. -- The specs preserve `--run ` as a separate direct boot path. -- The specs explicitly exclude `.pmc`, recursive discovery, return-to-Home, and - game-switch orchestration from this implementation slice. -- No spec reintroduces userland `run_cart`. - -## Tests - -- Run `discussion validate`. -- Run targeted text checks for `--games-root`, `LoadCartridge`, and absence of - `system.run_cart` as an active public launch mechanism in edited specs. - -## 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/16-host-abi-and-syscalls.md` -- `discussion/workflow/plans/PLN-0132-specify-games-root-home-launch-contract.md` diff --git a/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md b/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md deleted file mode 100644 index 65bc21f2..00000000 --- a/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -id: PLN-0133 -ticket: system-run-cart -title: Implement Games Root Library Discovery -status: done -created: 2026-07-03 -ref_decisions: [DEC-0036] -tags: [] ---- - -## Briefing - -`DEC-0036` requires a host-provided games root and an expandable internal -library model before Home can render or launch games. This plan implements the -library discovery and host plumbing only; UI launch wiring is handled by -`PLN-0134`. - -## Objective - -Add a tested games-root discovery model that scans immediate child directory -cartridges, filters to valid Game manifests, records diagnostics for invalid -candidates, and carries enough metadata for Home rendering and later launch. - -## Dependencies - -- Source decision: `DEC-0036`. -- `PLN-0132` should be completed first so code follows the documented contract. -- Existing cartridge manifest and directory loader behavior in the console - crates. -- Existing host CLI and `HostRunner` boot setup. - -## Scope - -- Add `--games-root ` to the desktop host CLI. -- Pass the optional games root into the host runner / firmware initialization - path without changing direct `--run` or `--debug` boot behavior. -- Add a SystemOS-owned games library model for directory cartridge discovery. -- Scan only immediate child directories of the root. -- Read and retain manifest data for each valid candidate. -- Include only candidates whose manifest declares `app_mode: Game`. -- Retain internal cartridge path and operational discovery metadata on each - entry. -- Emit diagnostics for invalid, unreadable, or non-Game candidates and omit - them from the returned entries. -- Keep discovery independent from full cartridge launch: discovery reads - metadata; launch still uses the existing loader in `PLN-0134`. - -## Non-Goals - -- No Home list rendering. -- No click handling or `LoadCartridge` transition. -- No `.pmc` discovery. -- No recursive scan. -- No Shell/System app catalog. -- No marketplace metadata, icons, search, sorting UI, or polished errors. -- No new guest syscall. - -## Execution Method - -1. Introduce a library/discovery module in the SystemOS side, preferably under - `crates/console/prometeu-system/src/services/` unless the existing module - layout has a more local owner. -2. Define an internal entry type with at least: - - loaded cartridge manifest data; - - `title`; - - `app_id`; - - `app_version`; - - internal cartridge path; - - discovery metadata, using `SystemTime` or another explicit discovery - marker. -3. Define discovery diagnostics that can be logged by the caller and asserted in - tests. -4. Implement a scanner that: - - accepts an optional root path; - - returns an empty library when no root is configured; - - iterates only immediate child directories; - - parses `manifest.json` using structured manifest types, not ad hoc string - matching; - - filters to `app_mode: Game`; - - sorts entries deterministically for stable Home presentation and tests. -5. Add `--games-root ` to - [crates/host/prometeu-host-desktop-winit/src/lib.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/lib.rs) - and pass the path into - [crates/host/prometeu-host-desktop-winit/src/runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs). -6. Store or expose the discovered library to firmware/SystemOS in a way that - `PLN-0134` can consume without re-scanning inside the Home click path. -7. Add focused unit tests using temporary cartridge roots with: - - one valid Game directory cartridge; - - one valid non-Game cartridge; - - one invalid directory; - - one nested directory that must not be discovered. - -## Acceptance Criteria - -- `--games-root ` is accepted by the desktop host CLI. -- Absence of `--games-root` leaves existing Hub boot behavior unchanged. -- Direct `--run ` and `--debug ` behavior remains unchanged. -- Discovery returns only immediate valid Game directory cartridges. -- Each library entry retains manifest data, path, title, app id, app version, - and discovery metadata. -- Invalid candidates are represented in diagnostics and excluded from entries. -- Discovery does not fully load or boot cartridge programs. - -## Tests - -- Add and run focused games-library discovery tests in the owning console crate. -- Run `cargo test -p prometeu-system` if the library lives in - `prometeu-system`. -- Run `cargo test -p prometeu-host-desktop-winit` for CLI/runner plumbing. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/` -- `crates/console/prometeu-system/src/lib.rs` or module exports as needed -- `crates/host/prometeu-host-desktop-winit/src/lib.rs` -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` -- `discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md` diff --git a/discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md b/discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md deleted file mode 100644 index 001a2bf0..00000000 --- a/discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: PLN-0134 -ticket: system-run-cart -title: Wire Home Game List Launch Flow -status: done -created: 2026-07-03 -ref_decisions: [DEC-0036] -tags: [] ---- - -## Briefing - -After `PLN-0133`, SystemOS can discover a local Game library but Home still -does not present or launch those entries. This plan wires the discovered -library into Prometeu Hub/Home and routes a selected game to firmware -`LoadCartridge`. - -## Objective - -Make Home display a minimal clickable games list and launch the selected Game -through a system-owned firmware/SystemOS path, without reintroducing userland -`run_cart`. - -## Dependencies - -- Source decision: `DEC-0036`. -- `PLN-0132` for documented contract. -- `PLN-0133` for `--games-root` plumbing and the internal library entry model. -- Existing Prometeu Hub Home profile and firmware state transitions. - -## Scope - -- Provide the discovered games library to Prometeu Hub/Home. -- Render a minimal Home list showing `title`, `app_id`, and `app_version`. -- Add Home hit-testing/click handling for game entries. -- Extend the system profile action model with a system-owned game launch action. -- Handle that action in firmware/SystemOS by loading the selected cartridge via - the existing cartridge loader and entering `LoadCartridge`. -- On load failure, log/record the error and keep or return the machine to Home. -- Preserve existing native shell launch behavior. - -## Non-Goals - -- No polished launcher UI. -- No icons, screenshots, search, filtering, or marketplace metadata. -- No return-to-Home while a Game is running. -- No Game A -> Home -> Game B foreground orchestration. -- No `.pmc` launch support. -- No guest syscall. -- No direct-boot behavior changes. - -## Execution Method - -1. Extend the SystemOS/Hub action type currently represented by - `SystemProfileAction::LaunchNativeShell` with a game-launch action that - carries a stable reference to the selected library entry or cartridge path. -2. Update the Prometeu Hub state in - [crates/console/prometeu-system/src/programs/prometeu_hub.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/programs/prometeu_hub.rs) - or its local modules to accept the discovered game entries. -3. Render the Home games list with stable dimensions and simple clickable rows. - Rows must display only `title`, `app_id`, and `app_version` for v1. -4. Add hit-testing so selecting a row emits the game-launch action immediately. -5. Update - [crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs) - to consume the new action. -6. In the firmware action handler: - - call the existing cartridge loader for the selected path; - - on success, return `FirmwareState::LoadCartridge(...)`; - - on failure, log/record the error and remain in the Home path. -7. Ensure shell launch still works exactly as before. -8. Add unit or integration tests for: - - Home list presentation data; - - click-to-action emission; - - firmware action handling success; - - firmware action handling failure. - -## Acceptance Criteria - -- Starting the host with `--games-root ` exposes valid Game entries in - Home. -- The Home list displays `title`, `app_id`, and `app_version`. -- Clicking a listed Game routes through a system action and enters - `LoadCartridge`. -- The launch path does not use a guest syscall and does not mention - `system.run_cart`. -- Invalid or failing launch paths remain in Home and record an error. -- Existing native shell Home actions continue to work. -- Direct `--run` remains separate and unchanged. - -## Tests - -- Run `cargo test -p prometeu-system` for Hub action and presentation behavior. -- Run `cargo test -p prometeu-firmware` for firmware transition behavior. -- Run `cargo test -p prometeu-host-desktop-winit` if runner integration changes. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/programs/prometeu_hub.rs` -- `crates/console/prometeu-system/src/services/` -- `crates/console/prometeu-firmware/src/firmware/steps/firmware_step_hub_home.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` -- `discussion/workflow/plans/PLN-0134-wire-home-game-list-launch-flow.md` diff --git a/discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md b/discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md deleted file mode 100644 index 95721ff1..00000000 --- a/discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: PLN-0135 -ticket: system-run-cart -title: Validate Games Root Launch End To End -status: done -created: 2026-07-03 -ref_decisions: [DEC-0036] -tags: [] ---- - -## Briefing - -`PLN-0132` through `PLN-0134` specify, discover, display, and launch games -from a local games root. This plan closes the slice by adding end-to-end -validation evidence and manual smoke instructions for the product behavior the -user wants: point the SO at a directory, click a listed game, and boot it. - -## Objective - -Prove the complete `--games-root -> Home list -> click -> LoadCartridge -> -GameRunning` path while preserving direct `--run` boot and the absence of -userland `run_cart`. - -## Dependencies - -- Source decision: `DEC-0036`. -- `PLN-0132`, `PLN-0133`, and `PLN-0134` must be complete. -- Existing test cartridge fixtures, especially `test-cartridges/stress-console`. - -## Scope - -- Add automated integration coverage for the full games-root Home launch path - where the existing host/firmware test seams allow it. -- Add or document a minimal local games-root fixture using existing directory - cartridges. -- Validate invalid-candidate omission at the end-to-end boundary. -- Validate direct `--run` still works after games-root wiring. -- Capture manual smoke-test commands for release-mode functional verification. - -## Non-Goals - -- No new product UI polish. -- No screenshot automation unless the existing test harness already supports it - cleanly. -- No return-to-Home after a running Game. -- No Game A -> Home -> Game B orchestration. -- No `.pmc` validation. -- No broad performance tuning beyond confirming release-mode smoke behavior. - -## Execution Method - -1. Add an integration-style firmware or host test that constructs a games root - with a valid Game directory cartridge and boots to Hub/Home with that root. -2. Drive the Home selection path through the same action path used by user - input, not by calling the cartridge loader directly. -3. Assert that the machine reaches `LoadCartridge` or `GameRunning`, depending - on the most stable observable state available in the test harness. -4. Add a negative case with an invalid candidate and assert it is omitted or - reported as a diagnostic without appearing in Home. -5. Add a regression assertion that no public guest syscall named - `system.run_cart` is required by this path. -6. Re-run direct boot tests or add a small regression test proving - `--run test-cartridges/stress-console` remains independent from - `--games-root`. -7. Record manual smoke commands in the relevant spec or developer-facing doc: - - `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges` - - `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console` - -## Acceptance Criteria - -- Automated tests cover a valid games-root Home launch path. -- Automated tests cover invalid candidate omission or diagnostics at the - end-to-end boundary. -- Automated or documented validation proves direct `--run` still works. -- Manual smoke instructions use release mode for realistic interactive speed. -- Validation evidence shows no guest syscall participates in launch. -- `discussion validate` passes. - -## Tests - -- Run `cargo test -p prometeu-system`. -- Run `cargo test -p prometeu-firmware`. -- Run `cargo test -p prometeu-host-desktop-winit`. -- Run `cargo test -p prometeu-hal syscalls` to keep the removed syscall surface - guarded. -- Manually smoke test: - `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges`. -- Manually smoke test: - `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/` -- `crates/console/prometeu-firmware/` -- `crates/host/prometeu-host-desktop-winit/` -- `crates/console/prometeu-hal/src/syscalls/tests.rs` -- Runtime specs or developer docs containing smoke commands -- `discussion/workflow/plans/PLN-0135-validate-games-root-launch-end-to-end.md` diff --git a/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md b/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md new file mode 100644 index 00000000..7117ec6e --- /dev/null +++ b/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md @@ -0,0 +1,96 @@ +--- +id: PLN-0136 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Route Desktop Home Key Outside Guest Input +status: open +created: 2026-07-03 +ref_decisions: [DEC-0037] +tags: [host, input, systemos, keyboard, lifecycle] +--- + +## Briefing + +`DEC-0037` makes desktop `Esc` the primary Home/SystemOS request and allows the +physical `Home` key as an alias. This request must be intercepted by the +host/SystemOS and must not appear as a guest pad button or input intrinsic. + +This plan adds the host input route for the Home request without changing the +guest-visible pad surface. + +## Decisions of Origin + +- `DEC-0037` - Foreground Stack and Game Pause Contract. + +## Target + +Route desktop `Esc` and optional physical `Home` key to a SystemOS Home request +outside `InputSignals`. + +## Scope + +- Extend `crates/host/prometeu-host-desktop-winit/src/input.rs` or a nearby + host input module with a host/system control signal separate from + `prometeu_hal::InputSignals`. +- Map `KeyCode::Escape` to Home/SystemOS request. +- Map `KeyCode::Home` as an alias. +- Ensure neither key mutates `start_signal`, `select_signal`, or any pad field. +- Route the request through `crates/host/prometeu-host-desktop-winit/src/runner.rs` + into firmware/SystemOS. +- Add tests proving `Esc` and `Home` do not affect guest pad state. + +## Out of Scope + +- No pause/resume lifecycle implementation. +- No render ownership gating. +- No guest-visible Home button. +- No gamepad hardware Home mapping unless a future plan defines controller + system controls separately. + +## Execution Plan + +1. Define host control state. + Add a small host-only type such as `HostSystemControls` with a + `home_requested` edge or event. Store it in `HostInputHandler` or in the + runner next to the existing input handler. + +2. Update keyboard handling. + In `HostInputHandler::handle_event`, recognize `KeyCode::Escape` and + `KeyCode::Home` on press. Set the host control event and return without + modifying `InputSignals`. + +3. Preserve pad mapping. + Keep existing pad mappings unchanged: + arrows, `A/D/W/S`, `Q/E`, `Z`, and Shift. Do not reuse `Start` or `Select` + for Home. + +4. Route to firmware/SystemOS. + In `crates/host/prometeu-host-desktop-winit/src/runner.rs`, consume the + host Home event before or during `about_to_wait` and call the firmware or + SystemOS method introduced by `PLN-0137` / `PLN-0140`. + +5. Add tests. + Add host input tests for `Esc`, physical `Home`, and existing pad mappings. + Tests must prove Home produces a system event and no guest pad mutation. + +## Acceptance Criteria + +- Pressing desktop `Esc` emits a Home/SystemOS host control event. +- Pressing physical `Home` emits the same event. +- Neither key affects `InputSignals`. +- Existing guest pad keyboard mappings still work. +- The runner consumes the Home request as a system action. +- The implementation does not add a guest syscall, intrinsic, or pad button. + +## Tests / Validation + +- Add unit tests in `prometeu-host-desktop-winit` for host input mapping. +- Run `cargo test -p prometeu-host-desktop-winit input`. +- Run `cargo test -p prometeu-hal input` if pad signal behavior changes. +- Run `discussion validate`. + +## Risks + +- `Esc` may conflict with debugger or window-close expectations. Keep debugger + start handling on `D` unchanged and do not map `Esc` to process exit. +- Physical `Home` is not available on every keyboard, so it must remain an + alias rather than the primary portable mapping. diff --git a/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md b/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md new file mode 100644 index 00000000..e3d29add --- /dev/null +++ b/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md @@ -0,0 +1,116 @@ +--- +id: PLN-0137 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Implement SystemOS Foreground Stack State +status: open +created: 2026-07-03 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, foreground, shell, game] +--- + +## Briefing + +`DEC-0037` requires SystemOS to own foreground authority. The system must know +which owner is foreground, keep Hub/Home as root, allow only one resident Game, +allow only one foreground Shell, and return from VM-backed Shell apps to Hub in +v1. + +This plan implements the SystemOS state model only. It does not yet deliver +Game pause/resume events, host keyboard routing, or render/audio/input boundary +changes. + +## Decisions of Origin + +- `DEC-0037` - Foreground Stack and Game Pause Contract. + +## Target + +Add an explicit SystemOS foreground stack model that can represent Hub, a +foreground Shell task, and a paused/suspended resident Game. + +## Scope + +- Add or extend SystemOS-owned foreground state under + `crates/console/prometeu-system/src/os/` or + `crates/console/prometeu-system/src/services/`. +- Represent v1 foreground owners: + - Hub/Home root; + - Shell task foreground; + - Game foreground; + - Game paused/suspended resident. +- Enforce at most one resident Game and at most one foreground Shell. +- Keep Hub as root/back-stack controller. +- Ensure Shell close returns to Hub/Home in v1. +- Expose focused queries through existing or new facades, likely + `crates/console/prometeu-system/src/os/facades/lifecycle.rs` and + `crates/console/prometeu-system/src/os/facades/sessions.rs`. + +## Out of Scope + +- No VM pause/resume event delivery. +- No host `Esc`/`Home` keyboard mapping. +- No render epoch implementation. +- No Game-to-Game switching. +- No background execution. +- No memory eviction for the resident Game. + +## Execution Plan + +1. Define foreground model types. + Add enums/structs for foreground owner and resident Game state in the most + local SystemOS module. The model must distinguish Game foreground, Game + paused/suspended, Hub foreground, and Shell task foreground. + +2. Add SystemOS storage. + Store the foreground model in `SystemOS` from + `crates/console/prometeu-system/src/os/system_os.rs` with initialization to + Hub/Home root. + +3. Wire lifecycle facade operations. + Extend lifecycle/session facades so firmware can request: + - set Game foreground; + - request Home from Game; + - set Shell foreground from Hub; + - close Shell and return to Hub; + - query current foreground owner; + - query resident Game task. + +4. Enforce cardinality. + Add checks that reject a second resident Game and a second foreground Shell + with typed lifecycle errors. Use existing `LifecycleError` style or add + precise variants. + +5. Integrate existing Shell close behavior. + Update `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` + only where needed so closing a Shell task routes through the foreground model + and ends in `FirmwareState::HubHome`. + +6. Add tests. + Cover initial Hub foreground, Game foreground registration, Home request + moving Game to resident paused/suspended state, Shell foreground activation, + Shell close returning to Hub, and cardinality rejection. + +## Acceptance Criteria + +- SystemOS has an explicit foreground owner model. +- Hub/Home is the initialized root foreground owner. +- Only one resident Game can exist in v1. +- Only one Shell task can be foreground in v1. +- VM-backed Shell close returns to Hub/Home. +- Foreground queries are available to firmware and later plans. +- No implementation path enables background execution. + +## Tests / Validation + +- Add focused unit tests in `prometeu-system` around the new model and facade. +- Run `cargo test -p prometeu-system lifecycle foreground`. +- Run `cargo test -p prometeu-firmware shell`. +- Run `discussion validate`. + +## Risks + +- Existing `TaskState::Foreground` and `TaskState::Suspended` may look like the + complete model. Do not overload them until foreground ownership and Hub root + authority are explicit. +- Shell windows already have focus semantics. Keep window focus as visual/task + evidence, not the sole source of SystemOS foreground truth. diff --git a/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md b/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md new file mode 100644 index 00000000..78ad1dea --- /dev/null +++ b/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md @@ -0,0 +1,114 @@ +--- +id: PLN-0138 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Specify Foreground Pause and Home Contract +status: open +created: 2026-07-03 +ref_decisions: [DEC-0037] +tags: [runtime, specs, lifecycle, input, renderer, foreground] +--- + +## Briefing + +`DEC-0037` establishes the v1 foreground stack contract: one foreground visual +owner, one resident Game, one foreground Shell, Hub as root, Game-visible +pause/resume, OS-owned suspension, desktop `Esc` as Home/SystemOS request, and +render epoch invalidation across foreground transitions. + +This plan updates the canonical runtime specs before implementation so code +plans cannot drift into guest-owned Home input, background execution, or stale +frame presentation. + +## Decisions of Origin + +- `DEC-0037` - Foreground Stack and Game Pause Contract. + +## Target + +Document the foreground, pause/suspend, Home request, input, audio, and render +ownership contract in the canonical runtime specs. + +## Scope + +- Update `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` with: + - Hub as root/special Shell owner; + - one foreground visual owner; + - one resident Game; + - one foreground Shell; + - `Game -> Home/Shell -> same Game` lifecycle; + - VM-backed Shell app return-to-Hub behavior. +- Update `docs/specs/runtime/06-input-peripheral.md` with: + - Home/SystemOS as host/system control, not pad input; + - desktop `Esc` as primary Home request; + - physical `Home` key as optional alias; + - input clear/barrier requirements across Game pause/resume. +- Update `docs/specs/runtime/09-events-and-concurrency.md` with: + - pause notification budget; + - OS-owned suspension after the budget; + - resume/foreground-restore notification before visual return. +- Update `docs/specs/runtime/14-boot-profiles.md` only to clarify that this + foreground contract is not Game-to-Game switching and does not change direct + `--run` or `--games-root` boot semantics. +- Update renderer-related spec text in the most local existing runtime spec if + present, otherwise add a short subsection to `12-firmware-pos-and-prometeuhub.md` + describing ownership epoch invalidation and first-valid-frame gating. + +## Out of Scope + +- No Rust implementation. +- No new guest ABI or pad button. +- No background execution policy. +- No Game A -> Home -> Game B switching. +- No memory eviction policy for resident Games. + +## Execution Plan + +1. Edit `12-firmware-pos-and-prometeuhub.md`. + Define foreground owner cardinality, Hub root ownership, Shell foreground + behavior, Game resident/suspended behavior, return-to-Hub from Shell, and the + requirement to keep Hub/Shell visible until a resumed Game submits a valid + current-epoch frame. + +2. Edit `06-input-peripheral.md`. + State that Home/SystemOS is outside the guest-visible pad and input + intrinsic surface. Document desktop `Esc` as the primary host mapping and + physical `Home` as an optional alias. + +3. Edit `09-events-and-concurrency.md`. + Add the pause/resume event ordering: Home request, pause notification, + bounded reaction budget, OS suspension, resume/foreground-restore + notification, first-valid-frame visual return. + +4. Edit `14-boot-profiles.md`. + Cross-reference the foreground contract and explicitly keep direct boot, + games-root Home launch, and future Game-to-Game switching separate. + +5. Run text checks. + Search for wording that implies Home is a guest input button, a guest ABI, or + background execution. Replace misleading text with the `DEC-0037` contract. + +## Acceptance Criteria + +- Specs define `Paused` as Game-visible and `Suspended` as OS-owned. +- Specs state that desktop `Esc` is the primary Home request and is not guest + pad input. +- Specs state that the Game receives pause/resume notifications but does not + control OS suspension. +- Specs state that Hub is the root Shell owner and Shell app close returns to + Hub in v1. +- Specs state that visual return to Game waits for a valid current-epoch render + submission. +- Specs explicitly exclude background execution and Game-to-Game switching. + +## Tests / Validation + +- Run `discussion validate`. +- Run `rg -n "Home|Esc|Suspended|Paused|foreground|background execution" docs/specs/runtime`. +- Verify no edited spec describes Home/SystemOS as a guest-visible pad button. + +## Risks + +- The spec could accidentally reopen Game-to-Game switching; keep that scoped to + `DSC-0043 / AGD-0044`. +- The input spec could confuse physical keyboard mapping with VM input; keep + host/system controls outside the guest input surface. diff --git a/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md b/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md new file mode 100644 index 00000000..b8f7086e --- /dev/null +++ b/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md @@ -0,0 +1,106 @@ +--- +id: PLN-0139 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Validate Game Home Shell Game End To End +status: open +created: 2026-07-03 +ref_decisions: [DEC-0037] +tags: [runtime, validation, firmware, host, lifecycle] +--- + +## Briefing + +`DEC-0037` is not complete until the product flow is proven end to end: +a Game runs, the user requests Home, the Game is paused/suspended, Hub or a +VM-backed Shell takes foreground, closing Shell returns to Hub, and the same +Game can resume only after a valid current-epoch frame exists. + +This plan adds final validation after the spec, SystemOS, VM, host input, and +render/audio/input boundary plans are implemented. + +## Decisions of Origin + +- `DEC-0037` - Foreground Stack and Game Pause Contract. + +## Target + +Prove the complete `Game -> Home/Shell -> same Game` lifecycle and prevent +regressions around guest input, foreground authority, and stale rendering. + +## Scope + +- Add integration-style tests for: + - Game running to Home request; + - pause notification and forced suspension; + - Hub foreground while Game is resident; + - VM-backed Shell foreground while Game is suspended; + - Shell close returning to Hub; + - resume request returning to same Game; + - first-valid-frame visual restore; + - host `Esc`/`Home` not entering guest input. +- Add manual smoke instructions for desktop host. +- Confirm direct `--run` and games-root Home launch remain separate. + +## Out of Scope + +- No new implementation beyond tests and developer-facing validation notes. +- No Game A -> Home -> Game B switching. +- No performance tuning. +- No polished UI. + +## Execution Plan + +1. Add firmware/SystemOS integration tests. + Use the most stable existing seams in `crates/console/prometeu-firmware/src/firmware/firmware.rs` + and `crates/console/prometeu-system` tests to drive Game foreground, Home + request, Shell foreground, Shell close, and Game resume. + +2. Add host input integration tests. + Ensure `Esc` and physical `Home` create a host Home event and do not mutate + guest `InputSignals`. + +3. Add render publication validation. + Assert stale submissions from the pre-pause Game epoch are rejected after + Home takes foreground and that the Game is not visually restored until a new + current-epoch submission exists. + +4. Add audio/input boundary validation. + Assert Game input is cleared or barriered across pause/resume. Assert Game + audio pause follows Game suspension. + +5. Add direct boot and games-root regression checks. + Ensure direct `--run ` remains independent from Home request handling + and that games-root Home launch still enters the Game path. + +6. Document manual smoke commands. + Add or update developer-facing spec text with release-mode commands: + - `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console` + - `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges` + Include the expected `Esc -> Home -> Shell -> Hub -> resume Game` behavior + once implemented. + +## Acceptance Criteria + +- Automated tests cover `Game -> Home -> same Game`. +- Automated tests cover `Game -> Home -> Shell -> Hub -> same Game`. +- Tests prove Home request is host/SystemOS control, not guest pad input. +- Tests prove suspended Game does not tick. +- Tests prove stale render submissions are not presented. +- Tests prove visual return waits for a current-epoch Game frame. +- Direct `--run` and games-root Home launch regressions pass. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- Run `cargo test -p prometeu-firmware`. +- Run `cargo test -p prometeu-host-desktop-winit`. +- Run any targeted render worker tests affected by ownership epoch changes. +- Run `discussion validate`. + +## Risks + +- The current test seams may not expose enough host-driven lifecycle behavior. + If needed, add narrow test-only helpers rather than testing through sleeps or + real-time window loops. +- End-to-end validation can become flaky if it depends on wall-clock timing. + Use deterministic ticks and explicit state transitions. diff --git a/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md b/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md new file mode 100644 index 00000000..32cf8eba --- /dev/null +++ b/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md @@ -0,0 +1,110 @@ +--- +id: PLN-0140 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Deliver Game Pause Resume and Suspension +status: open +created: 2026-07-03 +ref_decisions: [DEC-0037] +tags: [runtime, os, vm, lifecycle, pause, suspension] +--- + +## Briefing + +`DEC-0037` separates Game-visible `Paused` from OS-owned `Suspended`. When Home +is requested during Game execution, SystemOS must notify the Game, give it a +short bounded budget, then suspend the VM even if the Game does not cooperate. +When returning, SystemOS must reactivate the VM and send a resume or +foreground-restore notification before the visual owner returns to Game. + +This plan implements the pause/resume and suspension mechanics after the +foreground state model exists. + +## Decisions of Origin + +- `DEC-0037` - Foreground Stack and Game Pause Contract. + +## Target + +Add Game pause/resume lifecycle delivery and OS-enforced VM suspension for the +same resident Game. + +## Scope + +- Add a Game lifecycle event model for pause and resume/foreground-restore. +- Add a bounded pause reaction budget owned by SystemOS. +- Suspend the Game VM after the budget even if the Game does not cooperate. +- Ensure a suspended Game receives no normal gameplay ticks. +- Reactivate the VM and deliver resume/foreground-restore before Game visual + foreground restoration. +- Integrate with `GameRunningStep` and VM runtime tick control. + +## Out of Scope + +- No render first-valid-frame gating; that is in `PLN-0141`. +- No host keyboard routing; that is in `PLN-0136`. +- No new guest syscall. +- No background execution. +- No Game-to-Game switching. + +## Execution Plan + +1. Add lifecycle event representation. + Define pause/resume events in the most local runtime lifecycle module, + likely under `crates/console/prometeu-system/src/os/lifecycle.rs` or a new + SystemOS lifecycle submodule. Events must be observable by the VM/runtime + path without giving the Game authority over suspension. + +2. Add pause transition state. + Extend the foreground state from `PLN-0137` with a `PauseRequested` or + equivalent transition state that records the target Game task and the + remaining pause budget. + +3. Gate VM ticks. + Update `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` + and `crates/console/prometeu-system/src/services/vm_runtime/tick.rs` so a + suspended Game does not receive normal ticks, normal input, or frame pacing. + +4. Deliver pause notification. + Add the mechanism that makes the pause event visible to the running Game + before suspension. Use an existing VM event channel if present; otherwise add + a narrow lifecycle-event queue owned by SystemOS/VM runtime. + +5. Enforce the budget. + Advance the pause budget on firmware ticks. When it expires, transition the + Game task/process to suspended through SystemOS lifecycle regardless of Game + cooperation. + +6. Deliver resume notification. + On return to the resident Game, transition from suspended to resume-requested, + reactivate VM execution, deliver resume/foreground-restore, and leave visual + restoration to the render boundary plan. + +7. Add tests. + Cover cooperative pause, non-cooperative forced suspension, no tick while + suspended, resume notification delivery, and no Game-owned ability to cancel + OS suspension. + +## Acceptance Criteria + +- Game receives pause before suspension. +- SystemOS suspends after a bounded budget even if the Game does not cooperate. +- Suspended Game does not receive normal gameplay ticks. +- Resume/foreground-restore is delivered before visual return to Game. +- Game cannot prevent OS suspension through guest-visible behavior. +- Existing debugger pause behavior remains distinct from lifecycle suspension. + +## Tests / Validation + +- Add unit tests in `prometeu-system` for lifecycle transition state. +- Add firmware tests around `GameRunningStep` pause request and suspension. +- Add VM runtime tests proving suspended state suppresses normal ticks. +- Run `cargo test -p prometeu-system`. +- Run `cargo test -p prometeu-firmware`. +- Run `discussion validate`. + +## Risks + +- Debugger pause already exists in VM runtime. Keep debug pause separate from + lifecycle pause/suspend. +- A lifecycle event queue could become a general app event system. Keep this + plan limited to pause/resume required by `DEC-0037`. diff --git a/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md b/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md new file mode 100644 index 00000000..710caeb2 --- /dev/null +++ b/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md @@ -0,0 +1,104 @@ +--- +id: PLN-0141 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Integrate Render Audio and Input Pause Boundaries +status: open +created: 2026-07-03 +ref_decisions: [DEC-0037] +tags: [runtime, renderer, audio, input, foreground, lifecycle] +--- + +## Briefing + +`DEC-0037` requires foreground transitions to invalidate stale render +submissions, keep Hub/Shell visible until the resumed Game publishes a valid +current-epoch frame, clear or barrier Game input across pause/resume, and pause +Game audio with the Game VM in v1. + +This plan integrates those boundary effects after SystemOS foreground state and +Game pause/resume mechanics exist. + +## Decisions of Origin + +- `DEC-0037` - Foreground Stack and Game Pause Contract. + +## Target + +Make foreground transitions safe at the render, input, and audio boundaries. + +## Scope + +- Advance render ownership epoch/generation whenever foreground owner changes. +- Discard stale Game frames after Shell/Hub takes foreground. +- Discard stale Shell frames after Game resumes. +- Keep Hub/Shell visible until a resumed Game publishes a valid current-epoch + submission. +- Clear or barrier Game input across pause/resume boundaries. +- Pause Game audio with the Game VM in v1. +- Add telemetry/tests for stale discards and first-valid-frame gating. + +## Out of Scope + +- No independent background audio policy. +- No overlay rendering policy. +- No Game-to-Game switch rendering. +- No full memory eviction behavior. + +## Execution Plan + +1. Extend render ownership transition calls. + Update `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs` + and call sites in `tick.rs` so foreground owner changes explicitly advance + ownership epoch/generation, not only app mode/app id changes. + +2. Add foreground restore gating. + Add state in SystemOS or firmware that records when visual return to Game is + pending. The state must remain in Hub/Shell presentation until a + current-epoch Game submission is observed. + +3. Update host presentation selection. + Update `crates/host/prometeu-host-desktop-winit/src/runner.rs` if necessary + so `should_present_worker_frame` and fallback framebuffer presentation obey + the foreground restore gate. + +4. Add stale discard coverage. + Extend render manager and render worker tests to prove stale submissions + from the previous owner are discarded after epoch change. + +5. Add input boundary clearing. + Add a SystemOS or firmware input barrier when Game pause starts and when + Game resume starts. The barrier must prevent held or pressed Game input from + leaking across the lifecycle boundary. + +6. Add audio pause integration. + Ensure Game pause/suspend produces the existing host audio pause command path + or a new explicit lifecycle audio pause signal. Keep this separate from + debugger pause if the observable cause matters in tests. + +7. Add tests. + Cover stale frame discard, first-valid-frame visual return, input clearing, + and Game audio pause/resume behavior. + +## Acceptance Criteria + +- Foreground owner changes advance render ownership epoch/generation. +- Old Game frames cannot present after Hub/Shell takes foreground. +- Old Hub/Shell frames cannot present after Game resumes. +- Visual return to Game waits for a valid current-epoch Game submission. +- Game input does not leak across pause/resume boundaries. +- Game audio pauses with Game suspension in v1. + +## Tests / Validation + +- Run `cargo test -p prometeu-system render foreground`. +- Run `cargo test -p prometeu-firmware pause`. +- Run `cargo test -p prometeu-host-desktop-winit presentation`. +- Run `discussion validate`. + +## Risks + +- Existing render epoch logic may already key off app mode/app id. Foreground + owner changes must be explicit so Shell/Hub and resumed Game cannot share a + stale epoch by accident. +- Audio pause currently follows VM paused/debugger state in the host. Keep Game + lifecycle pause distinct enough that tests can prove the decision contract. From d54dadf2a5f5021969a3a79602352459ec8031d1 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Jul 2026 23:00:49 +0100 Subject: [PATCH 02/29] implements PLN-0136 --- .../src/firmware/firmware.rs | 5 + .../prometeu-host-desktop-winit/src/input.rs | 142 +++++++++++++++--- .../prometeu-host-desktop-winit/src/runner.rs | 6 + discussion/index.ndjson | 2 +- ...te-desktop-home-key-outside-guest-input.md | 2 +- 5 files changed, 132 insertions(+), 25 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 03e783a5..365717eb 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -2,6 +2,7 @@ use crate::firmware::boot_target::BootTarget; use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::cartridge::Cartridge; +use prometeu_hal::log::LogSource; use prometeu_hal::telemetry::CertificationConfig; use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_system::{PrometeuHub, SystemOS}; @@ -166,6 +167,10 @@ impl Firmware { self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)); self.state_initialized = false; } + + pub fn request_home_from_host(&mut self) { + self.os.info(LogSource::Pos, "Host requested Home/SystemOS".to_string()); + } } #[cfg(test)] diff --git a/crates/host/prometeu-host-desktop-winit/src/input.rs b/crates/host/prometeu-host-desktop-winit/src/input.rs index 7d9874ef..06626cab 100644 --- a/crates/host/prometeu-host-desktop-winit/src/input.rs +++ b/crates/host/prometeu-host-desktop-winit/src/input.rs @@ -43,9 +43,27 @@ impl FbViewport { pub struct HostInputHandler { pub signals: InputSignals, + system_controls: HostSystemControls, display_size: (usize, usize), } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct HostSystemControls { + home_requested: bool, +} + +impl HostSystemControls { + pub fn request_home(&mut self) { + self.home_requested = true; + } + + pub fn take_home_requested(&mut self) -> bool { + let requested = self.home_requested; + self.home_requested = false; + requested + } +} + impl Default for HostInputHandler { fn default() -> Self { Self::new((480, 270)) @@ -54,35 +72,47 @@ impl Default for HostInputHandler { impl HostInputHandler { pub fn new(display_size: (usize, usize)) -> Self { - Self { signals: InputSignals::default(), display_size } + Self { + signals: InputSignals::default(), + system_controls: HostSystemControls::default(), + display_size, + } + } + + pub fn take_home_requested(&mut self) -> bool { + self.system_controls.take_home_requested() + } + + pub fn handle_key_code(&mut self, code: KeyCode, state: ElementState) { + let is_down = state == ElementState::Pressed; + + match code { + KeyCode::Escape | KeyCode::Home if is_down => self.system_controls.request_home(), + + KeyCode::ArrowUp => self.signals.up_signal = is_down, + KeyCode::ArrowDown => self.signals.down_signal = is_down, + KeyCode::ArrowLeft => self.signals.left_signal = is_down, + KeyCode::ArrowRight => self.signals.right_signal = is_down, + + KeyCode::KeyA => self.signals.a_signal = is_down, + KeyCode::KeyD => self.signals.b_signal = is_down, + KeyCode::KeyW => self.signals.x_signal = is_down, + KeyCode::KeyS => self.signals.y_signal = is_down, + KeyCode::KeyQ => self.signals.l_signal = is_down, + KeyCode::KeyE => self.signals.r_signal = is_down, + + KeyCode::KeyZ => self.signals.start_signal = is_down, + KeyCode::ShiftLeft | KeyCode::ShiftRight => self.signals.select_signal = is_down, + + _ => {} + } } pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) { match event { WindowEvent::KeyboardInput { event, .. } => { if let PhysicalKey::Code(code) = event.physical_key { - let is_down = event.state == ElementState::Pressed; - - match code { - KeyCode::ArrowUp => self.signals.up_signal = is_down, - KeyCode::ArrowDown => self.signals.down_signal = is_down, - KeyCode::ArrowLeft => self.signals.left_signal = is_down, - KeyCode::ArrowRight => self.signals.right_signal = is_down, - - KeyCode::KeyA => self.signals.a_signal = is_down, - KeyCode::KeyD => self.signals.b_signal = is_down, - KeyCode::KeyW => self.signals.x_signal = is_down, - KeyCode::KeyS => self.signals.y_signal = is_down, - KeyCode::KeyQ => self.signals.l_signal = is_down, - KeyCode::KeyE => self.signals.r_signal = is_down, - - KeyCode::KeyZ => self.signals.start_signal = is_down, - KeyCode::ShiftLeft | KeyCode::ShiftRight => { - self.signals.select_signal = is_down - } - - _ => {} - } + self.handle_key_code(code, event.state); } } @@ -134,3 +164,69 @@ impl HostInputHandler { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_no_pad_signal(signals: InputSignals) { + assert!(!signals.up_signal); + assert!(!signals.down_signal); + assert!(!signals.left_signal); + assert!(!signals.right_signal); + assert!(!signals.a_signal); + assert!(!signals.b_signal); + assert!(!signals.x_signal); + assert!(!signals.y_signal); + assert!(!signals.l_signal); + assert!(!signals.r_signal); + assert!(!signals.start_signal); + assert!(!signals.select_signal); + assert!(!signals.f_signal); + } + + #[test] + fn escape_requests_home_without_mutating_guest_pad() { + let mut input = HostInputHandler::default(); + + input.handle_key_code(KeyCode::Escape, ElementState::Pressed); + + assert!(input.take_home_requested()); + assert!(!input.take_home_requested()); + assert_no_pad_signal(input.signals); + } + + #[test] + fn physical_home_requests_home_without_mutating_guest_pad() { + let mut input = HostInputHandler::default(); + + input.handle_key_code(KeyCode::Home, ElementState::Pressed); + + assert!(input.take_home_requested()); + assert_no_pad_signal(input.signals); + } + + #[test] + fn home_release_does_not_emit_a_new_home_request() { + let mut input = HostInputHandler::default(); + + input.handle_key_code(KeyCode::Home, ElementState::Released); + + assert!(!input.take_home_requested()); + assert_no_pad_signal(input.signals); + } + + #[test] + fn existing_keyboard_pad_mapping_is_preserved() { + let mut input = HostInputHandler::default(); + + input.handle_key_code(KeyCode::KeyZ, ElementState::Pressed); + input.handle_key_code(KeyCode::ShiftLeft, ElementState::Pressed); + input.handle_key_code(KeyCode::ArrowUp, ElementState::Pressed); + + assert!(input.signals.start_signal); + assert!(input.signals.select_signal); + assert!(input.signals.up_signal); + assert!(!input.take_home_requested()); + } +} diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index c6a9cc8c..e40c89cd 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -372,6 +372,12 @@ impl ApplicationHandler for HostRunner { self.firmware.os.fs().mount(Box::new(backend)); } + if self.input.take_home_requested() { + self.firmware.request_home_from_host(); + self.invalidate_host_surface(); + self.request_redraw_if_needed(); + } + // 3. Timing Management (The heart of determinism). // We measure the elapsed time since the last iteration and add it to an // accumulator. We then execute exactly as many 60Hz slices as the diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1779eddc..e6ee893f 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md b/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md index 7117ec6e..aaeb9fc2 100644 --- a/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md +++ b/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md @@ -2,7 +2,7 @@ id: PLN-0136 ticket: foreground-stack-game-pause-shell-vm-backed title: Route Desktop Home Key Outside Guest Input -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0037] tags: [host, input, systemos, keyboard, lifecycle] From 57c9af939b7df7a604006c850da41ff88ede3136 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Jul 2026 23:04:23 +0100 Subject: [PATCH 03/29] implements PLN-0137 --- crates/console/prometeu-system/src/lib.rs | 3 + .../src/os/facades/lifecycle.rs | 127 +++++++++++- .../src/os/facades/sessions.rs | 6 +- .../prometeu-system/src/os/lifecycle.rs | 3 + .../prometeu-system/src/os/system_os.rs | 100 +++++++++ .../src/services/foreground.rs | 189 ++++++++++++++++++ .../prometeu-system/src/services/mod.rs | 1 + discussion/index.ndjson | 2 +- ...plement-systemos-foreground-stack-state.md | 2 +- 9 files changed, 425 insertions(+), 8 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/foreground.rs diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index bdce77e5..02a87c1d 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -12,6 +12,9 @@ pub use services::async_work::{ AsyncWorkJobKind, AsyncWorkJobOutcome, AsyncWorkLane, AsyncWorkLaneConfig, AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority, }; +pub use services::foreground::{ + ForegroundOwner, ForegroundStack, ForegroundStackError, ResidentGame, ResidentGameState, +}; pub use services::fs; pub use services::game_library::{ GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root, diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index 7fe687b8..1d7c3e38 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -2,7 +2,8 @@ use crate::CrashReport; use crate::os::SystemOS; use crate::os::{LifecycleError, LifecycleOperation}; use crate::process::{ProcessId, ProcessState}; -use crate::task::{TaskId, TaskState}; +use crate::services::foreground::{ForegroundOwner, ForegroundStackError, ResidentGame}; +use crate::task::{TaskId, TaskKind, TaskState}; pub struct LifecycleFacade<'a> { pub(in crate::os) os: &'a mut SystemOS, @@ -23,19 +24,92 @@ impl<'a> LifecycleFacade<'a> { } pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + match task_kind_for_task(self.os, task_id)? { + TaskKind::Game => return self.set_game_foreground_task(task_id), + TaskKind::Shell => return self.set_shell_foreground_task(task_id), + } + } + + pub fn set_game_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { let process_id = process_id_for_task(self.os, task_id)?; + if task_kind_for_task(self.os, task_id)? != TaskKind::Game { + return Err(LifecycleError::InvalidTransition { + task_id, + from: self.task_state(task_id).unwrap_or(TaskState::Closed), + operation: LifecycleOperation::SetForeground, + }); + } + + self.os.foreground_stack.set_game_foreground(task_id).map_err(map_foreground_error)?; self.os.task_manager.set_foreground(task_id); self.os.process_manager.mark_running(process_id); Ok(()) } + pub fn set_shell_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = process_id_for_task(self.os, task_id)?; + + if task_kind_for_task(self.os, task_id)? != TaskKind::Shell { + return Err(LifecycleError::InvalidTransition { + task_id, + from: self.task_state(task_id).unwrap_or(TaskState::Closed), + operation: LifecycleOperation::SetForeground, + }); + } + + self.os.foreground_stack.set_shell_foreground(task_id).map_err(map_foreground_error)?; + self.os.task_manager.set_foreground(task_id); + self.os.process_manager.mark_running(process_id); + + Ok(()) + } + + pub fn foreground_owner(&self) -> ForegroundOwner { + self.os.foreground_stack.owner() + } + + pub fn resident_game(&self) -> Option { + self.os.foreground_stack.resident_game() + } + + pub fn resident_game_task(&self) -> Option { + self.os.foreground_stack.resident_game_task() + } + + pub fn request_home_from_game(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.foreground_stack.request_home_from_game(task_id).map_err(map_foreground_error)?; + self.os.task_manager.mark_suspended(task_id); + self.os.process_manager.mark_suspended(process_id); + + Ok(()) + } + + pub fn return_to_hub(&mut self) { + self.os.foreground_stack.return_to_hub(); + self.os.task_manager.clear_foreground(); + } + pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { let process_id = process_id_for_task(self.os, task_id)?; self.os.task_manager.mark_suspended(task_id); self.os.process_manager.mark_suspended(process_id); + match self.os.foreground_stack.owner() { + ForegroundOwner::Game(owner) if owner == task_id => { + self.os + .foreground_stack + .request_home_from_game(task_id) + .map_err(map_foreground_error)?; + } + ForegroundOwner::Shell(owner) if owner == task_id => { + self.os.foreground_stack.return_to_hub(); + } + _ => {} + } Ok(()) } @@ -54,8 +128,24 @@ impl<'a> LifecycleFacade<'a> { let process_id = process_id_for_task(self.os, task_id)?; - self.os.task_manager.set_foreground(task_id); - self.os.process_manager.mark_running(process_id); + match task_kind_for_task(self.os, task_id)? { + TaskKind::Game => { + self.os + .foreground_stack + .set_game_foreground(task_id) + .map_err(map_foreground_error)?; + self.os.task_manager.set_foreground(task_id); + self.os.process_manager.mark_running(process_id); + } + TaskKind::Shell => { + self.os + .foreground_stack + .set_shell_foreground(task_id) + .map_err(map_foreground_error)?; + self.os.task_manager.set_foreground(task_id); + self.os.process_manager.mark_running(process_id); + } + } Ok(()) } @@ -65,6 +155,12 @@ impl<'a> LifecycleFacade<'a> { self.os.task_manager.close_task(task_id); self.os.process_manager.mark_stopped(process_id); + if self.os.foreground_stack.resident_game_task() == Some(task_id) { + self.os.foreground_stack.clear_resident_game(task_id); + } else if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Shell(owner) if owner == task_id) + { + self.os.foreground_stack.return_to_hub(); + } Ok(()) } @@ -78,11 +174,22 @@ impl<'a> LifecycleFacade<'a> { self.os.task_manager.mark_crashed(task_id); self.os.process_manager.mark_crashed(process_id); + if self.os.foreground_stack.resident_game_task() == Some(task_id) { + self.os.foreground_stack.clear_resident_game(task_id); + } else if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Shell(owner) if owner == task_id) + { + self.os.foreground_stack.return_to_hub(); + } Ok(()) } } +fn task_kind_for_task(os: &SystemOS, task_id: TaskId) -> Result { + let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; + Ok(task.kind) +} + fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result { let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; @@ -92,3 +199,17 @@ fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result LifecycleError { + match error { + ForegroundStackError::ResidentGameAlreadyExists { existing, requested } => { + LifecycleError::ResidentGameAlreadyExists { existing, requested } + } + ForegroundStackError::ForegroundShellAlreadyExists { existing, requested } => { + LifecycleError::ForegroundShellAlreadyExists { existing, requested } + } + ForegroundStackError::TaskIsNotResidentGame(task_id) => { + LifecycleError::TaskIsNotResidentGame(task_id) + } + } +} diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs index 42f8f3fe..9dd7246a 100644 --- a/crates/console/prometeu-system/src/os/facades/sessions.rs +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -12,7 +12,7 @@ impl<'a> SessionsFacade<'a> { let task_id = self.os.task_manager.create_game_task(process_id, app_id, title); self.os .lifecycle() - .set_foreground_task(task_id) + .set_game_foreground_task(task_id) .expect("newly created game task should have an associated process"); task_id @@ -24,7 +24,7 @@ impl<'a> SessionsFacade<'a> { let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title); self.os .lifecycle() - .set_foreground_task(task_id) + .set_shell_foreground_task(task_id) .expect("newly created shell task should have an associated process"); task_id @@ -36,7 +36,7 @@ impl<'a> SessionsFacade<'a> { let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title); self.os .lifecycle() - .set_foreground_task(task_id) + .set_shell_foreground_task(task_id) .expect("newly created native shell task should have an associated process"); task_id diff --git a/crates/console/prometeu-system/src/os/lifecycle.rs b/crates/console/prometeu-system/src/os/lifecycle.rs index bd771d47..f9274551 100644 --- a/crates/console/prometeu-system/src/os/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/lifecycle.rs @@ -15,4 +15,7 @@ pub enum LifecycleError { TaskNotFound(TaskId), ProcessNotFound(ProcessId), InvalidTransition { task_id: TaskId, from: TaskState, operation: LifecycleOperation }, + ResidentGameAlreadyExists { existing: TaskId, requested: TaskId }, + ForegroundShellAlreadyExists { existing: TaskId, requested: TaskId }, + TaskIsNotResidentGame(TaskId), } diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 2b4e38ec..95088e11 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -2,6 +2,7 @@ use crate::VirtualMachineRuntime; use crate::fs::{FsState, VirtualFS}; use crate::os::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade}; use crate::process::ProcessManager; +use crate::services::foreground::ForegroundStack; use crate::services::memcard::MemcardService; use crate::services::windows::WindowManager; use crate::task::TaskManager; @@ -14,6 +15,7 @@ pub struct SystemOS { pub(super) vm_runtime: VirtualMachineRuntime, pub(super) process_manager: ProcessManager, pub(super) task_manager: TaskManager, + pub(super) foreground_stack: ForegroundStack, pub(super) window_manager: WindowManager, pub(super) log_service: LogService, pub(super) fs: VirtualFS, @@ -33,6 +35,7 @@ impl SystemOS { ), process_manager: ProcessManager::new(), task_manager: TaskManager::new(), + foreground_stack: ForegroundStack::new(), window_manager: WindowManager::new(), log_service, fs: VirtualFS::new(), @@ -135,6 +138,7 @@ mod tests { use super::*; use crate::os::{LifecycleError, LifecycleOperation}; use crate::process::ProcessState; + use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState}; use crate::task::{TaskId, TaskState}; fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState { @@ -157,6 +161,102 @@ mod tests { assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running); } + #[test] + fn foreground_stack_starts_at_hub() { + let os = SystemOS::new(None); + + assert_eq!(os.foreground_stack.owner(), ForegroundOwner::Hub); + assert_eq!(os.foreground_stack.resident_game(), None); + } + + #[test] + fn game_session_registers_single_resident_game() { + let mut os = SystemOS::new(None); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); + assert_eq!( + os.lifecycle().resident_game(), + Some(ResidentGame { task_id, state: ResidentGameState::Foreground }) + ); + } + + #[test] + fn second_resident_game_is_rejected() { + let mut os = SystemOS::new(None); + let first = os.sessions().create_vm_game_task(42, "Sector Crawl"); + let process_id = os.process_manager.spawn_vm_game(43, "Second"); + let second = os.task_manager.create_game_task(process_id, 43, "Second"); + + assert_eq!( + os.lifecycle().set_game_foreground_task(second), + Err(LifecycleError::ResidentGameAlreadyExists { existing: first, requested: second }) + ); + } + + #[test] + fn home_request_moves_game_to_resident_suspended_and_hub_foreground() { + let mut os = SystemOS::new(None); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + os.lifecycle().request_home_from_game(task_id).expect("home request should succeed"); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub); + assert_eq!( + os.lifecycle().resident_game(), + Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended }) + ); + assert_eq!( + os.task_manager.get(task_id).expect("task should exist").state, + TaskState::Suspended + ); + assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended); + } + + #[test] + fn shell_foreground_keeps_game_resident_and_marks_it_suspended() { + 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"); + let shell = os.sessions().create_vm_shell_task(7, "Settings"); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); + assert_eq!( + os.lifecycle().resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) + ); + } + + #[test] + fn second_foreground_shell_is_rejected() { + let mut os = SystemOS::new(None); + let first = os.sessions().create_vm_shell_task(7, "Settings"); + let process_id = os.process_manager.spawn_vm_shell(8, "Files"); + let second = os.task_manager.create_shell_task(process_id, 8, "Files"); + + assert_eq!( + os.lifecycle().set_shell_foreground_task(second), + Err(LifecycleError::ForegroundShellAlreadyExists { + existing: first, + requested: second, + }) + ); + } + + #[test] + fn closing_foreground_shell_returns_to_hub() { + let mut os = SystemOS::new(None); + let shell = os.sessions().create_vm_shell_task(7, "Settings"); + + os.lifecycle().close_task(shell).expect("close should succeed"); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub); + assert_eq!( + os.task_manager.get(shell).expect("shell should remain").state, + TaskState::Closed + ); + } + #[test] fn suspend_task_marks_task_and_process_suspended() { let mut os = SystemOS::new(None); diff --git a/crates/console/prometeu-system/src/services/foreground.rs b/crates/console/prometeu-system/src/services/foreground.rs new file mode 100644 index 00000000..5e4f4446 --- /dev/null +++ b/crates/console/prometeu-system/src/services/foreground.rs @@ -0,0 +1,189 @@ +use crate::task::TaskId; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ForegroundOwner { + Hub, + Game(TaskId), + Shell(TaskId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResidentGameState { + Foreground, + PausedSuspended, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResidentGame { + pub task_id: TaskId, + pub state: ResidentGameState, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ForegroundStackError { + ResidentGameAlreadyExists { existing: TaskId, requested: TaskId }, + ForegroundShellAlreadyExists { existing: TaskId, requested: TaskId }, + TaskIsNotResidentGame(TaskId), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForegroundStack { + owner: ForegroundOwner, + resident_game: Option, +} + +impl Default for ForegroundStack { + fn default() -> Self { + Self { owner: ForegroundOwner::Hub, resident_game: None } + } +} + +impl ForegroundStack { + pub fn new() -> Self { + Self::default() + } + + pub fn owner(&self) -> ForegroundOwner { + self.owner + } + + pub fn resident_game(&self) -> Option { + self.resident_game + } + + pub fn resident_game_task(&self) -> Option { + self.resident_game.map(|game| game.task_id) + } + + pub fn set_game_foreground(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> { + if let Some(game) = self.resident_game + && game.task_id != task_id + { + return Err(ForegroundStackError::ResidentGameAlreadyExists { + existing: game.task_id, + requested: task_id, + }); + } + + self.resident_game = Some(ResidentGame { task_id, state: ResidentGameState::Foreground }); + self.owner = ForegroundOwner::Game(task_id); + Ok(()) + } + + pub fn request_home_from_game(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> { + match self.resident_game { + Some(game) if game.task_id == task_id => { + self.resident_game = + Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended }); + self.owner = ForegroundOwner::Hub; + Ok(()) + } + _ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)), + } + } + + pub fn set_shell_foreground(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> { + if let ForegroundOwner::Shell(existing) = self.owner + && existing != task_id + { + return Err(ForegroundStackError::ForegroundShellAlreadyExists { + existing, + requested: task_id, + }); + } + + if let Some(game) = self.resident_game { + self.resident_game = Some(ResidentGame { + task_id: game.task_id, + state: ResidentGameState::PausedSuspended, + }); + } + self.owner = ForegroundOwner::Shell(task_id); + Ok(()) + } + + pub fn return_to_hub(&mut self) { + self.owner = ForegroundOwner::Hub; + } + + pub fn clear_resident_game(&mut self, task_id: TaskId) { + if self.resident_game_task() == Some(task_id) { + self.resident_game = None; + } + + if self.owner == ForegroundOwner::Game(task_id) { + self.owner = ForegroundOwner::Hub; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn foreground_stack_starts_at_hub() { + let stack = ForegroundStack::new(); + + assert_eq!(stack.owner(), ForegroundOwner::Hub); + assert_eq!(stack.resident_game(), None); + } + + #[test] + fn setting_game_foreground_registers_resident_game() { + let mut stack = ForegroundStack::new(); + let game = TaskId(7); + + stack.set_game_foreground(game).expect("game should become foreground"); + + assert_eq!(stack.owner(), ForegroundOwner::Game(game)); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::Foreground }) + ); + } + + #[test] + fn second_resident_game_is_rejected() { + let mut stack = ForegroundStack::new(); + + stack.set_game_foreground(TaskId(1)).expect("first game should be accepted"); + + assert_eq!( + stack.set_game_foreground(TaskId(2)), + Err(ForegroundStackError::ResidentGameAlreadyExists { + existing: TaskId(1), + requested: TaskId(2), + }) + ); + } + + #[test] + fn home_request_moves_resident_game_to_paused_suspended() { + let mut stack = ForegroundStack::new(); + let game = TaskId(3); + + stack.set_game_foreground(game).expect("game should be foreground"); + stack.request_home_from_game(game).expect("home request should suspend game"); + + assert_eq!(stack.owner(), ForegroundOwner::Hub); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) + ); + } + + #[test] + fn shell_foreground_keeps_resident_game() { + let mut stack = ForegroundStack::new(); + let game = TaskId(3); + let shell = TaskId(4); + + stack.set_game_foreground(game).expect("game should be foreground"); + stack.request_home_from_game(game).expect("home request should suspend game"); + stack.set_shell_foreground(shell).expect("shell should become foreground"); + + assert_eq!(stack.owner(), ForegroundOwner::Shell(shell)); + assert_eq!(stack.resident_game_task(), Some(game)); + } +} diff --git a/crates/console/prometeu-system/src/services/mod.rs b/crates/console/prometeu-system/src/services/mod.rs index 1af5a6c7..4c32356b 100644 --- a/crates/console/prometeu-system/src/services/mod.rs +++ b/crates/console/prometeu-system/src/services/mod.rs @@ -1,4 +1,5 @@ pub mod async_work; +pub mod foreground; pub mod fs; pub mod game_library; pub mod memcard; diff --git a/discussion/index.ndjson b/discussion/index.ndjson index e6ee893f..b3c7f0c9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md b/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md index e3d29add..51c1e4b8 100644 --- a/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md +++ b/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md @@ -2,7 +2,7 @@ id: PLN-0137 ticket: foreground-stack-game-pause-shell-vm-backed title: Implement SystemOS Foreground Stack State -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, foreground, shell, game] From 29e002c5f3cd6c2f4ae7632d8e4bf9288b84f173 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Jul 2026 23:06:35 +0100 Subject: [PATCH 04/29] implements PLN-0138 --- discussion/index.ndjson | 2 +- ...cify-foreground-pause-and-home-contract.md | 2 +- docs/specs/runtime/04-gfx-peripheral.md | 6 +- docs/specs/runtime/06-input-peripheral.md | 18 ++++++ .../runtime/09-events-and-concurrency.md | 41 +++++++++++-- .../12-firmware-pos-and-prometeuhub.md | 60 +++++++++++++++++-- docs/specs/runtime/14-boot-profiles.md | 5 ++ 7 files changed, 121 insertions(+), 13 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index b3c7f0c9..3b283c0d 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md b/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md index 78ad1dea..6065559f 100644 --- a/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md +++ b/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md @@ -2,7 +2,7 @@ id: PLN-0138 ticket: foreground-stack-game-pause-shell-vm-backed title: Specify Foreground Pause and Home Contract -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0037] tags: [runtime, specs, lifecycle, input, renderer, foreground] diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index 05680535..d6801caa 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -239,8 +239,10 @@ The same physical surface may be reused by multiple visual owners, but logical ownership MUST remain explicit. Game pause/resume, foreground stack behavior, and coexistence of a paused Game -with VM-backed Shell apps are outside this contract and are tracked by the -foreground/lifecycle discussion. +with VM-backed Shell apps are defined by the foreground/lifecycle contract. The +render boundary's responsibility is narrower: foreground owner changes MUST +advance ownership, stale submissions MUST be discarded, and visual return to a +resumed Game MUST wait for a valid submission from the current ownership epoch. ### 4.7 Render telemetry diff --git a/docs/specs/runtime/06-input-peripheral.md b/docs/specs/runtime/06-input-peripheral.md index 3706584e..b91b9dd5 100644 --- a/docs/specs/runtime/06-input-peripheral.md +++ b/docs/specs/runtime/06-input-peripheral.md @@ -107,3 +107,21 @@ Input access is VM-owned and should not be reported as host syscall consumption. All platforms must provide the mandatory input elements (`pad`, `touch`, `button`) to the runtime. Platform differences in physical device mapping are resolved outside VM semantics. + +## 9 Host/System Controls + +Host/System controls are not part of the guest-visible input surface. + +The Home/SystemOS request changes machine foreground authority. It must be +handled by the host, firmware, or SystemOS before input is exposed to the Game. +It must not be represented as `pad.start`, `pad.select`, a new pad field, a VM +intrinsic, or a userland syscall. + +For the desktop host, `Esc` is the primary keyboard mapping for the +Home/SystemOS request. The physical `Home` key may be supported as an alias. +Both mappings are host controls and must not mutate `InputSignals`. + +When a Game leaves or re-enters foreground, pending Game input must be cleared +or barriered so held or pressed input cannot leak across pause/resume +boundaries. This barrier applies to the Game-facing snapshot; it does not +change the meaning of host/system Home controls. diff --git a/docs/specs/runtime/09-events-and-concurrency.md b/docs/specs/runtime/09-events-and-concurrency.md index 497c25c0..0e9a1606 100644 --- a/docs/specs/runtime/09-events-and-concurrency.md +++ b/docs/specs/runtime/09-events-and-concurrency.md @@ -100,7 +100,40 @@ Important properties: - no execution occurs outside the frame loop; - frame structure remains observable for host tooling and host-owned certification. -## 7 Render Worker Concurrency +## 7 Foreground Lifecycle Events + +Foreground lifecycle events are system events with bounded delivery points. +They do not execute arbitrary guest callbacks at arrival time. + +The Game pause path is: + +```text +Home/SystemOS request + -> pause event becomes visible to the Game + -> bounded pause reaction budget + -> SystemOS suspension, even if the Game did not cooperate +``` + +The Game resume path is: + +```text +SystemOS resume request + -> VM reactivation + -> resume or foreground-restore event becomes visible to the Game + -> Game may synchronize while still internally paused + -> visual foreground returns after a valid current-epoch render submission +``` + +`Paused` is the Game-visible event/state. `Suspended` is the OS-owned +scheduler/runtime state. A Game may observe pause/resume, but it must not +control whether SystemOS suspends or resumes the VM. + +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 +contract. + +## 8 Render Worker Concurrency The render worker is not a machine-visible event source and does not introduce guest callbacks. It is an implementation-side consumer of closed render @@ -127,7 +160,7 @@ Shutdown is explicit and bounded. A shutdown request wakes a waiting worker, causes pending work that will not be consumed to be discarded, and reports a typed failure if the worker cannot join within the configured timeout. -## 8 Async Asset and IO Work Lane +## 9 Async Asset and IO Work Lane The asset/IO async work lane is not a machine-visible event source and does not introduce guest callbacks. It is an implementation-side lane for asset @@ -151,7 +184,7 @@ does not publish resident graphics/audio/scene state directly. FS and game persistence services may consume this lane for IO-style work, but public FS API shape is defined by the FS/app-home contract, not by this chapter. -## 9 Determinism and Best Practices +## 10 Determinism and Best Practices PROMETEU encourages: @@ -166,7 +199,7 @@ PROMETEU discourages: - hidden timing channels; - ambiguous out-of-band execution. -## 10 Relationship to Other Specs +## 11 Relationship to Other Specs - [`09a-coroutines-and-cooperative-scheduling.md`](09a-coroutines-and-cooperative-scheduling.md) defines coroutine lifecycle and scheduling behavior. - [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces. diff --git a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md index 86a25bab..3958b3b5 100644 --- a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md +++ b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md @@ -43,6 +43,13 @@ 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. +- **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 + prepare for foreground loss. +- **Suspended**: an OS-owned scheduler/runtime state. A suspended Game remains + resident but does not receive normal gameplay ticks, normal Game input, or + frame pacing. ## 3 POS Responsibilities @@ -154,7 +161,50 @@ Shell task loses its eligible focused window -> FirmwareState::HubHome ``` -## 7 Cartridge Load Flow +## 7 Foreground Stack and Game Pause + +The v1 foreground contract is single-owner: + +- exactly one foreground visual owner exists at a time; +- at most one Game may be resident; +- at most one Shell app may be foreground; +- Hub/Home is the root Shell owner and back-stack controller. + +The minimal supported navigation cycle is: + +```text +Game foreground + -> Home request + -> Game pause notification + -> Game suspended by SystemOS after a bounded budget + -> Hub/Home foreground + -> optional Shell foreground + -> Shell close returns to Hub/Home + -> same Game resume/foreground-restore notification + -> Game foreground after a valid current-epoch render submission +``` + +`Paused` is cooperative and visible to the Game. `Suspended` is imposed by +SystemOS. The Game may react to pause/resume, but it must not control whether +the OS suspends or resumes its VM. + +When Home is requested during Game execution, SystemOS must notify the Game and +grant a short bounded pause budget. After the budget, SystemOS may suspend the +VM even if the Game did not cooperate. While suspended, the Game must not +receive normal gameplay ticks, normal Game input, or frame pacing. + +When returning to the same Game, SystemOS must reactivate the VM and send a +resume or foreground-restore notification before visual foreground is restored. +The Game may remain internally paused while it synchronizes. Hub/Shell remains +visible until the resumed Game publishes a valid render submission for the +current ownership epoch. + +Input pending for the Game must be cleared or barriered across pause/resume +boundaries. Game audio pauses with the Game VM in v1. Independent background +audio, real background execution, multiple resident Games, multiple foreground +Shells, and direct Game-to-Game switching are outside this contract. + +## 8 Cartridge Load Flow Current high-level flow: @@ -179,7 +229,7 @@ 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. -## 8 Firmware States +## 9 Firmware States The current firmware state model includes: @@ -193,7 +243,7 @@ The current firmware state model includes: These states express machine orchestration above the VM. They are not guest-visible bytecode states. -## 9 Execution Contract +## 10 Execution Contract For game-mode execution, firmware/runtime coordination preserves: @@ -204,7 +254,7 @@ For game-mode execution, firmware/runtime coordination preserves: This keeps the machine model deterministic and observable. -## 10 Crash Handling +## 11 Crash Handling Firmware owns terminal fault presentation. @@ -216,7 +266,7 @@ When a terminal app fault occurs: Crash handling is outside the guest VM execution model. -## 11 Relationship to Other Specs +## 12 Relationship to Other Specs - [`02-vm-instruction-set.md`](02-vm-instruction-set.md) defines the VM subsystem run by firmware. - [`09-events-and-concurrency.md`](09-events-and-concurrency.md) defines the frame-boundary model used by firmware. diff --git a/docs/specs/runtime/14-boot-profiles.md b/docs/specs/runtime/14-boot-profiles.md index ec9611c6..285763c9 100644 --- a/docs/specs/runtime/14-boot-profiles.md +++ b/docs/specs/runtime/14-boot-profiles.md @@ -99,6 +99,11 @@ 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. +The foreground/home contract defines `Game -> Home/Shell -> same Game` after a +Game is already running. It does not change direct `--run`, debugger direct +boot, or `--games-root` Home library startup semantics. Home/SystemOS requests +are host/system controls, not guest cartridge boot APIs. + ## 5 Firmware State Relationship Boot target selection feeds into firmware states such as: From 1b29b2f985d2385c74f59cf49a4c0ca4ca8267a3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Jul 2026 23:10:03 +0100 Subject: [PATCH 05/29] implements PLN-0140 --- .../src/firmware/firmware.rs | 51 +++++++- .../firmware/firmware_step_game_running.rs | 15 ++- crates/console/prometeu-system/src/lib.rs | 5 +- .../src/os/facades/lifecycle.rs | 65 +++++++-- .../prometeu-system/src/os/lifecycle.rs | 14 ++ crates/console/prometeu-system/src/os/mod.rs | 5 +- .../prometeu-system/src/os/system_os.rs | 33 ++++- .../src/services/foreground.rs | 123 +++++++++++++++++- discussion/index.ndjson | 2 +- ...eliver-game-pause-resume-and-suspension.md | 2 +- 10 files changed, 294 insertions(+), 21 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 365717eb..2e32863b 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -170,6 +170,14 @@ impl Firmware { pub fn request_home_from_host(&mut self) { self.os.info(LogSource::Pos, "Host requested Home/SystemOS".to_string()); + if let FirmwareState::GameRunning(step) = &self.state { + if let Err(error) = self.os.lifecycle().request_home_from_game(step.task_id) { + self.os.warn( + LogSource::Pos, + format!("Home/SystemOS request could not pause Game: {error:?}"), + ); + } + } } } @@ -188,7 +196,10 @@ mod tests { use prometeu_system::process::ProcessState; use prometeu_system::task::{TaskId, TaskState}; use prometeu_system::windows::WindowOwner; - use prometeu_system::{CrashReport, discover_games_root}; + use prometeu_system::{ + CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState, + discover_games_root, + }; use std::fs; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; @@ -732,6 +743,44 @@ mod tests { assert_eq!(firmware.os.windows().window_count(), 0); } + #[test] + fn host_home_request_pauses_then_suspends_game_before_next_vm_tick() { + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::Game)); + firmware.tick(&signals, &mut platform); + + let task_id = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected GameRunning state, got {:?}", other), + }; + let tick_index_before_home = firmware.os.vm().tick_index(); + + firmware.request_home_from_host(); + + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); + assert_eq!( + firmware.os.lifecycle().resident_game().map(|game| game.state), + Some(ResidentGameState::PauseRequested { remaining_ticks: 1 }) + ); + assert_eq!( + firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::Pause) + ); + + firmware.tick(&signals, &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Suspended)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(task_id), + Ok(ProcessState::Suspended) + ); + assert_eq!(firmware.os.vm().tick_index(), tick_index_before_home); + } + #[test] fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() { assert!(Syscall::from_u32(0x0002).is_none()); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 911e978b..9535187a 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -1,4 +1,4 @@ -use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState}; +use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_system::CrashReport; @@ -36,6 +36,19 @@ impl GameRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } + match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) { + Ok(true) => return Some(FirmwareState::HubHome(HubHomeStep)), + Ok(false) => {} + Err(_) => { + let report = CrashReport::VmPanic { + message: "failed to advance game pause budget".to_string(), + pc: None, + }; + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + } + let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform); if let Some(report) = result { diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 02a87c1d..c297a6fe 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -4,7 +4,10 @@ mod programs; mod services; pub use crash_report::CrashReport; -pub use os::{LifecycleError, LifecycleOperation, SystemOS}; +pub use os::{ + DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, + LifecycleOperation, SystemOS, +}; pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink}; pub use services::async_work::{ diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index 1d7c3e38..49f58942 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -1,8 +1,13 @@ use crate::CrashReport; use crate::os::SystemOS; -use crate::os::{LifecycleError, LifecycleOperation}; +use crate::os::{ + DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, + LifecycleOperation, +}; use crate::process::{ProcessId, ProcessState}; -use crate::services::foreground::{ForegroundOwner, ForegroundStackError, ResidentGame}; +use crate::services::foreground::{ + ForegroundOwner, ForegroundStackError, ResidentGame, ResidentGameState, +}; use crate::task::{TaskId, TaskKind, TaskState}; pub struct LifecycleFacade<'a> { @@ -79,15 +84,47 @@ impl<'a> LifecycleFacade<'a> { } pub fn request_home_from_game(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { - let process_id = process_id_for_task(self.os, task_id)?; + let _process_id = process_id_for_task(self.os, task_id)?; + let previous = self.os.foreground_stack.resident_game(); - self.os.foreground_stack.request_home_from_game(task_id).map_err(map_foreground_error)?; - self.os.task_manager.mark_suspended(task_id); - self.os.process_manager.mark_suspended(process_id); + self.os + .foreground_stack + .request_home_from_game(task_id, DEFAULT_GAME_PAUSE_BUDGET_TICKS) + .map_err(map_foreground_error)?; + + if !matches!( + previous.map(|game| game.state), + Some(ResidentGameState::PauseRequested { .. } | ResidentGameState::PausedSuspended) + ) { + self.os + .game_lifecycle_events + .push(GameLifecycleEvent { task_id, kind: GameLifecycleEventKind::Pause }); + } Ok(()) } + pub fn advance_game_pause_budget(&mut self, task_id: TaskId) -> Result { + let expired = + self.os.foreground_stack.advance_pause_budget(task_id).map_err(map_foreground_error)?; + + if expired { + let process_id = process_id_for_task(self.os, task_id)?; + self.os.task_manager.mark_suspended(task_id); + self.os.process_manager.mark_suspended(process_id); + } + + Ok(expired) + } + + pub fn pending_game_lifecycle_events(&self) -> &[GameLifecycleEvent] { + &self.os.game_lifecycle_events + } + + pub fn take_game_lifecycle_events(&mut self) -> Vec { + std::mem::take(&mut self.os.game_lifecycle_events) + } + pub fn return_to_hub(&mut self) { self.os.foreground_stack.return_to_hub(); self.os.task_manager.clear_foreground(); @@ -102,7 +139,11 @@ impl<'a> LifecycleFacade<'a> { ForegroundOwner::Game(owner) if owner == task_id => { self.os .foreground_stack - .request_home_from_game(task_id) + .request_home_from_game(task_id, DEFAULT_GAME_PAUSE_BUDGET_TICKS) + .map_err(map_foreground_error)?; + self.os + .foreground_stack + .advance_pause_budget(task_id) .map_err(map_foreground_error)?; } ForegroundOwner::Shell(owner) if owner == task_id => { @@ -132,10 +173,18 @@ impl<'a> LifecycleFacade<'a> { TaskKind::Game => { self.os .foreground_stack - .set_game_foreground(task_id) + .request_resume_game(task_id) .map_err(map_foreground_error)?; self.os.task_manager.set_foreground(task_id); self.os.process_manager.mark_running(process_id); + self.os.game_lifecycle_events.push(GameLifecycleEvent { + task_id, + kind: GameLifecycleEventKind::ResumeForeground, + }); + self.os + .foreground_stack + .complete_resume_game(task_id) + .map_err(map_foreground_error)?; } TaskKind::Shell => { self.os diff --git a/crates/console/prometeu-system/src/os/lifecycle.rs b/crates/console/prometeu-system/src/os/lifecycle.rs index f9274551..7e1b437c 100644 --- a/crates/console/prometeu-system/src/os/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/lifecycle.rs @@ -1,6 +1,20 @@ use crate::process::ProcessId; use crate::task::{TaskId, TaskState}; +pub const DEFAULT_GAME_PAUSE_BUDGET_TICKS: u8 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GameLifecycleEventKind { + Pause, + ResumeForeground, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GameLifecycleEvent { + pub task_id: TaskId, + pub kind: GameLifecycleEventKind, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LifecycleOperation { SetForeground, diff --git a/crates/console/prometeu-system/src/os/mod.rs b/crates/console/prometeu-system/src/os/mod.rs index a438d5cc..f14a8b5e 100644 --- a/crates/console/prometeu-system/src/os/mod.rs +++ b/crates/console/prometeu-system/src/os/mod.rs @@ -3,5 +3,8 @@ mod lifecycle; mod system_os; pub use facades::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade}; -pub use lifecycle::{LifecycleError, LifecycleOperation}; +pub use lifecycle::{ + DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, + LifecycleOperation, +}; pub use system_os::SystemOS; diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 95088e11..ff25db5e 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -1,6 +1,8 @@ use crate::VirtualMachineRuntime; use crate::fs::{FsState, VirtualFS}; -use crate::os::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade}; +use crate::os::{ + FsFacade, GameLifecycleEvent, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade, +}; use crate::process::ProcessManager; use crate::services::foreground::ForegroundStack; use crate::services::memcard::MemcardService; @@ -16,6 +18,7 @@ pub struct SystemOS { pub(super) process_manager: ProcessManager, pub(super) task_manager: TaskManager, pub(super) foreground_stack: ForegroundStack, + pub(super) game_lifecycle_events: Vec, pub(super) window_manager: WindowManager, pub(super) log_service: LogService, pub(super) fs: VirtualFS, @@ -36,6 +39,7 @@ impl SystemOS { process_manager: ProcessManager::new(), task_manager: TaskManager::new(), foreground_stack: ForegroundStack::new(), + game_lifecycle_events: Vec::new(), window_manager: WindowManager::new(), log_service, fs: VirtualFS::new(), @@ -136,7 +140,7 @@ impl SystemOS { #[cfg(test)] mod tests { use super::*; - use crate::os::{LifecycleError, LifecycleOperation}; + use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation}; use crate::process::ProcessState; use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState}; use crate::task::{TaskId, TaskState}; @@ -195,12 +199,30 @@ mod tests { } #[test] - fn home_request_moves_game_to_resident_suspended_and_hub_foreground() { + fn home_request_notifies_game_before_budget_suspends_it() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().request_home_from_game(task_id).expect("home request should succeed"); + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); + assert_eq!( + os.lifecycle().resident_game(), + Some(ResidentGame { + task_id, + state: ResidentGameState::PauseRequested { remaining_ticks: 1 }, + }) + ); + assert_eq!( + os.lifecycle().pending_game_lifecycle_events()[0].kind, + GameLifecycleEventKind::Pause + ); + assert_eq!( + os.task_manager.get(task_id).expect("task should exist").state, + TaskState::Foreground + ); + + assert!(os.lifecycle().advance_game_pause_budget(task_id).expect("budget should advance")); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub); assert_eq!( os.lifecycle().resident_game(), @@ -218,6 +240,7 @@ mod tests { 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().foreground_owner(), ForegroundOwner::Shell(shell)); @@ -284,6 +307,10 @@ mod tests { TaskState::Foreground ); assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running); + assert_eq!( + os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::ResumeForeground) + ); } #[test] diff --git a/crates/console/prometeu-system/src/services/foreground.rs b/crates/console/prometeu-system/src/services/foreground.rs index 5e4f4446..1b2e8bc1 100644 --- a/crates/console/prometeu-system/src/services/foreground.rs +++ b/crates/console/prometeu-system/src/services/foreground.rs @@ -10,6 +10,8 @@ pub enum ForegroundOwner { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ResidentGameState { Foreground, + PauseRequested { remaining_ticks: u8 }, + ResumeRequested, PausedSuspended, } @@ -70,12 +72,81 @@ impl ForegroundStack { Ok(()) } - pub fn request_home_from_game(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> { + pub fn request_home_from_game( + &mut self, + task_id: TaskId, + pause_budget_ticks: u8, + ) -> Result<(), ForegroundStackError> { match self.resident_game { Some(game) if game.task_id == task_id => { + if !matches!( + game.state, + ResidentGameState::PauseRequested { .. } | ResidentGameState::PausedSuspended + ) { + self.resident_game = Some(ResidentGame { + task_id, + state: ResidentGameState::PauseRequested { + remaining_ticks: pause_budget_ticks.max(1), + }, + }); + } + Ok(()) + } + _ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)), + } + } + + pub fn advance_pause_budget(&mut self, task_id: TaskId) -> Result { + match self.resident_game { + Some(game) + if game.task_id == task_id + && matches!( + game.state, + ResidentGameState::PauseRequested { remaining_ticks: 1 } + ) => + { self.resident_game = Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended }); self.owner = ForegroundOwner::Hub; + Ok(true) + } + Some(game) + if game.task_id == task_id + && matches!(game.state, ResidentGameState::PauseRequested { .. }) => + { + let ResidentGameState::PauseRequested { remaining_ticks } = game.state else { + unreachable!(); + }; + self.resident_game = Some(ResidentGame { + task_id, + state: ResidentGameState::PauseRequested { + remaining_ticks: remaining_ticks.saturating_sub(1).max(1), + }, + }); + Ok(false) + } + Some(game) if game.task_id == task_id => Ok(false), + _ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)), + } + } + + pub fn request_resume_game(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> { + match self.resident_game { + Some(game) if game.task_id == task_id => { + self.resident_game = + Some(ResidentGame { task_id, state: ResidentGameState::ResumeRequested }); + Ok(()) + } + _ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)), + } + } + + pub fn complete_resume_game(&mut self, task_id: TaskId) -> Result<(), ForegroundStackError> { + match self.resident_game { + Some(game) if game.task_id == task_id => { + self.resident_game = + Some(ResidentGame { task_id, state: ResidentGameState::Foreground }); + self.owner = ForegroundOwner::Game(task_id); Ok(()) } _ => Err(ForegroundStackError::TaskIsNotResidentGame(task_id)), @@ -159,13 +230,32 @@ mod tests { } #[test] - fn home_request_moves_resident_game_to_paused_suspended() { + fn home_request_moves_resident_game_to_pause_requested() { let mut stack = ForegroundStack::new(); let game = TaskId(3); stack.set_game_foreground(game).expect("game should be foreground"); - stack.request_home_from_game(game).expect("home request should suspend game"); + stack.request_home_from_game(game, 2).expect("home request should pause game"); + assert_eq!(stack.owner(), ForegroundOwner::Game(game)); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { + task_id: game, + state: ResidentGameState::PauseRequested { remaining_ticks: 2 }, + }) + ); + } + + #[test] + fn pause_budget_expiry_suspends_game_and_returns_to_hub() { + let mut stack = ForegroundStack::new(); + let game = TaskId(3); + + stack.set_game_foreground(game).expect("game should be foreground"); + stack.request_home_from_game(game, 1).expect("home request should pause game"); + + assert!(stack.advance_pause_budget(game).expect("budget should advance")); assert_eq!(stack.owner(), ForegroundOwner::Hub); assert_eq!( stack.resident_game(), @@ -173,6 +263,30 @@ mod tests { ); } + #[test] + fn resume_request_precedes_game_foreground_restore() { + let mut stack = ForegroundStack::new(); + let game = TaskId(3); + + stack.set_game_foreground(game).expect("game should be foreground"); + stack.request_home_from_game(game, 1).expect("home request should pause game"); + stack.advance_pause_budget(game).expect("budget should expire"); + + stack.request_resume_game(game).expect("resume should be requested"); + assert_eq!(stack.owner(), ForegroundOwner::Hub); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::ResumeRequested }) + ); + + stack.complete_resume_game(game).expect("resume should complete"); + assert_eq!(stack.owner(), ForegroundOwner::Game(game)); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::Foreground }) + ); + } + #[test] fn shell_foreground_keeps_resident_game() { let mut stack = ForegroundStack::new(); @@ -180,7 +294,8 @@ mod tests { let shell = TaskId(4); stack.set_game_foreground(game).expect("game should be foreground"); - stack.request_home_from_game(game).expect("home request should suspend game"); + stack.request_home_from_game(game, 1).expect("home request should suspend game"); + stack.advance_pause_budget(game).expect("budget should expire"); stack.set_shell_foreground(shell).expect("shell should become foreground"); assert_eq!(stack.owner(), ForegroundOwner::Shell(shell)); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 3b283c0d..39526a2f 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md b/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md index 32cf8eba..e7ff2832 100644 --- a/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md +++ b/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md @@ -2,7 +2,7 @@ id: PLN-0140 ticket: foreground-stack-game-pause-shell-vm-backed title: Deliver Game Pause Resume and Suspension -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0037] tags: [runtime, os, vm, lifecycle, pause, suspension] From 999136fef301e9e7d6bf508ae7acb094d0fb6587 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Jul 2026 23:15:11 +0100 Subject: [PATCH 06/29] implements PLN-0141 --- .../src/firmware/firmware.rs | 111 ++++++++++++++++-- .../firmware/firmware_step_game_running.rs | 6 +- .../prometeu-system/src/os/facades/vm.rs | 14 ++- .../prometeu-host-desktop-winit/src/input.rs | 16 +++ .../prometeu-host-desktop-winit/src/runner.rs | 71 ++++++++--- discussion/index.ndjson | 2 +- ...render-audio-and-input-pause-boundaries.md | 2 +- 7 files changed, 193 insertions(+), 29 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 2e32863b..d9e1669a 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -1,11 +1,14 @@ use crate::firmware::boot_target::BootTarget; -use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep}; +use crate::firmware::firmware_state::{ + FirmwareState, GameRunningStep, LoadCartridgeStep, ResetStep, +}; use crate::firmware::prometeu_context::PrometeuContext; +use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::LogSource; use prometeu_hal::telemetry::CertificationConfig; use prometeu_hal::{InputSignals, RuntimePlatform}; -use prometeu_system::{PrometeuHub, SystemOS}; +use prometeu_system::{PrometeuHub, ResidentGameState, SystemOS}; use prometeu_vm::VirtualMachine; /// PROMETEU Firmware. @@ -36,6 +39,7 @@ pub struct Firmware { pub boot_target: BootTarget, /// State-machine lifecycle tracker. state_initialized: bool, + game_input_barrier_frames: u8, } impl Firmware { @@ -48,6 +52,7 @@ impl Firmware { state: FirmwareState::Reset(ResetStep), boot_target: BootTarget::Hub, state_initialized: false, + game_input_barrier_frames: 0, } } @@ -56,23 +61,31 @@ impl Firmware { /// This method is called exactly once per Host frame (60Hz). /// It updates peripheral signals and delegates the logic to the current state. pub fn tick(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { + let barriered_signals = InputSignals::default(); + let effective_signals = if self.game_input_barrier_frames > 0 { + self.game_input_barrier_frames -= 1; + &barriered_signals + } else { + signals + }; + // 0. Process asset commits at the beginning of the frame boundary. platform.assets_mut().apply_commits(); // 1. Update the peripheral state using the latest signals from the Host. // This ensures input is consistent throughout the entire update. - platform.input_mut().pad_mut().begin_frame(signals); - platform.input_mut().touch_mut().begin_frame(signals); + platform.input_mut().pad_mut().begin_frame(effective_signals); + platform.input_mut().touch_mut().begin_frame(effective_signals); // 2. State machine lifecycle management. if !self.state_initialized { - self.on_enter(signals, platform); + self.on_enter(effective_signals, platform); self.state_initialized = true; } // 3. Update the current state and check for transitions. - if let Some(next_state) = self.on_update(signals, platform) { - self.change_state(next_state, signals, platform); + if let Some(next_state) = self.on_update(effective_signals, platform) { + self.change_state(next_state, effective_signals, platform); } } @@ -176,9 +189,39 @@ impl Firmware { LogSource::Pos, format!("Home/SystemOS request could not pause Game: {error:?}"), ); + } else { + self.game_input_barrier_frames = 1; } } } + + pub fn resume_resident_game_from_home(&mut self) -> bool { + let Some(task_id) = self.os.lifecycle().resident_game_task() else { + return false; + }; + + if self.os.lifecycle().resume_task(task_id).is_err() { + return false; + } + + let app_id = self.os.vm().current_app_id(); + self.os.vm().transition_render_owner(AppMode::Game, app_id); + self.state = FirmwareState::GameRunning(GameRunningStep::new(task_id)); + self.state_initialized = false; + self.game_input_barrier_frames = 1; + true + } + + pub fn game_lifecycle_audio_paused(&mut self) -> bool { + matches!( + self.os.lifecycle().resident_game().map(|game| game.state), + Some( + ResidentGameState::PauseRequested { .. } + | ResidentGameState::PausedSuspended + | ResidentGameState::ResumeRequested, + ) + ) + } } #[cfg(test)] @@ -188,6 +231,7 @@ mod tests { use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_drivers::TestPlatform; + use prometeu_hal::RuntimePlatform; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; @@ -757,9 +801,11 @@ mod tests { other => panic!("expected GameRunning state, got {:?}", other), }; let tick_index_before_home = firmware.os.vm().tick_index(); + let render_ownership_before_home = firmware.os.vm().active_render_ownership(); firmware.request_home_from_host(); + assert!(firmware.game_lifecycle_audio_paused()); assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); assert_eq!( firmware.os.lifecycle().resident_game().map(|game| game.state), @@ -770,7 +816,9 @@ mod tests { Some(GameLifecycleEventKind::Pause) ); - firmware.tick(&signals, &mut platform); + let held_signals = + InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals }; + firmware.tick(&held_signals, &mut platform); assert!(matches!(firmware.state, FirmwareState::HubHome(_))); assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Suspended)); @@ -779,6 +827,53 @@ mod tests { Ok(ProcessState::Suspended) ); assert_eq!(firmware.os.vm().tick_index(), tick_index_before_home); + assert!(!platform.input().pad().any()); + assert!(!platform.input().touch().f().down); + + 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); + } + + #[test] + fn resident_game_resume_restores_render_owner_after_resume_event_and_barriers_input() { + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::Game)); + firmware.tick(&signals, &mut platform); + + let task_id = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected GameRunning state, got {:?}", other), + }; + + firmware.request_home_from_host(); + firmware.tick(&signals, &mut platform); + let hub_ownership = firmware.os.vm().active_render_ownership(); + + assert!(firmware.resume_resident_game_from_home()); + assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + assert!(!firmware.game_lifecycle_audio_paused()); + assert_eq!( + firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::ResumeForeground) + ); + + 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 }; + firmware.tick(&held_signals, &mut platform); + + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground)); + assert!(!platform.input().pad().any()); + assert!(!platform.input().touch().f().down); } #[test] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 9535187a..7deac3b8 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -1,5 +1,6 @@ use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep}; use crate::firmware::prometeu_context::PrometeuContext; +use prometeu_hal::app_mode::AppMode; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_system::CrashReport; use prometeu_system::task::{TaskId, TaskState}; @@ -37,7 +38,10 @@ impl GameRunningStep { } match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) { - Ok(true) => return Some(FirmwareState::HubHome(HubHomeStep)), + Ok(true) => { + ctx.os.vm().transition_render_owner(AppMode::Shell, 0); + return Some(FirmwareState::HubHome(HubHomeStep)); + } Ok(false) => {} Err(_) => { let report = CrashReport::VmPanic { diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 5335aab6..f112f4e5 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -5,8 +5,8 @@ use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; use prometeu_hal::{ - FrameId, InputSignals, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, - RuntimePlatform, + FrameId, InputSignals, RenderOwnership, RenderWorkerBackend, RenderWorkerError, + RenderWorkerFrameSink, RuntimePlatform, }; use prometeu_vm::VirtualMachine; use std::sync::atomic::Ordering; @@ -120,6 +120,16 @@ impl<'a> VmFacade<'a> { self.os.vm_runtime.record_repeated_render_worker_frame(frame_id); } + pub fn active_render_ownership(&self) -> RenderOwnership { + self.os.vm_runtime.render_manager.active_ownership() + } + + pub fn transition_render_owner(&mut self, app_mode: AppMode, app_id: u32) { + self.os.vm_runtime.render_manager.transition_render_owner(app_mode, app_id); + self.os.vm_runtime.sync_render_worker_ownership(); + self.os.vm_runtime.render_manager.sync_telemetry(&self.os.vm_runtime.atomic_telemetry); + } + pub fn cert_config(&self) -> &CertificationConfig { &self.os.vm_runtime.certifier.config } diff --git a/crates/host/prometeu-host-desktop-winit/src/input.rs b/crates/host/prometeu-host-desktop-winit/src/input.rs index 06626cab..68ab234c 100644 --- a/crates/host/prometeu-host-desktop-winit/src/input.rs +++ b/crates/host/prometeu-host-desktop-winit/src/input.rs @@ -83,6 +83,10 @@ impl HostInputHandler { self.system_controls.take_home_requested() } + pub fn clear_guest_signals(&mut self) { + self.signals = InputSignals::default(); + } + pub fn handle_key_code(&mut self, code: KeyCode, state: ElementState) { let is_down = state == ElementState::Pressed; @@ -229,4 +233,16 @@ mod tests { assert!(input.signals.up_signal); assert!(!input.take_home_requested()); } + + #[test] + fn clearing_guest_signals_preserves_system_home_control() { + let mut input = HostInputHandler::default(); + + input.handle_key_code(KeyCode::ArrowUp, ElementState::Pressed); + input.handle_key_code(KeyCode::Escape, ElementState::Pressed); + input.clear_guest_signals(); + + assert_no_pad_signal(input.signals); + assert!(input.take_home_requested()); + } } diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index e40c89cd..0bb7611e 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -10,8 +10,8 @@ use pixels::{Pixels, PixelsBuilder, SurfaceTexture}; use prometeu_drivers::hardware::Hardware; use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks}; use prometeu_firmware::{BootTarget, Firmware, FirmwareState}; -use prometeu_hal::RuntimePlatform; use prometeu_hal::telemetry::CertificationConfig; +use prometeu_hal::{OwnedRgba8888Frame, RenderOwnership, RuntimePlatform}; use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -88,8 +88,12 @@ fn desired_control_flow( } } -fn should_present_worker_frame(state: &FirmwareState) -> bool { - matches!(state, FirmwareState::GameRunning(_)) +fn should_present_worker_frame( + state: &FirmwareState, + frame: &OwnedRgba8888Frame, + active_ownership: RenderOwnership, +) -> bool { + matches!(state, FirmwareState::GameRunning(_)) && frame.ownership == active_ownership } /// The Desktop implementation of the PROMETEU Runtime. @@ -312,8 +316,13 @@ impl ApplicationHandler for HostRunner { { let frame = pixels.frame_mut(); - if should_present_worker_frame(&self.firmware.state) - && let Some(worker_frame) = self.render_frame_store.latest_frame() + let active_ownership = self.firmware.os.vm().active_render_ownership(); + if let Some(worker_frame) = self.render_frame_store.latest_frame() + && should_present_worker_frame( + &self.firmware.state, + &worker_frame, + active_ownership, + ) { self.firmware .os @@ -373,6 +382,7 @@ impl ApplicationHandler for HostRunner { } if self.input.take_home_requested() { + self.input.clear_guest_signals(); self.firmware.request_home_from_host(); self.invalidate_host_surface(); self.request_redraw_if_needed(); @@ -405,7 +415,10 @@ impl ApplicationHandler for HostRunner { // Sync pause state with audio. // We do this AFTER firmware.tick to avoid MasterPause/Resume commands // being cleared by the OS if a new logical frame starts in this tick. - let is_paused = self.firmware.os.vm().paused() || self.debugger.waiting_for_start; + let lifecycle_audio_paused = self.firmware.game_lifecycle_audio_paused(); + let is_paused = lifecycle_audio_paused + || self.firmware.os.vm().paused() + || self.debugger.waiting_for_start; if is_paused != self.last_paused_state { self.last_paused_state = is_paused; let cmd = @@ -420,8 +433,9 @@ impl ApplicationHandler for HostRunner { self.stats.record_frame(); } - if should_present_worker_frame(&self.firmware.state) - && let Some(worker_frame) = self.render_frame_store.latest_frame() + let active_ownership = self.firmware.os.vm().active_render_ownership(); + 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()); } else { @@ -542,14 +556,39 @@ mod tests { } #[test] - fn worker_frame_presentation_is_game_only() { - assert!(should_present_worker_frame(&FirmwareState::GameRunning(GameRunningStep::new( - TaskId(1), - )))); - assert!(!should_present_worker_frame(&FirmwareState::ShellRunning(ShellRunningStep::new( - TaskId(2) - ),))); - assert!(!should_present_worker_frame(&FirmwareState::HubHome(HubHomeStep))); + fn worker_frame_presentation_requires_game_state_and_current_ownership() { + let active = RenderOwnership::new(3, AppMode::Game, 1); + let current = OwnedRgba8888Frame::packed(FrameId::new(9), active, 1, 1, vec![0xFF00FFFF]) + .expect("test frame"); + let stale = OwnedRgba8888Frame::packed( + FrameId::new(8), + RenderOwnership::new(2, AppMode::Game, 1), + 1, + 1, + vec![0xFF00FFFF], + ) + .expect("test frame"); + + assert!(should_present_worker_frame( + &FirmwareState::GameRunning(GameRunningStep::new(TaskId(1))), + ¤t, + active, + )); + assert!(!should_present_worker_frame( + &FirmwareState::GameRunning(GameRunningStep::new(TaskId(1),)), + &stale, + active, + )); + assert!(!should_present_worker_frame( + &FirmwareState::ShellRunning(ShellRunningStep::new(TaskId(2))), + ¤t, + active, + )); + assert!(!should_present_worker_frame( + &FirmwareState::HubHome(HubHomeStep), + ¤t, + active, + )); } #[test] diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 39526a2f..a0aa7b43 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md b/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md index 710caeb2..b10f2d88 100644 --- a/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md +++ b/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md @@ -2,7 +2,7 @@ id: PLN-0141 ticket: foreground-stack-game-pause-shell-vm-backed title: Integrate Render Audio and Input Pause Boundaries -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0037] tags: [runtime, renderer, audio, input, foreground, lifecycle] From 33f70d1c6e62fb045a1eb76c508a1d3a86bbcb87 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 13:10:55 +0100 Subject: [PATCH 07/29] implements PLN-0139 --- .../src/firmware/firmware.rs | 57 +++++++++++++++++++ discussion/index.ndjson | 2 +- ...alidate-game-home-shell-game-end-to-end.md | 2 +- .../12-firmware-pos-and-prometeuhub.md | 25 +++++++- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index d9e1669a..fe083683 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -876,6 +876,63 @@ mod tests { assert!(!platform.input().touch().f().down); } + #[test] + fn game_home_shell_hub_same_game_flow_is_end_to_end_deterministic() { + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::Game)); + firmware.tick(&signals, &mut platform); + + let game_task = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected GameRunning state, got {:?}", other), + }; + + firmware.request_home_from_host(); + firmware.tick(&signals, &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(), Some(game_task)); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + + firmware.tick(&signals, &mut platform); + let shell_a_click = + InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() }; + firmware.tick(&shell_a_click, &mut platform); + + let shell_task = match &firmware.state { + FirmwareState::ShellRunning(step) => step.task_id, + other => panic!("expected ShellRunning state, got {:?}", other), + }; + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell_task)); + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + + let close_shell = InputSignals { start_signal: true, ..Default::default() }; + firmware.tick(&close_shell, &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub); + assert_eq!(firmware.os.lifecycle().task_state(shell_task), Some(TaskState::Closed)); + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); + + assert!(firmware.resume_resident_game_from_home()); + + match &firmware.state { + FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task), + other => panic!("expected GameRunning state, got {:?}", other), + } + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(game_task)); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Foreground)); + assert_eq!( + firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::ResumeForeground) + ); + } + #[test] fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() { assert!(Syscall::from_u32(0x0002).is_none()); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index a0aa7b43..143d6d40 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md b/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md index b8f7086e..9df5d7fb 100644 --- a/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md +++ b/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md @@ -2,7 +2,7 @@ id: PLN-0139 ticket: foreground-stack-game-pause-shell-vm-backed title: Validate Game Home Shell Game End To End -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0037] tags: [runtime, validation, firmware, host, lifecycle] diff --git a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md index 3958b3b5..d4c5782a 100644 --- a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md +++ b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md @@ -266,7 +266,30 @@ When a terminal app fault occurs: Crash handling is outside the guest VM execution model. -## 12 Relationship to Other Specs +## 12 Desktop Smoke Validation + +The foreground contract can be smoke-tested on the desktop host with: + +```text +cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console +cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges +``` + +Expected behavior: + +- direct `--run` still boots the selected Game without using a guest cartridge + switching API; +- `--games-root` starts in Home and launches Games through the Home library; +- while a Game is running, `Esc` opens Home/SystemOS without entering the guest + pad input surface; +- the Game is paused, then suspended after the bounded SystemOS budget; +- Home remains foreground while the Game is resident; +- launching and closing a Shell app returns to Home without losing the resident + Game; +- returning to the same Game resumes it through the foreground lifecycle and + waits for current-epoch Game rendering before visual Game presentation. + +## 13 Relationship to Other Specs - [`02-vm-instruction-set.md`](02-vm-instruction-set.md) defines the VM subsystem run by firmware. - [`09-events-and-concurrency.md`](09-events-and-concurrency.md) defines the frame-boundary model used by firmware. From 5ff273f1672055cd0423eb753be79b43982fec74 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 13:17:30 +0100 Subject: [PATCH 08/29] Foreground Stack, Game Pause, and VM-Backed Shell Coexistence --- discussion/index.ndjson | 4 +- ...ver-game-lifecycle-events-to-vm-runtime.md | 127 ++++++++++++++++++ ...ive-pause-budget-tick-before-suspension.md | 122 +++++++++++++++++ ...sident-game-when-shell-takes-foreground.md | 124 +++++++++++++++++ 4 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md create mode 100644 discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md create mode 100644 discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 143d6d40..28bf9780 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":38,"PLN":142,"LSN":52,"CLSN":1}} +{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":38,"PLN":145,"LSN":52,"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":"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":"in_progress","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-03","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md b/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md new file mode 100644 index 00000000..9ee5c0a5 --- /dev/null +++ b/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md @@ -0,0 +1,127 @@ +--- +id: PLN-0142 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Deliver Game Lifecycle Events To VM Runtime +status: open +created: 2026-07-04 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +`DEC-0037` requires pause/resume to be Game-visible lifecycle events. The +current implementation records `GameLifecycleEvent` values in SystemOS, but no +runtime or VM execution path consumes them. Tests currently prove that the OS +queue contains events, not that the Game/runtime can observe them before +suspension or after resume. + +## Objective + +Deliver Game lifecycle pause/resume events from SystemOS into the VM runtime +boundary in a deterministic, testable way, without exposing Home as guest pad +input or a userland syscall. + +## Dependencies + +- Source decision: `DEC-0037`. +- Fixes the lifecycle event delivery gap found after `PLN-0140` and + `PLN-0141`. +- `PLN-0143` depends on this plan if it needs to prove that the pause event is + consumed before the pause budget expires. + +## Scope + +Included: + +- Define the runtime-facing lifecycle event delivery API for `Pause` and + `ResumeForeground`. +- Move events from the SystemOS queue into the VM/runtime tick boundary. +- Make delivered events observable in tests without creating a broad app event + system. +- Preserve the distinction between lifecycle pause and debugger pause. +- Ensure events are delivered only to the resident Game task they target. + +Excluded: + +- No guest Home input, pad field, intrinsic, or syscall. +- No general userland event bus. +- No background execution while suspended. +- No new Shell lifecycle event model. + +## Non-Goals + +- Do not implement Game-to-Game switching. +- Do not change renderer ownership policy except where tests need to assert + lifecycle event ordering. +- Do not add wall-clock timers or asynchronous callbacks. + +## Execution Method + +1. Add a narrow VM runtime lifecycle inbox. + - What changes: introduce a small data structure for pending/delivered Game + lifecycle events in `VirtualMachineRuntime`. + - How it changes: add methods such as `queue_game_lifecycle_events`, + `take_delivered_game_lifecycle_events`, or a more idiomatic local name + that preserves deterministic frame-boundary delivery. + - File(s): `crates/console/prometeu-system/src/services/vm_runtime/mod.rs`, + `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`, and + focused tests under `crates/console/prometeu-system/src/services/vm_runtime/`. + +2. Route SystemOS events into the VM facade before ticking. + - What changes: `VmFacade::tick` must drain lifecycle events from SystemOS + and pass only events targeted at the currently running resident Game into + the VM runtime lifecycle inbox. + - How it changes: use the existing `LifecycleFacade::take_game_lifecycle_events` + or replace it with an equivalent internal method if borrowing requires a + cleaner API. Do not leave events only observable through + `pending_game_lifecycle_events`. + - File(s): `crates/console/prometeu-system/src/os/facades/vm.rs`, + `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. + +3. Expose deterministic test visibility. + - What changes: tests must be able to assert that `Pause` and + `ResumeForeground` reached the VM runtime boundary. + - How it changes: add test-only or public diagnostic accessors on + `VmFacade`/`VirtualMachineRuntime` for delivered lifecycle events, scoped + to lifecycle validation rather than a guest ABI. + - File(s): `crates/console/prometeu-system/src/os/facades/vm.rs`, + `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`. + +4. Update firmware tests to assert delivery, not only queue presence. + - What changes: existing firmware tests that inspect + `pending_game_lifecycle_events` should assert VM-runtime delivery at the + tick boundary. + - How it changes: after a Game pause/resume flow, verify that the VM/runtime + observed the lifecycle event in order. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. + +## Acceptance Criteria + +- [ ] `Pause` is moved from SystemOS into the VM runtime boundary before + suspension is allowed to complete. +- [ ] `ResumeForeground` is moved into the VM runtime boundary before the Game + is considered visually restored. +- [ ] Tests fail if events remain only in `SystemOS.game_lifecycle_events`. +- [ ] Lifecycle event delivery does not mutate `InputSignals`. +- [ ] Debugger pause remains separate from lifecycle pause. + +## Tests + +- Unit tests in `prometeu-system` proving VM runtime lifecycle event delivery + and drain semantics. +- Firmware tests proving Home request delivers `Pause` to the VM/runtime before + suspension. +- Firmware tests proving resident Game resume delivers `ResumeForeground` + before Game foreground restoration. +- Run `cargo test -p prometeu-system`. +- Run `cargo test -p prometeu-firmware`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/facades/vm.rs` +- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` +- `crates/console/prometeu-system/src/services/vm_runtime/mod.rs` +- `crates/console/prometeu-system/src/services/vm_runtime/tick.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md b/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md new file mode 100644 index 00000000..0ec8a98b --- /dev/null +++ b/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md @@ -0,0 +1,122 @@ +--- +id: PLN-0143 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Guarantee Cooperative Pause Budget Tick Before Suspension +status: open +created: 2026-07-04 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +`DEC-0037` requires a Game-visible `Paused` event followed by a bounded +cooperative reaction budget before SystemOS imposes `Suspended`. The current +implementation uses a one-tick budget and advances it before `vm.tick` in +`GameRunningStep`, so the Game can be suspended without any runtime tick in +which it could observe or react to pause. + +## Objective + +Guarantee at least one deterministic VM/runtime delivery point for the Game to +observe pause before forced suspension, while preserving SystemOS authority to +suspend the Game after the budget. + +## Dependencies + +- Source decision: `DEC-0037`. +- Should be executed after or together with `PLN-0142`, because budget tests + must prove delivered lifecycle events, not only queued OS events. + +## Scope + +Included: + +- Fix pause budget ordering so the pause budget cannot expire before the VM + runtime has a delivery point for `Pause`. +- Define the budget in deterministic machine ticks/frames, not wall-clock time. +- Ensure a Game receives no normal gameplay ticks after it reaches + `PausedSuspended`. +- Preserve the existing forced-suspension authority when the budget expires. +- Update tests that currently assert no VM tick occurs immediately after Home. + +Excluded: + +- No guest-controlled cancellation of suspension. +- No background execution after suspension. +- No extension of the pause budget based on host timing. +- No direct visual-return behavior; render restore gating remains covered by + the existing render boundary work. + +## Non-Goals + +- Do not add a general scheduler. +- Do not make pause cooperation mandatory for suspension. +- Do not conflate lifecycle pause with debugger pause. + +## Execution Method + +1. Redefine pause budget state transitions. + - What changes: `ResidentGameState::PauseRequested` must represent a state + in which one or more delivery ticks remain before `PausedSuspended`. + - How it changes: adjust `ForegroundStack::advance_pause_budget` or split it + into explicit methods such as `mark_pause_delivered` and + `advance_pause_budget_after_delivery`. + - File(s): `crates/console/prometeu-system/src/services/foreground.rs`. + +2. Move budget advancement after lifecycle delivery. + - What changes: `GameRunningStep::on_update` must not suspend before the VM + runtime has a chance to receive and process the pause lifecycle event. + - How it changes: reorder the GameRunning update so the event delivery path + runs first, then the budget is advanced, and only a subsequent eligible + boundary can force suspension. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs`, + `crates/console/prometeu-system/src/os/facades/vm.rs`. + +3. Preserve no-tick-after-suspended behavior. + - What changes: once `ResidentGameState::PausedSuspended` and + `TaskState::Suspended` are reached, the Game must not receive normal ticks, + normal input, or frame pacing. + - How it changes: keep the existing early transition to Hub after suspension + expires, but ensure it occurs after the cooperative pause window rather + than before it. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs`. + +4. Update tests to match the corrected semantics. + - What changes: tests must stop asserting that the first tick after Home does + zero VM work purely because Home was requested. + - How it changes: assert that the pause event was delivered during the + cooperative window, then assert that a later deterministic tick forces + suspension and no further Game ticks occur. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`, + `crates/console/prometeu-system/src/os/system_os.rs`. + +## Acceptance Criteria + +- [ ] A Game receives a deterministic pause delivery point before SystemOS + suspends it. +- [ ] SystemOS still suspends the Game after the bounded budget even if the + Game does not cooperate. +- [ ] A suspended Game receives no normal gameplay ticks, normal Game input, or + frame pacing. +- [ ] Tests prove the order: Home request -> pause delivery -> budget expiry -> + suspension. +- [ ] Debugger pause behavior is unchanged. + +## Tests + +- Unit tests in `prometeu-system` for pause budget transitions. +- Firmware tests for non-cooperative forced suspension after at least one + delivered pause boundary. +- Regression tests proving suspended Game tick count does not advance. +- Run `cargo test -p prometeu-system foreground lifecycle`. +- Run `cargo test -p prometeu-firmware pause`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/foreground.rs` +- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` +- `crates/console/prometeu-system/src/os/facades/vm.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md b/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md new file mode 100644 index 00000000..3bb7c682 --- /dev/null +++ b/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md @@ -0,0 +1,124 @@ +--- +id: PLN-0144 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Suspend Resident Game When Shell Takes Foreground +status: open +created: 2026-07-04 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +`DEC-0037` says a suspended resident Game must not receive normal gameplay +ticks, normal Game input, or frame pacing while Hub/Shell owns foreground. The +current `set_shell_foreground` path can mark the resident Game as +`PausedSuspended` in the foreground stack while `TaskManager::set_foreground` +only demotes the previous foreground Game task to `Background`. This creates a +state mismatch if a Shell is opened while the Game has not already gone through +the Home suspension path. + +## Objective + +Make Shell foreground acquisition either require an already suspended resident +Game or perform the necessary OS-owned suspension consistently across +foreground stack, task manager, and process manager. + +## Dependencies + +- Source decision: `DEC-0037`. +- Can be implemented independently of `PLN-0142` and `PLN-0143`, but tests + should be rerun with those fixes if they are executed first. + +## Scope + +Included: + +- Close the mismatch between `ResidentGameState::PausedSuspended` and + `TaskState::Background`. +- Ensure Shell foreground cannot leave a resident Game running or backgrounded. +- Cover both `create_vm_shell_task` and `create_native_shell_task` paths. +- Preserve the rule that only one foreground Shell may exist. +- Keep the normal Home -> Shell flow working. + +Excluded: + +- No direct Shell -> Game return behavior; v1 still returns Shell close to Hub. +- No background Game execution. +- No multiple resident Games. +- No Shell persistence policy changes. + +## Non-Goals + +- Do not broaden Shell lifecycle semantics. +- Do not implement Game-to-Game switching. +- Do not add UI controls for resume. + +## Execution Method + +1. Define the legal Shell foreground precondition. + - What changes: decide in code whether Shell foreground requires the + resident Game to already be `PausedSuspended`, or whether + `set_shell_foreground_task` is responsible for suspending it. + - How it changes: prefer centralizing the invariant in + `LifecycleFacade::set_shell_foreground_task`, because it owns task/process + transitions. + - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`, + `crates/console/prometeu-system/src/services/foreground.rs`. + +2. Synchronize task/process state when Shell takes foreground. + - What changes: if a resident Game exists and is not already suspended, + mark the Game task and process `Suspended` before or during Shell + foreground transition. + - How it changes: use `process_id_for_task` for the resident Game and call + `task_manager.mark_suspended` and `process_manager.mark_suspended` in the + same lifecycle operation that sets Shell foreground. + - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. + +3. Tighten foreground stack behavior. + - What changes: `ForegroundStack::set_shell_foreground` must not silently + hide invalid state transitions. + - How it changes: either reject Shell foreground when the resident Game is + still `Foreground`/`PauseRequested`, or return enough information for the + lifecycle facade to suspend the resident Game explicitly. + - File(s): `crates/console/prometeu-system/src/services/foreground.rs`. + +4. Add regression tests for reachable session paths. + - What changes: create tests that call `create_vm_shell_task` and + `create_native_shell_task` while a Game task is foreground. + - How it changes: assert foreground owner is Shell, resident Game remains + recorded, Game task state is `Suspended`, and Game process state is + `Suspended`. + - File(s): `crates/console/prometeu-system/src/os/system_os.rs`, + `crates/console/prometeu-firmware/src/firmware/firmware.rs` if the + firmware Home/Shell path needs coverage. + +## Acceptance Criteria + +- [ ] A Shell foreground transition cannot leave the resident Game in + `TaskState::Background`. +- [ ] Foreground stack state, task state, and process state agree that the + resident Game is suspended while Shell is foreground. +- [ ] `create_vm_shell_task` and `create_native_shell_task` both preserve the + invariant. +- [ ] Opening a second foreground Shell remains rejected. +- [ ] Closing Shell still returns to Hub/Home, not directly to Game. + +## Tests + +- Unit tests in `prometeu-system` for `set_shell_foreground_task` with a + resident foreground Game. +- Session facade tests for VM Shell and native Shell creation while a Game is + resident. +- Existing firmware Shell close tests must continue to pass. +- Run `cargo test -p prometeu-system foreground lifecycle shell`. +- Run `cargo test -p prometeu-firmware shell`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/foreground.rs` +- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` +- `crates/console/prometeu-system/src/os/facades/sessions.rs` +- `crates/console/prometeu-system/src/os/system_os.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` From d645bbb7bd307bd6b6fe61f175be16b4b214c344 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 13:20:52 +0100 Subject: [PATCH 09/29] implements PLN-0142 --- .../src/firmware/firmware.rs | 10 +++++ .../firmware/firmware_step_game_running.rs | 2 + .../prometeu-system/src/os/facades/vm.rs | 31 +++++++++++++- .../src/services/vm_runtime/lifecycle.rs | 40 +++++++++++++++++++ .../src/services/vm_runtime/mod.rs | 2 + discussion/index.ndjson | 2 +- ...ver-game-lifecycle-events-to-vm-runtime.md | 2 +- 7 files changed, 86 insertions(+), 3 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index fe083683..6bff5ef8 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -834,6 +834,11 @@ mod tests { 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), + Some(GameLifecycleEventKind::Pause) + ); } #[test] @@ -874,6 +879,11 @@ mod tests { assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground)); assert!(!platform.input().pad().any()); assert!(!platform.input().touch().f().down); + assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty()); + assert_eq!( + firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::ResumeForeground) + ); } #[test] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 7deac3b8..0fc5815b 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -37,6 +37,8 @@ impl GameRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } + ctx.os.vm().deliver_pending_game_lifecycle_events(); + match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) { Ok(true) => { ctx.os.vm().transition_render_owner(AppMode::Shell, 0); diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index f112f4e5..971b20cc 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -1,5 +1,5 @@ use crate::CrashReport; -use crate::os::SystemOS; +use crate::os::{GameLifecycleEvent, SystemOS}; use crate::{RenderWorkerConfig, RenderWorkerController}; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; @@ -31,6 +31,8 @@ impl<'a> VmFacade<'a> { signals: &InputSignals, platform: &mut dyn RuntimePlatform, ) -> Option { + self.deliver_pending_game_lifecycle_events(); + self.os.vm_runtime.tick( &mut self.os.log_service, &mut self.os.fs, @@ -44,6 +46,25 @@ impl<'a> VmFacade<'a> { ) } + pub fn deliver_pending_game_lifecycle_events(&mut self) { + if self.os.vm_runtime.current_cartridge_app_mode == AppMode::Game { + let resident_game_task = self.os.foreground_stack.resident_game_task(); + let mut retained = Vec::new(); + let mut delivered = Vec::new(); + + for event in self.os.game_lifecycle_events.drain(..) { + if Some(event.task_id) == resident_game_task { + delivered.push(event); + } else { + retained.push(event); + } + } + + self.os.game_lifecycle_events = retained; + self.os.vm_runtime.deliver_game_lifecycle_events(delivered); + } + } + pub fn reset(&mut self, vm: &mut VirtualMachine) { self.os.vm_runtime.reset(vm); } @@ -80,6 +101,14 @@ impl<'a> VmFacade<'a> { self.os.vm_runtime.logical_frame_active } + pub fn delivered_game_lifecycle_events(&self) -> &[GameLifecycleEvent] { + self.os.vm_runtime.delivered_game_lifecycle_events() + } + + pub fn take_delivered_game_lifecycle_events(&mut self) -> Vec { + self.os.vm_runtime.take_delivered_game_lifecycle_events() + } + pub fn tick_index(&self) -> u64 { self.os.vm_runtime.tick_index } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index a2c5ee2a..404bf9b1 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -32,6 +32,7 @@ impl VirtualMachineRuntime { gfx2d_commands: Vec::new(), gfxui_commands: Vec::new(), logs_written_this_frame: HashMap::new(), + game_lifecycle_events_delivered: Vec::new(), atomic_telemetry, last_crash_report: None, certifier: Certifier::new(cap_config.unwrap_or_default()), @@ -135,6 +136,7 @@ impl VirtualMachineRuntime { self.gfx2d_commands.clear(); self.gfxui_commands.clear(); self.logs_written_this_frame.clear(); + self.game_lifecycle_events_delivered.clear(); self.last_crash_report = None; @@ -191,6 +193,21 @@ impl VirtualMachineRuntime { } } + pub fn deliver_game_lifecycle_events(&mut self, events: I) + where + I: IntoIterator, + { + self.game_lifecycle_events_delivered.extend(events); + } + + pub fn delivered_game_lifecycle_events(&self) -> &[GameLifecycleEvent] { + &self.game_lifecycle_events_delivered + } + + pub fn take_delivered_game_lifecycle_events(&mut self) -> Vec { + std::mem::take(&mut self.game_lifecycle_events_delivered) + } + pub fn initialize_vm( &mut self, log_service: &mut LogService, @@ -224,3 +241,26 @@ impl VirtualMachineRuntime { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::os::{GameLifecycleEvent, GameLifecycleEventKind}; + use crate::task::TaskId; + + #[test] + fn vm_runtime_records_and_drains_delivered_game_lifecycle_events() { + let mut runtime = VirtualMachineRuntime::new(None); + let pause = GameLifecycleEvent { task_id: TaskId(7), kind: GameLifecycleEventKind::Pause }; + let resume = GameLifecycleEvent { + task_id: TaskId(7), + kind: GameLifecycleEventKind::ResumeForeground, + }; + + runtime.deliver_game_lifecycle_events([pause, resume]); + + assert_eq!(runtime.delivered_game_lifecycle_events(), &[pause, resume]); + assert_eq!(runtime.take_delivered_game_lifecycle_events(), vec![pause, resume]); + assert!(runtime.delivered_game_lifecycle_events().is_empty()); + } +} diff --git a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 6db48101..7f1cf23c 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/mod.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -13,6 +13,7 @@ mod tests; mod tick; use crate::CrashReport; +use crate::os::GameLifecycleEvent; pub use frame_scheduler::FrameScheduler; use prometeu_bytecode::string_materialization_count; use prometeu_hal::app_mode::AppMode; @@ -49,6 +50,7 @@ pub struct VirtualMachineRuntime { pub gfx2d_commands: Vec, pub gfxui_commands: Vec, pub logs_written_this_frame: HashMap, + pub game_lifecycle_events_delivered: Vec, pub atomic_telemetry: Arc, pub last_crash_report: Option, pub certifier: Certifier, diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 28bf9780..0c34fdd8 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md b/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md index 9ee5c0a5..72101b29 100644 --- a/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md +++ b/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md @@ -2,7 +2,7 @@ id: PLN-0142 ticket: foreground-stack-game-pause-shell-vm-backed title: Deliver Game Lifecycle Events To VM Runtime -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From a771f6320e073afe9bc1c7b2e42c684bf75129cb Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 13:24:08 +0100 Subject: [PATCH 10/29] implements PLN-0143 --- .../src/firmware/firmware.rs | 8 +++- .../firmware/firmware_step_game_running.rs | 40 +++++++++++-------- .../src/services/foreground.rs | 26 ++++++++++++ discussion/index.ndjson | 2 +- ...ive-pause-budget-tick-before-suspension.md | 2 +- 5 files changed, 57 insertions(+), 21 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 6bff5ef8..9f8ad7ba 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -788,7 +788,7 @@ mod tests { } #[test] - fn host_home_request_pauses_then_suspends_game_before_next_vm_tick() { + fn host_home_request_delivers_pause_tick_before_forced_suspension() { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let signals = InputSignals::default(); @@ -826,7 +826,7 @@ mod tests { firmware.os.lifecycle().process_state_for_task(task_id), Ok(ProcessState::Suspended) ); - assert_eq!(firmware.os.vm().tick_index(), tick_index_before_home); + assert_eq!(firmware.os.vm().tick_index(), tick_index_before_home + 1); assert!(!platform.input().pad().any()); assert!(!platform.input().touch().f().down); @@ -839,6 +839,10 @@ mod tests { firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::Pause) ); + + let tick_index_after_suspension = firmware.os.vm().tick_index(); + firmware.tick(&signals, &mut platform); + assert_eq!(firmware.os.vm().tick_index(), tick_index_after_suspension); } #[test] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 0fc5815b..a3c37323 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -2,8 +2,8 @@ use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::app_mode::AppMode; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_system::CrashReport; use prometeu_system::task::{TaskId, TaskState}; +use prometeu_system::{CrashReport, ResidentGameState}; #[derive(Debug, Clone)] pub struct GameRunningStep { @@ -39,22 +39,10 @@ impl GameRunningStep { ctx.os.vm().deliver_pending_game_lifecycle_events(); - match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) { - Ok(true) => { - ctx.os.vm().transition_render_owner(AppMode::Shell, 0); - return Some(FirmwareState::HubHome(HubHomeStep)); - } - Ok(false) => {} - Err(_) => { - let report = CrashReport::VmPanic { - message: "failed to advance game pause budget".to_string(), - pc: None, - }; - let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); - return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); - } - } - + let pause_requested = matches!( + ctx.os.lifecycle().resident_game().map(|game| game.state), + Some(ResidentGameState::PauseRequested { .. }) + ); let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform); if let Some(report) = result { @@ -62,6 +50,24 @@ impl GameRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } + if pause_requested { + match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) { + Ok(true) => { + ctx.os.vm().transition_render_owner(AppMode::Shell, 0); + return Some(FirmwareState::HubHome(HubHomeStep)); + } + Ok(false) => {} + Err(_) => { + let report = CrashReport::VmPanic { + message: "failed to advance game pause budget".to_string(), + pc: None, + }; + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + } + } + None } diff --git a/crates/console/prometeu-system/src/services/foreground.rs b/crates/console/prometeu-system/src/services/foreground.rs index 1b2e8bc1..a299bb87 100644 --- a/crates/console/prometeu-system/src/services/foreground.rs +++ b/crates/console/prometeu-system/src/services/foreground.rs @@ -263,6 +263,32 @@ mod tests { ); } + #[test] + fn pause_budget_decrements_before_expiry() { + let mut stack = ForegroundStack::new(); + let game = TaskId(3); + + stack.set_game_foreground(game).expect("game should be foreground"); + stack.request_home_from_game(game, 2).expect("home request should pause game"); + + assert!(!stack.advance_pause_budget(game).expect("budget should advance")); + assert_eq!(stack.owner(), ForegroundOwner::Game(game)); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { + task_id: game, + state: ResidentGameState::PauseRequested { remaining_ticks: 1 }, + }) + ); + + assert!(stack.advance_pause_budget(game).expect("budget should expire")); + assert_eq!(stack.owner(), ForegroundOwner::Hub); + assert_eq!( + stack.resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) + ); + } + #[test] fn resume_request_precedes_game_foreground_restore() { let mut stack = ForegroundStack::new(); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 0c34fdd8..7965fbb5 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md b/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md index 0ec8a98b..95252d59 100644 --- a/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md +++ b/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md @@ -2,7 +2,7 @@ id: PLN-0143 ticket: foreground-stack-game-pause-shell-vm-backed title: Guarantee Cooperative Pause Budget Tick Before Suspension -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From a517a249788815532b79ef720e56e59dbe95c46c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 13:26:05 +0100 Subject: [PATCH 11/29] implements PLN-0144 --- .../src/os/facades/lifecycle.rs | 9 +++++ .../prometeu-system/src/os/system_os.rs | 38 +++++++++++++++++++ discussion/index.ndjson | 2 +- ...sident-game-when-shell-takes-foreground.md | 2 +- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index 49f58942..a6ebcb78 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -64,6 +64,15 @@ impl<'a> LifecycleFacade<'a> { }); } + if let Some(game_task_id) = self.os.foreground_stack.resident_game_task() + && game_task_id != task_id + && self.task_state(game_task_id) != Some(TaskState::Suspended) + { + let game_process_id = process_id_for_task(self.os, game_task_id)?; + self.os.task_manager.mark_suspended(game_task_id); + self.os.process_manager.mark_suspended(game_process_id); + } + self.os.foreground_stack.set_shell_foreground(task_id).map_err(map_foreground_error)?; self.os.task_manager.set_foreground(task_id); self.os.process_manager.mark_running(process_id); diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index ff25db5e..e263593c 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -250,6 +250,44 @@ mod tests { ); } + #[test] + fn vm_shell_foreground_suspends_foreground_resident_game() { + let mut os = SystemOS::new(None); + let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + let shell = os.sessions().create_vm_shell_task(7, "Settings"); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); + assert_eq!( + os.lifecycle().resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) + ); + assert_eq!( + os.task_manager.get(game).expect("game should exist").state, + TaskState::Suspended + ); + assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended); + } + + #[test] + fn native_shell_foreground_suspends_foreground_resident_game() { + let mut os = SystemOS::new(None); + let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + let shell = os.sessions().create_native_shell_task(7, "Settings"); + + assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); + assert_eq!( + os.lifecycle().resident_game(), + Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) + ); + assert_eq!( + os.task_manager.get(game).expect("game should exist").state, + TaskState::Suspended + ); + assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended); + } + #[test] fn second_foreground_shell_is_rejected() { let mut os = SystemOS::new(None); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 7965fbb5..bcebe218 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md b/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md index 3bb7c682..1bec2807 100644 --- a/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md +++ b/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md @@ -2,7 +2,7 @@ id: PLN-0144 ticket: foreground-stack-game-pause-shell-vm-backed title: Suspend Resident Game When Shell Takes Foreground -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From e19409a2efd58631bfd6d63bda5481eb03a300f6 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 18:26:25 +0100 Subject: [PATCH 12/29] fix resident game relaunch from hub --- .../src/firmware/firmware.rs | 59 +++++++++++++++++++ .../src/firmware/firmware_step_hub_home.rs | 53 ++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 9f8ad7ba..aa550d97 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -96,9 +96,14 @@ impl Firmware { signals: &InputSignals, platform: &mut dyn RuntimePlatform, ) { + let entering_game_from_home = matches!(self.state, FirmwareState::HubHome(_)) + && matches!(new_state, FirmwareState::GameRunning(_)); self.on_exit(signals, platform); self.state = new_state; self.state_initialized = false; + if entering_game_from_home { + self.game_input_barrier_frames = self.game_input_barrier_frames.max(1); + } // Enter the new state immediately to avoid "empty" frames during transitions. self.on_enter(signals, platform); @@ -620,6 +625,60 @@ mod tests { assert_eq!(firmware.os.windows().window_count(), 0); } + #[test] + fn hub_home_clicking_resident_game_resumes_same_task_without_reloading() { + let root = TestGameRoot::new(); + root.add_game("stress", 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 launch_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&launch_click, &mut platform); + firmware.tick(&InputSignals::default(), &mut platform); + + let game_task = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected 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(game_task)); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + + firmware.tick(&InputSignals::default(), &mut platform); + firmware.tick(&launch_click, &mut platform); + + match &firmware.state { + FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task), + other => panic!("expected resident GameRunning state, got {:?}", other), + } + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); + assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(game_task)); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Foreground)); + assert_eq!( + firmware.os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::ResumeForeground) + ); + + let held_click = + InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() }; + firmware.tick(&held_click, &mut platform); + + assert!(!platform.input().pad().any()); + assert!(!platform.input().touch().f().down); + assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty()); + assert_eq!( + firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind), + Some(GameLifecycleEventKind::ResumeForeground) + ); + } + #[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 1e39275c..e42deff6 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 @@ -1,7 +1,9 @@ use crate::firmware::firmware_state::{ - AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep, + AppCrashesStep, FirmwareState, GameRunningStep, LoadCartridgeStep, ShellRunningStep, }; use crate::firmware::prometeu_context::PrometeuContext; +use prometeu_hal::InputSignals; +use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::primitives::Rect; @@ -38,6 +40,55 @@ impl HubHomeStep { } Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { Ok(cartridge) => { + if cartridge.app_mode == AppMode::Game + && let Some(task_id) = ctx.os.lifecycle().resident_game_task() + { + if ctx.os.vm().current_app_id() == cartridge.app_id { + match ctx.os.lifecycle().resume_task(task_id) { + Ok(()) => { + ctx.os + .vm() + .transition_render_owner(AppMode::Game, cartridge.app_id); + ctx.platform + .input_mut() + .pad_mut() + .begin_frame(&InputSignals::default()); + ctx.platform + .input_mut() + .touch_mut() + .begin_frame(&InputSignals::default()); + return Some(FirmwareState::GameRunning(GameRunningStep::new( + task_id, + ))); + } + Err(error) => { + ctx.os.log( + LogLevel::Warn, + LogSource::Hub, + 0, + format!( + "Failed to resume resident Home game {}: {error:?}", + cartridge.title + ), + ); + return None; + } + } + } + + let resident_app_id = ctx.os.vm().current_app_id(); + 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) => { From a22215392da130ed83e26d01de14ae1dbb0bd66a Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 18:32:44 +0100 Subject: [PATCH 13/29] fix native shell ticking resident game --- .../src/firmware/firmware.rs | 9 ++++ .../firmware/firmware_step_shell_running.rs | 52 ++++++++++++++++--- .../src/os/facades/lifecycle.rs | 11 +++- .../prometeu-system/src/os/system_os.rs | 17 +++++- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index aa550d97..31287217 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -984,6 +984,15 @@ mod tests { assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + let tick_index_before_native_shell_updates = firmware.os.vm().tick_index(); + for _ in 0..61 { + firmware.tick(&signals, &mut platform); + } + + assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); + assert_eq!(firmware.os.vm().tick_index(), tick_index_before_native_shell_updates); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + let close_shell = InputSignals { start_signal: true, ..Default::default() }; firmware.tick(&close_shell, &mut platform); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs index b0a76f1c..aff9a139 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs @@ -1,6 +1,7 @@ use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::log::LogSource; +use prometeu_system::process::ProcessKind; use prometeu_system::task::{TaskId, TaskState}; use prometeu_system::{CrashReport, SystemProfileAction}; @@ -59,14 +60,53 @@ impl ShellRunningStep { } } - let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform); + let process_kind = match ctx.os.lifecycle().process_kind_for_task(self.task_id) { + Ok(process_kind) => process_kind, + Err(error) => { + let report = CrashReport::VmPanic { + message: format!( + "ShellRunningStep cannot resolve process for {:?}: {:?}", + self.task_id, error, + ), + pc: None, + }; + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + }; - if let Some(report) = outcome.crash { - let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); - return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); - } + let action = match process_kind { + ProcessKind::VmShell => { + let outcome = + ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform); - if outcome.action == Some(SystemProfileAction::CloseShell) { + if let Some(report) = outcome.crash { + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + + outcome.action + } + ProcessKind::NativeShell => { + let mut action = ctx.hub.gui_update(ctx.os, ctx.platform); + if ctx.platform.input().pad().start().down { + action = Some(SystemProfileAction::CloseShell); + } + ctx.hub.render(ctx.os, ctx.platform); + action + } + ProcessKind::VmGame => { + let report = CrashReport::VmPanic { + message: format!( + "ShellRunningStep expected shell process for {:?}, got VmGame", + self.task_id, + ), + pc: None, + }; + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + }; + + if action == Some(SystemProfileAction::CloseShell) { self.close_current_shell(ctx); return Some(FirmwareState::HubHome(HubHomeStep)); } diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index a6ebcb78..4f311fbc 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -4,7 +4,7 @@ use crate::os::{ DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, LifecycleOperation, }; -use crate::process::{ProcessId, ProcessState}; +use crate::process::{ProcessId, ProcessKind, ProcessState}; use crate::services::foreground::{ ForegroundOwner, ForegroundStackError, ResidentGame, ResidentGameState, }; @@ -28,6 +28,15 @@ impl<'a> LifecycleFacade<'a> { .ok_or(LifecycleError::ProcessNotFound(process_id)) } + pub fn process_kind_for_task(&self, task_id: TaskId) -> Result { + let process_id = process_id_for_task(self.os, task_id)?; + self.os + .process_manager + .get(process_id) + .map(|process| process.kind) + .ok_or(LifecycleError::ProcessNotFound(process_id)) + } + pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { match task_kind_for_task(self.os, task_id)? { TaskKind::Game => return self.set_game_foreground_task(task_id), diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index e263593c..439e21df 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -141,7 +141,7 @@ impl SystemOS { mod tests { use super::*; use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation}; - use crate::process::ProcessState; + use crate::process::{ProcessKind, ProcessState}; use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState}; use crate::task::{TaskId, TaskState}; @@ -288,6 +288,21 @@ mod tests { assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended); } + #[test] + fn lifecycle_exposes_process_kind_for_shell_tasks() { + let mut os = SystemOS::new(None); + let vm_shell = os.sessions().create_vm_shell_task(7, "Settings"); + os.lifecycle().close_task(vm_shell).expect("vm shell should close"); + + let native_shell = os.sessions().create_native_shell_task(8, "Files"); + + assert_eq!(os.lifecycle().process_kind_for_task(vm_shell), Ok(ProcessKind::VmShell)); + assert_eq!( + os.lifecycle().process_kind_for_task(native_shell), + Ok(ProcessKind::NativeShell) + ); + } + #[test] fn second_foreground_shell_is_rejected() { let mut os = SystemOS::new(None); From 3eeda25e9702101d6d5b6d1102adf2848429ca9b Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 18:48:11 +0100 Subject: [PATCH 14/29] Foreground Stack, Game Pause, and VM-Backed Shell Coexistence --- .../src/os/facades/lifecycle.rs | 4 +- discussion/index.ndjson | 4 +- ...tralize-resident-game-resume-transition.md | 149 ++++++++++++++++++ ...lidate-lifecycle-process-lookup-helpers.md | 119 ++++++++++++++ ...ve-native-shell-profile-update-into-hub.md | 144 +++++++++++++++++ 5 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md create mode 100644 discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md create mode 100644 discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index 4f311fbc..9cf67528 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -39,8 +39,8 @@ impl<'a> LifecycleFacade<'a> { pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { match task_kind_for_task(self.os, task_id)? { - TaskKind::Game => return self.set_game_foreground_task(task_id), - TaskKind::Shell => return self.set_shell_foreground_task(task_id), + TaskKind::Game => self.set_game_foreground_task(task_id), + TaskKind::Shell => self.set_shell_foreground_task(task_id), } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index bcebe218..1f61aee2 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":38,"PLN":145,"LSN":52,"CLSN":1}} +{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":38,"PLN":148,"LSN":52,"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md b/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md new file mode 100644 index 00000000..f3982d76 --- /dev/null +++ b/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md @@ -0,0 +1,149 @@ +--- +id: PLN-0145 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Centralize Resident Game Resume Transition +status: open +created: 2026-07-04 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +`DEC-0037` requires returning to the same resident Game to reactivate the Game +VM, restore render ownership, send a resume/foreground lifecycle notification, +and clear or barrier pending Game input. That transition currently exists in +more than one firmware path: `Firmware::resume_resident_game_from_home` and the +Hub library click path that resumes the selected resident Game by `app_id`. + +The behavior is correct, but the duplication is fragile. Any future change to +resume ordering, input barriers, render ownership, or lifecycle event delivery +could fix one path while leaving the other path stale. + +## Objective + +Create one firmware-owned resident Game resume helper and route every +Home/Hub-to-Game resume path through it. + +## Dependencies + +- Source decision: `DEC-0037`. +- Depends on the accepted v1 identity rule that `app_id` is unique for all + games and may be used to decide whether a Hub game selection refers to the + current resident Game. +- Should preserve the behavior introduced by `PLN-0142`, `PLN-0143`, and + `PLN-0144`. + +## Scope + +Included: + +- Centralize resident Game resume orchestration in `Firmware`. +- Preserve resume lifecycle event queuing through `LifecycleFacade::resume_task`. +- Preserve render ownership transition back to `AppMode::Game`. +- Preserve input barrier behavior across all Home/Hub-to-Game resume paths. +- Preserve the Hub rule that selecting the same resident Game resumes it, while + selecting a different Game remains blocked by v1 scope. +- Keep tests proving same-task resume, lifecycle event delivery, and input + barriering. + +Excluded: + +- No Game-to-Game switching. +- No multiple resident Games. +- No change to `app_id` uniqueness policy. +- No direct Shell-to-Game close behavior. + +## Non-Goals + +- Do not move foreground authority from SystemOS into firmware. +- Do not change pause budget or suspend behavior. +- Do not add UI affordances beyond the existing Hub game row selection. + +## Execution Method + +1. Introduce a single resident Game resume helper. + - What changes: create a private firmware method that performs the complete + resident Game resume operation and returns the target `TaskId` or a typed + failure. + - How it changes: move the shared sequence currently duplicated across + `resume_resident_game_from_home` and `HubHomeStep`: read resident task, + call `LifecycleFacade::resume_task`, transition VM render ownership to + `AppMode::Game`, install `GameRunningStep`, clear state initialization, + and arm the input barrier. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. + +2. Route explicit resume through the helper. + - What changes: `Firmware::resume_resident_game_from_home` becomes a thin + wrapper around the centralized helper. + - How it changes: preserve its public `bool` return contract for existing + tests and call sites. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. + +3. Route Hub same-game selection through the helper. + - What changes: `HubHomeStep` must no longer hand-roll resume task, render + ownership, and input clearing. + - How it changes: expose a state transition request that lets the firmware + perform the centralized helper, or restructure the Hub action handling so + the parent `Firmware` owns this transition. Keep `HubHomeStep` responsible + only for recognizing that the selected cartridge matches the resident + `app_id`. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`, + `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`, + `crates/console/prometeu-firmware/src/firmware/firmware_state.rs` if a new + transition variant is needed. + +4. Keep different-game launch blocked under v1. + - What changes: selecting a different `app_id` while a resident Game exists + continues to log and remain in Hub. + - How it changes: keep the existing warning behavior, but ensure it does not + duplicate resume side effects. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`. + +5. Update regression tests. + - What changes: tests should prove both explicit resume and Hub same-game + selection use the same observable transition. + - How it changes: assert same `TaskId`, foreground owner restored to Game, + pending resume lifecycle event exists before the next Game tick, delivered + resume event after the Game tick, and input barrier suppresses held input. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. + +## Acceptance Criteria + +- [ ] There is exactly one implementation of the resident Game resume sequence. +- [ ] `Firmware::resume_resident_game_from_home` preserves its external + behavior. +- [ ] Hub selection of the same resident Game resumes the existing `TaskId` + without creating or loading a new Game task. +- [ ] The resume path always queues and delivers the Game lifecycle + `ResumeForeground` event. +- [ ] The resume path always barriers held Game input. +- [ ] Selecting a different Game while one Game is resident remains blocked in + v1. + +## Tests + +- Unit/integration tests in `prometeu-firmware` for explicit resume from Home. +- Unit/integration tests in `prometeu-firmware` for Hub same-game selection + after Home suspension. +- Regression assertion that held input does not leak into the Game immediately + after resume. +- Run `cargo test -p prometeu-firmware resident_game_resume`. +- Run `cargo test -p prometeu-firmware hub_home_clicking_resident_game`. +- Run `cargo test -p prometeu-system`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_state.rs` + +## Risks + +- The current step/state architecture gives `HubHomeStep` a `PrometeuContext` + but not direct access to private `Firmware` fields. If a new transition + variant is introduced, it must stay narrow and must not turn state steps into + owners of lifecycle authority. +- Duplicating less logic must not accidentally weaken the input barrier that was + added for Hub-to-Game transitions. diff --git a/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md b/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md new file mode 100644 index 00000000..f890917a --- /dev/null +++ b/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md @@ -0,0 +1,119 @@ +--- +id: PLN-0146 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Consolidate Lifecycle Process Lookup Helpers +status: open +created: 2026-07-04 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +`DEC-0037` depends on SystemOS lifecycle authority being internally +consistent. The lifecycle facade now exposes both process state and process kind +for a task so firmware can distinguish VM-backed Shell tasks from native Shell +tasks. Both accessors repeat the same task-to-process lookup pattern. + +The duplication is small, but this facade is now part of the foreground +contract surface. Keeping lookup and error mapping in one place reduces the +chance that future lifecycle accessors drift in behavior. + +## Objective + +Consolidate task-to-process lookup in `LifecycleFacade` behind one private +helper, while preserving the public lifecycle facade behavior and errors. + +## Dependencies + +- Source decision: `DEC-0037`. +- Depends on the process/task authority from SystemOS lifecycle work. +- Can be implemented independently of `PLN-0145` and `PLN-0147`. + +## Scope + +Included: + +- Add one private helper that resolves a `TaskId` to its backing process. +- Route `process_state_for_task` and `process_kind_for_task` through that + helper. +- Preserve `LifecycleError::TaskNotFound` and + `LifecycleError::ProcessNotFound` behavior exactly. +- Keep the current process/task manager ownership boundaries intact. +- Keep the current non-architectural cleanup in `set_foreground_task` if it is + still present in the working tree. + +Excluded: + +- No new public process manager API. +- No changes to task or process IDs. +- No changes to Shell/Game lifecycle semantics. +- No broader facade refactor outside lifecycle lookup. + +## Non-Goals + +- Do not expose raw process manager access through SystemOS. +- Do not change `ProcessKind` or `TaskKind` definitions. +- Do not alter foreground stack behavior. + +## Execution Method + +1. Add a private lifecycle lookup helper. + - What changes: add a private function near `process_id_for_task` that + returns a process reference for a `TaskId`. + - How it changes: the helper should call `process_id_for_task`, then + retrieve the process from `process_manager`, returning + `LifecycleError::ProcessNotFound(process_id)` if the process is missing. + - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. + +2. Route state and kind accessors through the helper. + - What changes: `LifecycleFacade::process_state_for_task` and + `LifecycleFacade::process_kind_for_task` stop duplicating the process + manager lookup. + - How it changes: both methods call the helper and map the process reference + to `ProcessState` or `ProcessKind`. + - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. + +3. Preserve existing foreground code cleanup. + - What changes: keep `set_foreground_task` as an expression-returning match + rather than using redundant `return` statements. + - How it changes: ensure formatting and clippy stay clean. + - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. + +4. Tighten tests around lookup behavior. + - What changes: extend or keep tests proving VM Shell and native Shell + process kinds are observable through lifecycle. + - How it changes: add missing-task and missing-process coverage only if the + helper changes the observable path enough to justify it. + - File(s): `crates/console/prometeu-system/src/os/system_os.rs`. + +## Acceptance Criteria + +- [ ] `process_state_for_task` and `process_kind_for_task` share one private + task-to-process lookup implementation. +- [ ] Missing task and missing process errors remain unchanged. +- [ ] Existing lifecycle tests continue to pass. +- [ ] Firmware can still distinguish `ProcessKind::VmShell` from + `ProcessKind::NativeShell`. +- [ ] Clippy reports no warnings for the touched crates. + +## Tests + +- Unit tests in `prometeu-system` for shell process kind lookup. +- Existing lifecycle missing task/process tests must continue to pass. +- Run `cargo test -p prometeu-system lifecycle process_kind`. +- Run `cargo test -p prometeu-firmware shell`. +- Run `cargo clippy -p prometeu-system -p prometeu-firmware --all-targets -- -D warnings`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` +- `crates/console/prometeu-system/src/os/system_os.rs` + +## Risks + +- Over-generalizing the helper could leak process manager concepts outside the + lifecycle facade. Keep it private and narrowly typed. +- This is intentionally small cleanup. It must not become a broad SystemOS + facade redesign. diff --git a/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md b/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md new file mode 100644 index 00000000..cdcc2ce1 --- /dev/null +++ b/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md @@ -0,0 +1,144 @@ +--- +id: PLN-0147 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Move Native Shell Profile Update Into Hub +status: open +created: 2026-07-04 +ref_decisions: [DEC-0037] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +`DEC-0037` says a suspended Game must not receive normal gameplay ticks while +Hub or Shell owns foreground. A recent fix made `ShellRunningStep` branch on +`ProcessKind`: VM-backed Shell tasks use the VM-backed shell profile path, while +native Shell tasks call `PrometeuHub::gui_update` and `PrometeuHub::render` +without ticking the VM. + +That behavior is correct, but the native Shell update sequence now lives in +firmware rather than in the Hub profile API. This duplicates part of +`PrometeuHub::update_shell_profile` and creates a drift risk if Shell close, +render, or input rules change later. + +## Objective + +Move native Shell profile updating into `PrometeuHub` so `ShellRunningStep` +chooses the correct profile API without reimplementing Hub behavior. + +## Dependencies + +- Source decision: `DEC-0037`. +- Depends on the `ProcessKind::VmShell` vs `ProcessKind::NativeShell` + distinction available through `LifecycleFacade::process_kind_for_task`. +- May be implemented before or after `PLN-0146`, but should use the consolidated + lookup helper if `PLN-0146` has already landed. + +## Scope + +Included: + +- Add a Hub-owned native Shell update API that does not tick the VM. +- Keep VM-backed Shell update behavior in the existing VM-backed profile path. +- Keep Shell close handling consistent between native and VM-backed Shells. +- Keep native Shell rendering in the Hub/Shell UI path. +- Keep firmware responsible for choosing which profile applies to the current + Shell task. +- Preserve regression coverage that a suspended resident Game does not advance + while native Shell is foreground. + +Excluded: + +- No change to VM-backed Shell execution semantics. +- No change to native Shell UI content. +- No direct Shell close to Game. +- No multiple foreground Shells. +- No background Game ticking. + +## Non-Goals + +- Do not move foreground lifecycle authority into `PrometeuHub`. +- Do not make native Shells VM tasks. +- Do not introduce a general scheduler abstraction. + +## Execution Method + +1. Add a native Shell profile update method on `PrometeuHub`. + - What changes: create `update_native_shell_profile` or an equivalent + clearly named method that returns `SystemProfileUpdate`. + - How it changes: the method calls `gui_update`, maps Start input to + `SystemProfileAction::CloseShell` when a shell window is focused, calls + `render`, and always returns `crash: None`. + - File(s): + `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. + +2. Keep the VM-backed Shell profile method explicitly VM-backed. + - What changes: either keep `update_shell_profile` and document/name its VM + tick behavior, or rename/split it if the codebase convention supports that + without churn. + - How it changes: ensure the VM-backed path is the only Hub profile update + method that calls `os.vm().tick(...)`. + - File(s): + `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. + +3. Simplify `ShellRunningStep`. + - What changes: remove manual native Shell `gui_update`, Start handling, and + render calls from firmware. + - How it changes: branch on `ProcessKind` and call the appropriate Hub + profile method; keep crash handling only for the VM-backed method. + - File(s): + `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs`. + +4. Add Hub-level native profile tests. + - What changes: prove native Shell profile update emits close on the close + button and on Start, renders Shell UI, and does not tick the VM. + - How it changes: use existing Hub/system test helpers where possible; assert + VM tick index remains unchanged for native profile update. + - File(s): + `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` + and/or `crates/console/prometeu-firmware/src/firmware/firmware.rs`. + +5. Preserve end-to-end regression coverage. + - What changes: keep the Game -> Home -> native Shell test asserting that + repeated Shell frames do not advance the resident Game VM tick index. + - How it changes: rerun the firmware regression added for native Shell + foreground. + - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. + +## Acceptance Criteria + +- [ ] Native Shell update behavior is owned by `PrometeuHub`, not hand-coded in + `ShellRunningStep`. +- [ ] The native Shell profile update never calls `os.vm().tick(...)`. +- [ ] VM-backed Shell update remains the only Shell profile path that ticks the + VM runtime. +- [ ] Start and close-button Shell close behavior remains unchanged for native + Shells. +- [ ] A suspended resident Game receives no normal gameplay ticks while native + Shell is foreground. + +## Tests + +- Hub-level tests for native Shell profile close behavior. +- Hub-level or firmware-level tests proving native Shell profile update does not + tick the VM. +- Existing firmware regression for Game -> Home -> native Shell repeated frames. +- Run `cargo test -p prometeu-system native_shell shell_profile`. +- Run `cargo test -p prometeu-firmware game_home_shell_hub_same_game_flow`. +- Run `cargo test -p prometeu-firmware`. +- Run `cargo test -p prometeu-system`. +- Run `discussion validate`. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` + +## Risks + +- Renaming the existing VM-backed shell profile method may create unnecessary + churn. Prefer adding the native method and keeping call sites explicit unless + the rename is small and clearly improves readability. +- Hub APIs must remain UI/profile orchestration only; lifecycle close and task + state changes still belong to firmware/SystemOS lifecycle. From 0ecab2910b20dc40902d06bbb3de7f0a7f66a168 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 18:50:50 +0100 Subject: [PATCH 15/29] implements PLN-0145 --- .../src/firmware/firmware.rs | 33 +++++++++++++---- .../src/firmware/firmware_state.rs | 1 + .../src/firmware/firmware_step_hub_home.rs | 36 ++----------------- discussion/index.ndjson | 2 +- ...tralize-resident-game-resume-transition.md | 2 +- 5 files changed, 32 insertions(+), 42 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 31287217..f0ff3ece 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -96,6 +96,15 @@ impl Firmware { signals: &InputSignals, platform: &mut dyn RuntimePlatform, ) { + let new_state = match new_state { + FirmwareState::ResumeResidentGame => { + let Some(resume_state) = self.resident_game_resume_state() else { + return; + }; + resume_state + } + state => state, + }; let entering_game_from_home = matches!(self.state, FirmwareState::HubHome(_)) && matches!(new_state, FirmwareState::GameRunning(_)); self.on_exit(signals, platform); @@ -126,6 +135,7 @@ impl Firmware { FirmwareState::LaunchHub(s) => s.on_enter(&mut req), FirmwareState::HubHome(s) => s.on_enter(&mut req), FirmwareState::LoadCartridge(s) => s.on_enter(&mut req), + FirmwareState::ResumeResidentGame => {} FirmwareState::GameRunning(s) => s.on_enter(&mut req), FirmwareState::ShellRunning(s) => s.on_enter(&mut req), FirmwareState::AppCrashes(s) => s.on_enter(&mut req), @@ -153,6 +163,7 @@ impl Firmware { FirmwareState::LaunchHub(s) => s.on_update(&mut req), FirmwareState::HubHome(s) => s.on_update(&mut req), FirmwareState::LoadCartridge(s) => s.on_update(&mut req), + FirmwareState::ResumeResidentGame => None, FirmwareState::GameRunning(s) => s.on_update(&mut req), FirmwareState::ShellRunning(s) => s.on_update(&mut req), FirmwareState::AppCrashes(s) => s.on_update(&mut req), @@ -175,6 +186,7 @@ impl Firmware { FirmwareState::LaunchHub(s) => s.on_exit(&mut req), FirmwareState::HubHome(s) => s.on_exit(&mut req), FirmwareState::LoadCartridge(s) => s.on_exit(&mut req), + FirmwareState::ResumeResidentGame => {} FirmwareState::GameRunning(s) => s.on_exit(&mut req), FirmwareState::ShellRunning(s) => s.on_exit(&mut req), FirmwareState::AppCrashes(s) => s.on_exit(&mut req), @@ -201,20 +213,27 @@ impl Firmware { } pub fn resume_resident_game_from_home(&mut self) -> bool { - let Some(task_id) = self.os.lifecycle().resident_game_task() else { + let Some(state) = self.resident_game_resume_state() else { return false; }; - if self.os.lifecycle().resume_task(task_id).is_err() { - return false; + self.state = state; + self.state_initialized = false; + true + } + + fn resident_game_resume_state(&mut self) -> Option { + let task_id = self.os.lifecycle().resident_game_task()?; + + if let Err(error) = self.os.lifecycle().resume_task(task_id) { + self.os.warn(LogSource::Pos, format!("Resident Game resume failed: {error:?}")); + return None; } let app_id = self.os.vm().current_app_id(); self.os.vm().transition_render_owner(AppMode::Game, app_id); - self.state = FirmwareState::GameRunning(GameRunningStep::new(task_id)); - self.state_initialized = false; - self.game_input_barrier_frames = 1; - true + self.game_input_barrier_frames = self.game_input_barrier_frames.max(1); + Some(FirmwareState::GameRunning(GameRunningStep::new(task_id))) } pub fn game_lifecycle_audio_paused(&mut self) -> bool { diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_state.rs b/crates/console/prometeu-firmware/src/firmware/firmware_state.rs index 744ba05a..130d8812 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_state.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_state.rs @@ -14,6 +14,7 @@ pub enum FirmwareState { LaunchHub(LaunchHubStep), HubHome(HubHomeStep), LoadCartridge(LoadCartridgeStep), + ResumeResidentGame, GameRunning(GameRunningStep), ShellRunning(ShellRunningStep), AppCrashes(AppCrashesStep), 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 e42deff6..cede2f36 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 @@ -1,8 +1,7 @@ use crate::firmware::firmware_state::{ - AppCrashesStep, FirmwareState, GameRunningStep, LoadCartridgeStep, ShellRunningStep, + AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep, }; use crate::firmware::prometeu_context::PrometeuContext; -use prometeu_hal::InputSignals; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::log::{LogLevel, LogSource}; @@ -41,39 +40,10 @@ impl HubHomeStep { Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { Ok(cartridge) => { if cartridge.app_mode == AppMode::Game - && let Some(task_id) = ctx.os.lifecycle().resident_game_task() + && ctx.os.lifecycle().resident_game_task().is_some() { if ctx.os.vm().current_app_id() == cartridge.app_id { - match ctx.os.lifecycle().resume_task(task_id) { - Ok(()) => { - ctx.os - .vm() - .transition_render_owner(AppMode::Game, cartridge.app_id); - ctx.platform - .input_mut() - .pad_mut() - .begin_frame(&InputSignals::default()); - ctx.platform - .input_mut() - .touch_mut() - .begin_frame(&InputSignals::default()); - return Some(FirmwareState::GameRunning(GameRunningStep::new( - task_id, - ))); - } - Err(error) => { - ctx.os.log( - LogLevel::Warn, - LogSource::Hub, - 0, - format!( - "Failed to resume resident Home game {}: {error:?}", - cartridge.title - ), - ); - return None; - } - } + return Some(FirmwareState::ResumeResidentGame); } let resident_app_id = ctx.os.vm().current_app_id(); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1f61aee2..54a349fc 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md b/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md index f3982d76..e73b3070 100644 --- a/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md +++ b/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md @@ -2,7 +2,7 @@ id: PLN-0145 ticket: foreground-stack-game-pause-shell-vm-backed title: Centralize Resident Game Resume Transition -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From 6646ccfbb5fa6571fbff6b5689e68c845df45529 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 18:52:49 +0100 Subject: [PATCH 16/29] implements PLN-0146 --- .../src/os/facades/lifecycle.rs | 21 +++++++------------ discussion/index.ndjson | 2 +- ...lidate-lifecycle-process-lookup-helpers.md | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs index 9cf67528..680a1cc5 100644 --- a/crates/console/prometeu-system/src/os/facades/lifecycle.rs +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -4,7 +4,7 @@ use crate::os::{ DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, LifecycleOperation, }; -use crate::process::{ProcessId, ProcessKind, ProcessState}; +use crate::process::{Process, ProcessId, ProcessKind, ProcessState}; use crate::services::foreground::{ ForegroundOwner, ForegroundStackError, ResidentGame, ResidentGameState, }; @@ -20,21 +20,11 @@ impl<'a> LifecycleFacade<'a> { } pub fn process_state_for_task(&self, task_id: TaskId) -> Result { - let process_id = process_id_for_task(self.os, task_id)?; - self.os - .process_manager - .get(process_id) - .map(|process| process.state) - .ok_or(LifecycleError::ProcessNotFound(process_id)) + process_for_task(self.os, task_id).map(|process| process.state) } pub fn process_kind_for_task(&self, task_id: TaskId) -> Result { - let process_id = process_id_for_task(self.os, task_id)?; - self.os - .process_manager - .get(process_id) - .map(|process| process.kind) - .ok_or(LifecycleError::ProcessNotFound(process_id)) + process_for_task(self.os, task_id).map(|process| process.kind) } pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { @@ -267,6 +257,11 @@ fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result Result<&Process, LifecycleError> { + let process_id = process_id_for_task(os, task_id)?; + os.process_manager.get(process_id).ok_or(LifecycleError::ProcessNotFound(process_id)) +} + fn map_foreground_error(error: ForegroundStackError) -> LifecycleError { match error { ForegroundStackError::ResidentGameAlreadyExists { existing, requested } => { diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 54a349fc..1173d078 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md b/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md index f890917a..22bb498b 100644 --- a/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md +++ b/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md @@ -2,7 +2,7 @@ id: PLN-0146 ticket: foreground-stack-game-pause-shell-vm-backed title: Consolidate Lifecycle Process Lookup Helpers -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From bd27f5eb63d2c02e644b1ca7170960cc8487cfc3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 18:55:24 +0100 Subject: [PATCH 17/29] implements PLN-0147 --- .../firmware/firmware_step_shell_running.rs | 28 +++---- .../src/programs/prometeu_hub/prometeu_hub.rs | 75 +++++++++++++++++++ discussion/index.ndjson | 2 +- ...ve-native-shell-profile-update-into-hub.md | 2 +- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs index aff9a139..856bdd3f 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs @@ -74,26 +74,11 @@ impl ShellRunningStep { } }; - let action = match process_kind { + let outcome = match process_kind { ProcessKind::VmShell => { - let outcome = - ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform); - - if let Some(report) = outcome.crash { - let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); - return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); - } - - outcome.action - } - ProcessKind::NativeShell => { - let mut action = ctx.hub.gui_update(ctx.os, ctx.platform); - if ctx.platform.input().pad().start().down { - action = Some(SystemProfileAction::CloseShell); - } - ctx.hub.render(ctx.os, ctx.platform); - action + ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform) } + ProcessKind::NativeShell => ctx.hub.update_native_shell_profile(ctx.os, ctx.platform), ProcessKind::VmGame => { let report = CrashReport::VmPanic { message: format!( @@ -106,7 +91,12 @@ impl ShellRunningStep { } }; - if action == Some(SystemProfileAction::CloseShell) { + if let Some(report) = outcome.crash { + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + + if outcome.action == Some(SystemProfileAction::CloseShell) { self.close_current_shell(ctx); return Some(FirmwareState::HubHome(HubHomeStep)); } 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 d7bf48f9..9496fb58 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 @@ -193,6 +193,22 @@ impl PrometeuHub { SystemProfileUpdate { crash, action } } + + pub fn update_native_shell_profile( + &mut self, + os: &mut SystemOS, + platform: &mut dyn RuntimePlatform, + ) -> SystemProfileUpdate { + let mut action = self.gui_update(os, platform); + + if os.windows().focused_window().is_some() && platform.input().pad().start().down { + action = Some(SystemProfileAction::CloseShell); + } + + self.render(os, platform); + + SystemProfileUpdate { crash: None, action } + } } fn action_for_click( @@ -398,6 +414,34 @@ fn draw_buffer_text(commands: &mut Vec, x: i32, y: i32, text: &str #[cfg(test)] mod tests { use super::*; + use crate::task::TaskId; + use crate::windows::WindowOwner; + use prometeu_drivers::TestPlatform; + use prometeu_hal::{InputSignals, RuntimePlatform}; + + fn focused_native_shell_fixture( + signals: &InputSignals, + ) -> (PrometeuHub, SystemOS, TestPlatform) { + let hub = PrometeuHub::new(); + let mut os = SystemOS::new(None); + let mut platform = TestPlatform::new(); + + { + let mut windows = os.windows(); + let id = windows.add_window( + "ShellA".to_string(), + WindowOwner::Task(TaskId(7)), + SHELL_FRAME, + Color::GREEN, + ); + windows.set_focus(id); + } + + platform.input_mut().pad_mut().begin_frame(signals); + platform.input_mut().touch_mut().begin_frame(signals); + + (hub, os, platform) + } #[test] fn shell_a_button_click_emits_launch_action() { @@ -518,4 +562,35 @@ mod tests { CLOSE_BUTTON.y + CLOSE_BUTTON.h - 1 )); } + + #[test] + fn native_shell_profile_start_close_does_not_tick_vm() { + let signals = InputSignals { start_signal: true, ..Default::default() }; + let (mut hub, mut os, mut platform) = focused_native_shell_fixture(&signals); + + let tick_index_before_update = os.vm().tick_index(); + let outcome = hub.update_native_shell_profile(&mut os, &mut platform); + + assert_eq!(outcome.action, Some(SystemProfileAction::CloseShell)); + assert!(outcome.crash.is_none()); + assert_eq!(os.vm().tick_index(), tick_index_before_update); + } + + #[test] + fn native_shell_profile_close_button_does_not_tick_vm() { + let signals = InputSignals { + f_signal: true, + x_pos: CLOSE_BUTTON.x, + y_pos: CLOSE_BUTTON.y, + ..Default::default() + }; + let (mut hub, mut os, mut platform) = focused_native_shell_fixture(&signals); + + let tick_index_before_update = os.vm().tick_index(); + let outcome = hub.update_native_shell_profile(&mut os, &mut platform); + + assert_eq!(outcome.action, Some(SystemProfileAction::CloseShell)); + assert!(outcome.crash.is_none()); + assert_eq!(os.vm().tick_index(), tick_index_before_update); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1173d078..5c18b8d8 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md b/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md index cdcc2ce1..502c06fd 100644 --- a/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md +++ b/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md @@ -2,7 +2,7 @@ id: PLN-0147 ticket: foreground-stack-game-pause-shell-vm-backed title: Move Native Shell Profile Update Into Hub -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0037] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From 23a181de462d5a8e80ceb6463ee6dbd422a0ee54 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:25:55 +0100 Subject: [PATCH 18/29] VM Context Ownership for Resident Game and VM-Backed Shell --- discussion/index.ndjson | 4 +- ...p-for-resident-game-and-vm-backed-shell.md | 186 ++++++++++++++++ ...ssion-ownership-for-vm-backed-processes.md | 199 ++++++++++++++++++ ...roduce-vm-session-registry-and-identity.md | 92 ++++++++ ...-move-vm-runtime-state-into-vm-sessions.md | 99 +++++++++ ...e-cartridge-loading-through-vm-sessions.md | 90 ++++++++ ...51-schedule-foreground-vm-session-ticks.md | 92 ++++++++ ...e-game-and-vm-shell-session-coexistence.md | 86 ++++++++ ...nership-and-background-ready-scheduling.md | 82 ++++++++ 9 files changed, 928 insertions(+), 2 deletions(-) create mode 100644 discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md create mode 100644 discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md create mode 100644 discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md create mode 100644 discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md create mode 100644 discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md create mode 100644 discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md create mode 100644 discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md create mode 100644 discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 5c18b8d8..6b424383 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":46,"DEC":38,"PLN":148,"LSN":52,"CLSN":1}} +{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":154,"LSN":52,"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md b/discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md new file mode 100644 index 00000000..9e6306b9 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md @@ -0,0 +1,186 @@ +--- +id: AGD-0046 +ticket: foreground-stack-game-pause-shell-vm-backed +title: VM Context Ownership for Resident Game and VM-Backed Shell +status: accepted +created: 2026-07-04 +resolved: +decision: DEC-0038 +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Contexto + +`DEC-0037` define que o v1 deve suportar o fluxo `Game -> Home/Shell -> same +Game`, com o Game residente pausado/suspenso enquanto Hub ou Shell possuem o +foreground. A implementacao atual ja impede que Shell nativo ticke o Game +suspenso, porque Shell nativo nao usa a VM. + +A lacuna aparece quando o Shell tambem eh VM-backed. Hoje `Firmware` possui um +unico `VirtualMachine` global (`Firmware.vm`). O Game residente e um Shell +VM-backed competem por esse mesmo contexto. Se o Shell VM-backed inicializa uma +nova cartridge nesse objeto, ele sobrescreve o contexto do Game que deveria +permanecer residente/suspenso. + +Isso significa que o modelo de foreground/task/process ja avancou, mas o modelo +de contexto VM ainda esta acoplado ao firmware global. + +## Problema + +Precisamos decidir onde vive o contexto de execucao VM quando existem, ao mesmo +tempo, um Game residente suspenso e um Shell VM-backed foreground. + +O problema nao eh apenas pausar ticks. Para coexistencia real, cada VM-backed +process precisa preservar seu proprio estado: PC, stack, heap, globals, +cartridge identity, runtime services escopados, render ownership metadata e +eventos lifecycle pendentes. + +Enquanto `Firmware.vm` for unico, "suspender" um Game VM nao preserva o +contexto se outra cartridge VM-backed for carregada. + +## Pontos Criticos + +- `DEC-0037` diz que um Game suspenso nao recebe ticks/input/frame pacing, mas + continua residente para voltar ao mesmo Game. +- Shell nativo nao precisa de VM context proprio, mas Shell VM-backed precisa. +- `SystemOS` ja eh autoridade de lifecycle/process/task, entao eh candidato + natural para possuir ou localizar contextos VM por sessao. +- `Firmware` deve orquestrar macro estados, mas nao deveria ser dono unico do + contexto de todos os processos VM-backed. +- `VirtualMachineRuntime` contem estado atualmente misto: identidade da + cartridge atual, render manager, lifecycle delivery, fs/memcard/open handles + e telemetria. Precisamos separar o que eh global do OS do que eh por sessao + VM-backed. +- A solucao deve preservar a restricao v1: no maximo um Game residente e no + maximo um Shell foreground. Ela nao deve abrir multitasking geral. +- Game-to-Game switching continua fora desta agenda e pertence a `DSC-0043`. + +## Opcoes + +### Opcao A - Manter VM global no Firmware e bloquear Shell VM-backed com Game residente + +- **Abordagem:** documentar que Shell VM-backed nao pode abrir enquanto um Game + VM esta residente; apenas Shell nativo coexiste com Game suspenso. +- **Pro:** menor implementacao imediata. +- **Contra:** contradiz a intencao de coexistencia de VM-backed Shell em + `DEC-0037` e deixa a arquitetura incompleta. +- **Manutenibilidade:** baixa, porque o sistema teria regras diferentes para + Shell nativo e Shell VM-backed sem uma fronteira clara de sessao. + +### Opcao B - Serializar o Game VM ao suspender e restaurar ao resumir + +- **Abordagem:** ao suspender Game, extrair snapshot do `VirtualMachine` e do + runtime associado; ao resumir, restaurar snapshot depois do Shell VM-backed. +- **Pro:** preserva a ideia de um unico VM executor ativo. +- **Contra:** exige serializacao profunda e correta de todo o estado VM/runtime, + incluindo handles, assets, render state e side effects. Isso eh muito mais + complexo que a necessidade v1. +- **Manutenibilidade:** baixa a media; snapshots viram contrato dificil e + fragil antes de termos necessidade de hibernacao real. + +### Opcao C - Tornar o contexto VM propriedade da sessao/processo VM-backed + +- **Abordagem:** cada processo VM-backed possui seu proprio `VirtualMachine` e + seu contexto runtime VM-scoped. O scheduler/firmware seleciona qual sessao + recebe tick conforme foreground/lifecycle. Game suspenso simplesmente deixa + de ser tickado, mas seu contexto permanece armazenado na sessao Game. +- **Pro:** alinha com task/process/lifecycle, suporta Game residente + Shell + VM-backed sem sobrescrever contexto, e prepara o terreno para futuras sessoes + sem implementar multitasking geral agora. +- **Contra:** exige separar estado hoje concentrado em `Firmware.vm` e + `SystemOS.vm_runtime`, decidindo o que eh global do OS e o que eh por sessao. +- **Manutenibilidade:** alta, porque a posse do contexto acompanha o processo + que executa a VM. + +### Opcao D - Criar apenas dois slots VM fixos: Game VM e Shell VM + +- **Abordagem:** manter um slot `game_vm` e um slot `shell_vm`, em vez de uma VM + por processo/sessao. +- **Pro:** mais simples que uma tabela geral de sessoes. +- **Contra:** codifica uma limitacao acidental. Ainda precisariamos decidir + ownership, runtime scoped state e lifecycle por slot. Pode virar refactor + duplo quando mais de um Shell ou Game switching aparecer. +- **Manutenibilidade:** media; aceitavel como stepping stone somente se modelado + internamente como sessao, nao como campos especiais permanentes. + +## Sugestao / Recomendacao + +Seguir a Opcao C: contexto VM deve pertencer a uma sessao/processo VM-backed, +nao ao `Firmware` global. + +Para v1, a implementacao pode continuar limitada a uma sessao Game residente e +uma sessao Shell foreground, mas o modelo deve ser "session-owned VM context". +Isso permite que Shell VM-backed rode enquanto o Game fica suspenso sem perder +PC/stack/heap/estado runtime. + +O firmware deve deixar de possuir a VM como estado global de todos os apps e +passar a pedir ao SystemOS/runtime scheduler o contexto VM ativo para o owner +foreground. `SystemOS` deve continuar sendo a autoridade de lifecycle; o +runtime deve separar estado VM-scoped de estado global de OS/render/telemetria +conforme necessario. + +## Perguntas em Aberto + +- [ ] Qual objeto deve representar a sessao VM-backed: `Process`, novo + `VmSession`, ou uma tabela separada ligada a `ProcessId`/`TaskId`? +- [ ] Quais campos de `VirtualMachineRuntime` sao por sessao VM-backed e quais + devem permanecer globais em `SystemOS`? +- [ ] O render manager fica global por foreground owner ou dentro de cada + contexto VM com handoff global? +- [ ] Handles de FS, memcard, assets e open files devem ser por sessao, + por app_id, ou globais com namespace por processo? +- [ ] Como `LoadCartridgeStep` deve mudar: criar sessao VM nova, ativar sessao + existente, ou delegar tudo para uma facade de sessions? +- [ ] Como preservar os testes e invariantes de `DEC-0037` durante a migracao + incremental sem abrir multitasking geral? +- [ ] Como processos background-capable, como um player de musica, entram no + modelo sem confundir foreground visual ownership com elegibilidade de + execucao? +- [ ] Cada Shell VM-backed deve ter uma VM propria, mesmo quando existe mais de + um Shell em memoria no futuro? + +## Criterio para Encerrar + +Esta agenda pode virar decision quando definirmos: + +- o proprietario canonico do contexto VM; +- a fronteira entre estado VM-scoped e estado global do OS; +- como firmware seleciona/ticka a sessao VM ativa; +- como Game residente suspenso e Shell VM-backed foreground coexistem; +- quais restricoes v1 permanecem explicitamente fora de escopo. + +## Discussion + +- 2026-07-04: Aberta a partir da observacao de que Shell nativo coexiste com + Game suspenso, mas Shell VM-backed ainda conflita com o `Firmware.vm` unico. +- 2026-07-04: Discussao adicional separou foreground ownership de background + execution. A recomendacao atual eh que todo processo VM-backed tenha sua + propria `VmSession`; processos background-capable devem ser modelados como + sessoes com classe/politica de scheduling propria, nao como foreground owners. + +## Resolution + +Resolvido em 2026-07-04: + +- O modelo deve seguir a Opcao C: todo processo VM-backed deve possuir uma + `VmSession` propria, ligada ao lifecycle/task/process, em vez de depender de + um `Firmware.vm` global. +- Cada Shell VM-backed deve ter sua propria sessao VM. Ele pode compartilhar a + engine/runtime e caches imutaveis, mas nao stack, heap, PC, globals, handles + ou identidade mutavel de execucao. +- O Game residente suspenso preserva seu contexto porque sua `VmSession` + continua viva; ele apenas deixa de ser elegivel para ticks normais enquanto + Hub/Shell possui foreground. +- Foreground ownership e background execution sao conceitos separados. + Foreground ownership decide input visual, render ownership e frame pacing + normal. Execution eligibility decide quais sessoes podem receber ticks sem + serem foreground. +- Background execution, como player de musica ou background services, nao entra + na primeira implementacao desta agenda. Mesmo assim, o modelo de `VmSession` + deve ser obrigatoriamente compativel com essa evolucao em um futuro proximo. +- Render ownership permanece autoridade global do SystemOS. Sessoes VM-backed + produzem frames, mas nao decidem se podem apresentar. +- `LoadCartridgeStep` nao deve inicializar uma VM global diretamente. Ele deve + delegar criacao/ativacao de sessao para uma facade de sessions/scheduler. + +Esta agenda esta pronta para virar decision normativa. diff --git a/discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md b/discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md new file mode 100644 index 00000000..f923823c --- /dev/null +++ b/discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md @@ -0,0 +1,199 @@ +--- +id: DEC-0038 +ticket: foreground-stack-game-pause-shell-vm-backed +title: VM Session Ownership for VM-Backed Processes +status: accepted +created: 2026-07-04 +ref_agenda: AGD-0046 +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Status + +Open for review. + +## Contexto + +`DEC-0037` established the foreground stack and Game pause contract for +`Game -> Home/Shell -> same Game`. It requires a suspended resident Game to stop +receiving normal gameplay ticks, input, and frame pacing while remaining +resident for later resume. + +The current implementation works for native Shell apps because native Shell +apps do not use the VM. It does not fully support VM-backed Shell coexistence: +`Firmware` owns a single global `VirtualMachine` (`Firmware.vm`), so loading a +VM-backed Shell can overwrite the suspended Game context that should remain +resident. + +`AGD-0046` closes this gap by separating VM execution context ownership from +foreground ownership. A foreground owner decides who receives visual/input +authority. A VM execution context decides where PC, stack, heap, globals, +handles, cartridge identity, lifecycle delivery state, and VM-scoped runtime +state live. + +## Decisao + +PROMETEU MUST make VM execution context session-owned for VM-backed processes. + +Every VM-backed process MUST have its own `VmSession` or equivalent +session-owned context. A VM-backed Game and a VM-backed Shell MUST NOT share the +same mutable `VirtualMachine` instance, stack, heap, PC, globals, open handles, +cartridge identity, or VM-scoped lifecycle state. + +`Firmware.vm` MUST NOT remain the canonical owner of all VM-backed app context. +Firmware MAY continue to orchestrate macro states such as loading, Home, Shell, +and GameRunning, but it MUST delegate VM session creation, activation, lookup, +and ticking to SystemOS/session services. + +For v1, the system MAY still enforce: + +- at most one resident Game; +- at most one foreground Shell; +- no Game-to-Game switching; +- no general background execution. + +Those restrictions MUST NOT be implemented by relying on a single global VM +context. They must be expressed as lifecycle/session policy. + +Each VM-backed Shell MUST receive its own VM session. If future Shell +persistence allows more than one Shell app to remain in memory, each retained +VM-backed Shell MUST still have distinct mutable VM context. + +A suspended resident Game MUST preserve its VM session. Suspension means the +session is not eligible for normal foreground ticks, normal Game input, or +normal frame pacing. It does not mean serializing, deleting, or overwriting the +VM context. + +Foreground ownership and execution eligibility MUST be separate concepts: + +- foreground ownership controls visual owner, primary input, render ownership, + and normal foreground frame pacing; +- execution eligibility controls which sessions may receive ticks or restricted + callbacks while not foreground. + +Background execution, including music players or background services, is out of +scope for the first implementation of this decision. However, the session model +MUST be compatible with adding background-capable scheduling soon. The initial +model MUST NOT bake in assumptions that only the foreground session can ever be +eligible to execute. + +Render ownership MUST remain a global SystemOS authority. VM sessions may +produce render submissions, but they MUST NOT decide whether those submissions +can be presented. SystemOS/render ownership policy remains responsible for +active owner, epoch/generation validity, stale submission rejection, and final +publication authority. + +`LoadCartridgeStep` MUST stop initializing a global firmware VM directly as the +long-term model. Cartridge load/activation MUST be delegated to a sessions or +VM-session facade that can create a new VM session, activate an existing VM +session, or reject an unsupported transition according to lifecycle policy. + +## Rationale + +The foreground stack is already task/process-oriented. VM execution context must +follow the same ownership model. Otherwise the system can mark a Game as +resident and suspended while the only real VM context has already been reused +for a Shell. + +Session ownership solves the actual coexistence problem without requiring VM +snapshot/restore. A suspended Game is simply a live session that is not scheduled +for normal ticks. A VM-backed Shell is another session with its own mutable +state. + +Keeping background execution out of the first implementation avoids expanding +this work into a general scheduler. At the same time, making execution +eligibility separate from foreground ownership prevents an immediate future +background audio/player feature from requiring another foundational refactor. + +Keeping render ownership global preserves the contract from `DEC-0037` and the +render ownership model from `DSC-0040`: sessions can generate frames, but only +the current foreground/render owner can publish valid current-epoch output. + +## Invariantes / Contrato + +- VM-backed mutable execution state is owned by a VM session, not by a global + firmware VM. +- A VM-backed Game and VM-backed Shell never share mutable VM context. +- A suspended resident Game retains its VM session. +- Suspension prevents normal foreground Game ticks/input/frame pacing; it does + not destroy or overwrite VM state. +- Native Shell apps do not require VM sessions. +- VM-backed Shell apps always require VM sessions. +- Each VM-backed Shell gets its own mutable VM session. +- Foreground ownership and execution eligibility are distinct. +- General background execution is out of scope for the initial implementation, + but the model must support adding it without replacing session ownership. +- SystemOS remains lifecycle and render ownership authority. +- Render ownership and stale frame rejection remain global policy, not per-VM + session policy. +- Cartridge load/activation must go through session services rather than + initializing one global firmware VM. +- V1 restrictions on one resident Game and one foreground Shell remain policy, + not a consequence of single-VM storage. + +## Impactos + +- **Runtime / SystemOS:** add a VM session model or equivalent service that + stores VM-backed process execution context and maps it to task/process + lifecycle. +- **Firmware:** remove `Firmware.vm` as the canonical VM context owner for all + apps. Firmware should request session creation/activation/tick from SystemOS. +- **VM:** support multiple distinct `VirtualMachine` instances or equivalent + isolated VM contexts in one OS lifetime. +- **Lifecycle:** task/process states must control session scheduling + eligibility without conflating foreground ownership with future background + execution. +- **Hub / Shell:** VM-backed Shell load must create or activate a Shell VM + session without overwriting a resident Game session. +- **Game:** Game pause/suspend/resume must preserve the Game VM session and + deliver lifecycle events to that session. +- **Renderer:** render manager stays global and validates ownership/epoch for + submissions produced by sessions. +- **FS / Memcard / Assets:** mutable handles and per-app runtime handles must + be evaluated for session scoping. Durable memcard/app data can remain keyed by + `app_id`; open handles and staging state should not accidentally leak across + sessions. +- **Tests:** add coverage proving VM-backed Shell creation does not overwrite a + suspended Game session, and that only eligible sessions tick. + +## Alternativas Descartadas + +- **Keep one global firmware VM and block VM-backed Shell when a Game is + resident:** rejected because it leaves `DEC-0037` incomplete and creates + divergent Shell behavior. +- **Serialize/snapshot the Game VM on suspension:** rejected because snapshot + correctness is harder than needed for v1 and would include VM, handles, + render state, assets, and side effects. +- **Hard-code two VM slots (`game_vm`, `shell_vm`):** rejected as a permanent + model because it encodes a short-term limit as architecture. It may only be + acceptable as an internal stepping stone if represented through the same + session abstraction. +- **Treat background-capable apps as hidden foreground owners:** rejected + because it would violate foreground ownership semantics and make render/input + authority ambiguous. + +## Referencias + +- Source agenda: `AGD-0046` +- Foreground stack and Game pause contract: `DEC-0037` +- Runtime mode separation: `DSC-0031` +- SystemOS lifecycle/process/task authority: `DSC-0032` +- VM render parallel execution boundary: `DSC-0040` +- Future Game-to-Game switching: `DSC-0043` + +## Propagacao Necessaria + +- Add implementation plans for introducing VM sessions and moving VM context + out of `Firmware`. +- Add implementation plans for separating VM-scoped runtime state from global + SystemOS/render state. +- Update runtime/firmware specs to describe session-owned VM context and the + distinction between foreground ownership and execution eligibility. +- Update tests for Game resident suspension, VM-backed Shell creation, and + session isolation. +- Keep future background execution as out of scope for first implementation, + but document that the model must support background-capable scheduling. + +## Revision Log + +- 2026-07-04: Initial decision draft from `AGD-0046`. diff --git a/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md b/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md new file mode 100644 index 00000000..06712327 --- /dev/null +++ b/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md @@ -0,0 +1,92 @@ +--- +id: PLN-0148 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Introduce VM Session Registry and Identity +status: open +created: 2026-07-04 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 requires every VM-backed process to own its own mutable VM context instead of sharing `Firmware.vm`. This plan introduces the identity and registry layer required before moving runtime state or changing cartridge loading. + +The implementation must make VM session ownership explicit inside `SystemOS`. A VM-backed Game task and a VM-backed Shell task must be able to coexist as distinct sessions, even before all execution paths are migrated to session-owned VMs. + +## Objective + +Add a `VmSession` model and registry owned by `SystemOS`, keyed by a stable session identity tied to the owning task/process. The registry must support creating, retrieving, and validating VM sessions for VM-backed Game and Shell tasks. + +## Dependencies + +- Source decision: DEC-0038. +- Existing foreground/task/process lifecycle from DEC-0037 remains authoritative. +- Must be implemented before PLN-0149, PLN-0150, PLN-0151, and PLN-0152. + +## Scope + +- Add a session identity type, preferably `VmSessionId`, with a deliberate mapping from the owning `TaskId`. +- Add a `VmSession` data structure containing at least: + - owning `TaskId`; + - owning process id or process lookup path; + - `app_id`, title, version, and `AppMode`; + - initial placeholder for the session-owned `VirtualMachine`; + - initial placeholder for session-owned runtime state to be populated by PLN-0149. +- Add a registry under `crates/console/prometeu-system/src/services/` or an equivalent existing services module. +- Expose creation and lookup through `crates/console/prometeu-system/src/os/facades/sessions.rs`. +- Update `SystemOS` construction in `crates/console/prometeu-system/src/os/system_os.rs` to own the registry. +- Enforce V1 uniqueness: + - at most one resident Game session; + - one foreground Shell task at a time; + - same `app_id` resident Game relaunch maps to the existing resident session rather than creating a second Game session. + +## Non-Goals + +- Do not move all VM runtime fields yet; that is PLN-0149. +- Do not route cartridge loading through sessions yet; that is PLN-0150. +- Do not remove `Firmware.vm` yet. +- Do not implement general background execution. +- Do not allow Game-to-Game switching. + +## Execution Method + +1. Inspect `TaskId`, process ids, and `ProcessKind` definitions in `crates/console/prometeu-system/src/services/task/` and `crates/console/prometeu-system/src/services/process/`. +2. Add the VM session module with a small API: + - create session for an existing VM-backed task/process; + - get immutable/mutable session by task id; + - get resident Game session by `app_id`; + - reject incompatible duplicate Game sessions. +3. Wire the registry into `SystemOS`. +4. Extend `SessionsFacade::create_vm_game_task` and `create_vm_shell_task` so task creation also creates a VM session record. +5. Keep native Shell task creation out of the VM session registry. +6. Add focused unit tests under `prometeu-system` for registry identity and duplicate handling. + +## Acceptance Criteria + +- `SystemOS` owns a VM session registry. +- Creating a VM Game task creates exactly one VM session associated with that task. +- Creating a VM Shell task creates exactly one VM session associated with that task. +- Creating a native Shell task creates no VM session. +- Attempting to create a second resident Game session for a different `app_id` is rejected or routed through the existing lifecycle rejection path. +- Relaunching the same resident Game can resolve the existing VM session by `app_id`. +- No firmware state transition depends on the new registry yet except through task/session creation. + +## Tests + +- `cargo test -p prometeu-system` +- Add unit tests for: + - VM Game task creates a VM session; + - VM Shell task creates a VM session; + - native Shell task does not create a VM session; + - lookup by `TaskId`; + - same resident Game `app_id` resolves to the existing session; + - different resident Game `app_id` remains blocked under V1. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/system_os.rs` +- `crates/console/prometeu-system/src/os/facades/sessions.rs` +- `crates/console/prometeu-system/src/services/mod.rs` +- New or extended module under `crates/console/prometeu-system/src/services/` +- `crates/console/prometeu-system/src/lib.rs` if exports are required diff --git a/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md b/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md new file mode 100644 index 00000000..d03b29f0 --- /dev/null +++ b/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md @@ -0,0 +1,99 @@ +--- +id: PLN-0149 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Move VM Runtime State Into VM Sessions +status: open +created: 2026-07-04 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 states that mutable VM context must belong to the VM-backed process session, not to firmware or a single global runtime slot. The current split has `Firmware.vm: VirtualMachine` and `SystemOS.vm_runtime: VirtualMachineRuntime`, which makes a suspended Game vulnerable to VM-backed Shell execution. + +This plan migrates mutable per-application VM runtime state into the session model introduced by PLN-0148 while keeping global OS services global. + +## Objective + +Make each VM session own the state needed to suspend and later resume that VM-backed process without being overwritten by another VM-backed process. + +## Dependencies + +- Source decision: DEC-0038. +- Requires PLN-0148. +- Must precede full cartridge-loading and scheduler migration in PLN-0150 and PLN-0151. + +## Scope + +- Classify `VirtualMachineRuntime` fields into session-owned versus global OS-owned state. +- Move session-owned fields into the `VmSession` context. +- Keep global services in `SystemOS`, including: + - `LogService`; + - process/task/window managers; + - foreground stack; + - durable `MemcardService`; + - global render publication authority or worker coordination that must remain host-facing. +- Treat these as session-owned unless proven otherwise during implementation: + - `VirtualMachine`; + - current cartridge identity; + - frame and tick counters; + - lifecycle delivery bookkeeping; + - crash report for that session; + - pause/debug flags that apply to that session; + - per-session open files, handles, and staging state used by VM syscalls. +- Keep durable memcard contents keyed by `app_id`; do not turn save data into session-private durable storage. +- Update `VmFacade` and VM runtime helpers so callers can operate on a selected session. + +## Non-Goals + +- Do not redesign render workers beyond separating session-local render command state from global presentation authority. +- Do not implement background execution. +- Do not change syscall semantics except where required to route state through the active session. +- Do not remove host debugger features; preserve or adapt their access path. + +## Execution Method + +1. Add an internal field-by-field classification comment or test-only assertion near `VirtualMachineRuntime` before moving fields. +2. Introduce a session runtime/context type in the VM session module or by splitting `VirtualMachineRuntime` into: + - session-local runtime; + - global render/log/runtime services. +3. Move `VirtualMachine` ownership from firmware placeholder/session stub into the session context. +4. Move per-cartridge and per-frame state from `SystemOS.vm_runtime` into the session context. +5. Move `open_files`, `next_handle`, and VM syscall staging state out of global `SystemOS` if they are VM-session specific. +6. Update `VmRuntimeHost` in `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs` to receive session-local state plus references to required global services. +7. Update `VmFacade` in `crates/console/prometeu-system/src/os/facades/vm.rs` with explicit session-targeted methods. +8. Preserve temporary compatibility methods only where needed for existing host/debugger code, and mark them as active-session views rather than canonical global ownership. +9. Add tests proving two VM sessions can hold different app identity and counters at the same time. + +## Acceptance Criteria + +- No canonical VM state lives in `Firmware`. +- `SystemOS` can hold at least two VM sessions with independent `VirtualMachine` and per-session runtime state. +- VM session A and VM session B can have different `app_id`, title, frame counters, lifecycle delivery history, crash report, open file handles, and debug pause state. +- Durable memcard contents remain shared by `app_id` according to the existing memcard contract. +- Global render ownership remains controlled by SystemOS/host-facing runtime services, not by individual sessions. +- Existing `prometeu-system` VM runtime tests still pass after adapting their setup. + +## Tests + +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-firmware` +- Add tests for: + - independent session app identity; + - independent tick/logical-frame counters; + - independent lifecycle delivery bookkeeping; + - VM Shell session initialization does not mutate resident Game session identity; + - session-scoped file handles do not collide across sessions. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/services/vm_runtime/` +- VM session module introduced by PLN-0148 +- `crates/console/prometeu-system/src/os/facades/vm.rs` +- `crates/console/prometeu-system/src/os/system_os.rs` +- `crates/console/prometeu-system/src/os/facades/fs.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `crates/host/prometeu-host-desktop-winit/src/stats.rs` diff --git a/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md b/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md new file mode 100644 index 00000000..7239a283 --- /dev/null +++ b/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md @@ -0,0 +1,90 @@ +--- +id: PLN-0150 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Route Cartridge Loading Through VM Sessions +status: open +created: 2026-07-04 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 requires cartridge loading to create or activate a VM-backed process session instead of initializing a single global VM. The current `LoadCartridgeStep` initializes `ctx.os.vm().initialize(ctx.vm, &cartridge)` before task/session ownership is selected. + +This plan moves cartridge initialization behind session services so Game and VM-backed Shell loading cannot overwrite each other. + +## Objective + +Route Game and VM Shell cartridge loading through the VM session registry and initialize the cartridge inside the target session-owned VM context. + +## Dependencies + +- Source decision: DEC-0038. +- Requires PLN-0148. +- Requires enough of PLN-0149 to expose session-owned VM/runtime initialization. +- Must precede the end-to-end coexistence validation in PLN-0152. + +## Scope + +- Replace direct global VM initialization in `LoadCartridgeStep`. +- Add a session/facade operation that: + - selects an existing resident Game session when the same `app_id` is relaunched; + - rejects or reports different resident Game launches under V1; + - creates a new VM Shell session for a Shell cartridge; + - initializes the cartridge inside the selected session VM. +- Keep asset manager initialization aligned with the foreground cartridge being launched. +- Ensure `AppCrashesStep` receives initialization failures from the target session. +- Update Hub cartridge selection logic so resume versus load is based on lifecycle/session identity, not global VM identity. + +## Non-Goals + +- Do not implement multiple resident Games. +- Do not implement background execution. +- Do not redesign cartridge format or asset packaging. +- Do not broaden Shell windowing behavior beyond the existing one-foreground-Shell model. + +## Execution Method + +1. Add a `load_vm_cartridge` or equivalent method to `SessionsFacade` or `VmFacade`. +2. Make the method return an execution target: + - existing Game task for same resident `app_id`; + - new Game task for first Game launch; + - new VM Shell task for Shell launch; + - structured error/crash report for invalid VM initialization; + - explicit lifecycle error for blocked second Game. +3. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` to call the facade method instead of initializing `ctx.vm`. +4. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` so same-Game relaunch checks session/lifecycle state rather than `ctx.os.vm().current_app_id()`. +5. Preserve AppMode-based transition: + - Game target transitions to `GameRunning`; + - Shell target creates/focuses a window and transitions to `ShellRunning`. +6. Adapt crash tests and resident-game relaunch tests in firmware. + +## Acceptance Criteria + +- `LoadCartridgeStep` no longer initializes a global firmware VM. +- Loading a Shell cartridge creates/initializes a Shell VM session. +- Loading the first Game cartridge creates/initializes the resident Game VM session. +- Clicking the same resident Game from Hub resumes the existing Game session without reinitializing it. +- Attempting to load a different Game while one resident Game exists remains blocked under V1 with a controlled outcome. +- VM initialization failure still transitions to `AppCrashesStep`. + +## Tests + +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-system` +- Add or update firmware tests for: + - first Game load creates one session and enters `GameRunning`; + - same Game relaunch resumes existing task/session; + - VM Shell load creates a distinct session; + - Shell load does not change resident Game session app identity; + - invalid cartridge reports `CrashReport::VmInit`. + +## 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/facades/vm.rs` +- VM session module introduced by PLN-0148 diff --git a/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md b/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md new file mode 100644 index 00000000..b07684c9 --- /dev/null +++ b/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md @@ -0,0 +1,92 @@ +--- +id: PLN-0151 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Schedule Foreground VM Session Ticks +status: open +created: 2026-07-04 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 separates session ownership from foreground ownership and states that V1 does not provide general background execution. The runtime therefore needs an explicit scheduler path that ticks the VM session eligible for foreground execution, while leaving suspended resident Game sessions intact. + +## Objective + +Make firmware and SystemOS tick the VM session associated with the current foreground VM-backed task, instead of ticking a global VM/runtime pair. + +## Dependencies + +- Source decision: DEC-0038. +- Requires PLN-0148. +- Requires PLN-0149. +- Requires PLN-0150 for loaded cartridges to map to sessions. + +## Scope + +- Add scheduler/facade operations that tick a specific VM session by `TaskId`. +- Update `GameRunningStep` to tick the Game session identified by its `task_id`. +- Update `ShellRunningStep` to tick only VM-backed Shell sessions. +- Ensure native Shell foreground state performs native profile updates without ticking any VM session. +- Keep suspended Game sessions non-executing while Hub or Shell is foreground. +- Preserve lifecycle event delivery: + - pause/resume events go to the resident Game session; + - Shell lifecycle events, if any, remain scoped to the Shell session. +- Preserve host debugger pause/step behavior against the active VM session view. + +## Non-Goals + +- Do not add background execution classes yet. +- Do not allow multiple simultaneous VM ticks in one host frame. +- Do not implement general service/player processes. +- Do not change the 60 Hz firmware tick contract. + +## Execution Method + +1. Add `SystemOS` facade methods for: + - ticking a VM session by task id; + - querying whether a task is VM-backed; + - exposing the active VM session to host/debugger tooling. +2. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` to tick through the task/session facade. +3. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` to branch on process kind: + - VM Shell ticks its VM session; + - native Shell runs native profile logic only. +4. Remove direct `ctx.vm` use from Game/Shell execution steps. +5. Ensure resident Game pause budget can deliver one final pause tick and then stop ticking once suspended. +6. Update host/debugger active VM access paths to resolve through the foreground session. +7. Add regression tests for tick counters across Game, Hub, native Shell, and VM Shell transitions. + +## Acceptance Criteria + +- A suspended Game session does not advance while Hub is foreground. +- A suspended Game session does not advance while a Shell is foreground. +- A VM Shell session advances while that VM Shell is foreground. +- A native Shell does not tick any VM session. +- Returning from Hub to resident Game resumes ticking the same Game session. +- Host debugger and stats read the active VM session, not a stale global VM. + +## Tests + +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-host-desktop-winit` +- Add tests for: + - Game tick count freezes after Home suspension; + - VM Shell tick count advances independently; + - native Shell does not advance Game tick count; + - resume returns to same Game session tick counter; + - active-session debugger snapshot uses foreground VM session. + +## Affected Artifacts + +- `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` +- `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-system/src/os/facades/vm.rs` +- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` +- VM session module introduced by PLN-0148 +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `crates/host/prometeu-host-desktop-winit/src/stats.rs` diff --git a/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md b/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md new file mode 100644 index 00000000..64cef273 --- /dev/null +++ b/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md @@ -0,0 +1,86 @@ +--- +id: PLN-0152 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Validate Game and VM Shell Session Coexistence +status: open +created: 2026-07-04 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 was motivated by the concrete failure mode where a resident Game is suspended, a Shell is opened, and Game logs or state unexpectedly continue under the wrong foreground owner. After VM sessions and scheduling are implemented, the system needs explicit regression coverage for this workflow. + +## Objective + +Add end-to-end and integration validation proving a resident Game VM session and a VM-backed Shell session can coexist without overwriting or ticking each other incorrectly. + +## Dependencies + +- Source decision: DEC-0038. +- Requires PLN-0148, PLN-0149, PLN-0150, and PLN-0151. +- Should be executed before closing the broader DEC-0038 implementation thread. + +## Scope + +- Add firmware-level regression tests for: + - Game launch; + - Home/SystemOS request; + - Game pause/suspend; + - VM Shell launch; + - Shell foreground ticking; + - Shell exit; + - Hub return; + - resident Game resume. +- Add system-level tests for independent session state. +- Add host-level tests only where active VM/debugger/stat routing cannot be covered at firmware/system level. +- Verify logs from the stress cartridge or equivalent test cartridge do not appear while Game is suspended and Shell is foreground. +- Verify Shell logs, frames, or tick counters belong to the Shell session. + +## Non-Goals + +- Do not introduce new architecture in this plan. +- Do not broaden background execution. +- Do not rewrite Hub UI behavior except where required to exercise the flow. +- Do not depend on manual desktop testing as the only evidence. + +## Execution Method + +1. Identify or add deterministic test cartridges/helpers that can emit per-session tick/log evidence. +2. Add a firmware integration test for `Game -> Home -> VM Shell -> Hub -> same Game`. +3. Capture Game session counters before Shell launch and after Shell foreground ticks. +4. Assert the Game session counters and lifecycle delivery state remain stable while Shell is foreground. +5. Assert the Shell session counters advance while Shell is foreground. +6. Assert returning to the same Game resumes the same task/session and then advances the Game counter. +7. Add a regression specifically covering the prior symptom: Game `stress` logs must not appear during Shell foreground execution. +8. Run the relevant crate tests and record evidence in the plan completion notes when implemented. + +## Acceptance Criteria + +- The failing manual scenario is covered by automated tests. +- The Game session is suspended and non-ticking while VM Shell is foreground. +- The VM Shell session ticks independently. +- Returning to Hub and selecting the same Game resumes the resident Game session instead of creating a duplicate process/session. +- No test relies on a single global `current_app_id` to infer correctness. + +## Tests + +- `cargo test -p prometeu-system` +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-host-desktop-winit` if host active-session plumbing changed. +- Optional manual smoke: + - run `cargo run --bin prometeu-host-desktop-winit --profile release --manifest-path crates/host/prometeu-host-desktop-winit/Cargo.toml -- --games-root test-cartridges`; + - open Stress Console; + - press ESC/Home; + - open VM-backed Shell; + - verify no Stress logs appear while Shell is foreground; + - return and resume Stress Console. + +## Affected Artifacts + +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` tests +- `crates/console/prometeu-system/src/os/system_os.rs` tests +- `crates/console/prometeu-system/src/services/vm_runtime/tests.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` tests if needed +- `test-cartridges/` only if a deterministic VM Shell fixture is missing diff --git a/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md b/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md new file mode 100644 index 00000000..2d0f0e81 --- /dev/null +++ b/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md @@ -0,0 +1,82 @@ +--- +id: PLN-0153 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Specify VM Session Ownership and Background-Ready Scheduling +status: open +created: 2026-07-04 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 locks the runtime direction: VM context is session-owned, firmware orchestrates macro-state, and background execution is out of V1 but must remain a near-term compatible extension. The canonical specs must describe that contract in English so future implementation and debugging do not drift back to a single global VM model. + +## Objective + +Update canonical runtime specifications to document VM session ownership, foreground execution eligibility, render publication authority, and the background-ready shape of the scheduler without committing to background execution in this implementation batch. + +## Dependencies + +- Source decision: DEC-0038. +- Should be informed by PLN-0148 through PLN-0151 once implementation names settle. +- May be done before PLN-0152 if the implementation contract is already stable, but final wording must match implemented APIs. + +## Scope + +- Update the canonical specs under `docs/specs/runtime/` in English. +- Document: + - each VM-backed process owns one VM session; + - VM sessions own mutable VM context; + - foreground ownership and execution eligibility are separate concepts; + - V1 executes only the foreground VM-backed task, except the existing pause handoff budget; + - suspended resident Game sessions preserve VM context but do not tick; + - VM-backed Shell sessions have their own VM context; + - native Shell does not own a VM session; + - durable app data remains keyed by `app_id`; + - session-scoped handles and staging state are not durable app data; + - global render publication remains SystemOS/host authority. +- Add background-ready wording: + - scheduler model can later mark sessions or service processes as eligible without changing ownership; + - no current guarantee of background progress is introduced. + +## Non-Goals + +- Do not write implementation plans into specs. +- Do not document background execution as available. +- Do not move normative text into `discussion/lessons/`. +- Do not create new legacy docs paths. + +## Execution Method + +1. Locate the most relevant runtime specs, likely: + - `docs/specs/runtime/09-events-and-concurrency.md`; + - `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md`; + - `docs/specs/runtime/14-boot-profiles.md`; + - `docs/specs/runtime/16-host-abi-and-syscalls.md` if host/debugger active VM views are affected. +2. Add concise normative sections using `MUST`, `MUST NOT`, and `SHALL` where appropriate. +3. Keep discussion history out of the spec; cite behavior, not agenda chronology. +4. Align terminology with implemented type names from PLN-0148 through PLN-0151. +5. Run discussion validation after edits and any repository tests required by touched docs tooling. + +## Acceptance Criteria + +- Specs explicitly state that VM context is session-owned for VM-backed processes. +- Specs explicitly state that native Shell does not own a VM session. +- Specs distinguish foreground ownership from future execution eligibility. +- Specs state that V1 does not support general background execution. +- Specs preserve a future path for background-capable sessions/services without changing VM ownership. +- No new framework artifacts are written outside `discussion/`. + +## Tests + +- `discussion validate` +- Manual review of touched specs for English-only normative text. +- Run repository doc checks if such a command exists; otherwise no code test is required for this plan. + +## Affected Artifacts + +- `docs/specs/runtime/09-events-and-concurrency.md` +- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` +- `docs/specs/runtime/14-boot-profiles.md` +- `docs/specs/runtime/16-host-abi-and-syscalls.md` if active VM host/debugger wording is needed From 49a676ff86f6cb39ad2d0cd1f84be87bd3dbb455 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:30:00 +0100 Subject: [PATCH 19/29] implements PLN-0148 --- crates/console/prometeu-system/src/lib.rs | 1 + .../src/os/facades/sessions.rs | 50 ++++- .../prometeu-system/src/os/system_os.rs | 61 ++++++ .../prometeu-system/src/services/mod.rs | 1 + .../src/services/vm_session.rs | 195 ++++++++++++++++++ discussion/index.ndjson | 2 +- ...roduce-vm-session-registry-and-identity.md | 2 +- 7 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 crates/console/prometeu-system/src/services/vm_session.rs diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index c297a6fe..3baf7d46 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -29,4 +29,5 @@ pub use services::vm_runtime::{ LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff, RenderWorkerHandoffWait, RenderWorkerOwnership, VirtualMachineRuntime, }; +pub use services::vm_session; pub use services::windows; diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs index 9dd7246a..459194d7 100644 --- a/crates/console/prometeu-system/src/os/facades/sessions.rs +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -1,5 +1,7 @@ +use crate::os::LifecycleError; use crate::os::SystemOS; use crate::task::TaskId; +use crate::vm_session::{VmSession, VmSessionError}; pub struct SessionsFacade<'a> { pub(in crate::os) os: &'a mut SystemOS, @@ -7,15 +9,27 @@ pub struct SessionsFacade<'a> { impl<'a> SessionsFacade<'a> { pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into) -> TaskId { + self.try_create_vm_game_task(app_id, title) + .expect("newly created game task should have an associated process") + } + + pub fn try_create_vm_game_task( + &mut self, + app_id: u32, + title: impl Into, + ) -> Result { + if let Some(session) = self.os.vm_sessions.resident_game_for_app(app_id) { + return Ok(session.task_id); + } + let title = title.into(); let process_id = self.os.process_manager.spawn_vm_game(app_id, title.clone()); let task_id = self.os.task_manager.create_game_task(process_id, app_id, title); - self.os - .lifecycle() - .set_game_foreground_task(task_id) - .expect("newly created game task should have an associated process"); + self.os.lifecycle().set_game_foreground_task(task_id)?; + self.create_vm_session_for_task(task_id) + .expect("newly created game task should create a VM session"); - task_id + Ok(task_id) } pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { @@ -26,6 +40,8 @@ impl<'a> SessionsFacade<'a> { .lifecycle() .set_shell_foreground_task(task_id) .expect("newly created shell task should have an associated process"); + self.create_vm_session_for_task(task_id) + .expect("newly created shell task should create a VM session"); task_id } @@ -41,4 +57,28 @@ impl<'a> SessionsFacade<'a> { task_id } + + pub fn vm_session_for_task(&self, task_id: TaskId) -> Option<&VmSession> { + self.os.vm_sessions.for_task(task_id) + } + + pub fn resident_game_session_for_app(&self, app_id: u32) -> Option<&VmSession> { + self.os.vm_sessions.resident_game_for_app(app_id) + } + + pub fn vm_session_count(&self) -> usize { + self.os.vm_sessions.len() + } + + fn create_vm_session_for_task(&mut self, task_id: TaskId) -> Result<(), VmSessionError> { + let task = self.os.task_manager.get(task_id).expect("session task should exist").clone(); + let process = self + .os + .process_manager + .get(task.process_id) + .expect("session process should exist") + .clone(); + + self.os.vm_sessions.create_for_task(&task, &process, "").map(|_| ()) + } } diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 439e21df..2398f2a7 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -6,6 +6,7 @@ use crate::os::{ use crate::process::ProcessManager; use crate::services::foreground::ForegroundStack; use crate::services::memcard::MemcardService; +use crate::services::vm_session::VmSessionRegistry; use crate::services::windows::WindowManager; use crate::task::TaskManager; use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource}; @@ -18,6 +19,7 @@ pub struct SystemOS { pub(super) process_manager: ProcessManager, pub(super) task_manager: TaskManager, pub(super) foreground_stack: ForegroundStack, + pub(super) vm_sessions: VmSessionRegistry, pub(super) game_lifecycle_events: Vec, pub(super) window_manager: WindowManager, pub(super) log_service: LogService, @@ -39,6 +41,7 @@ impl SystemOS { process_manager: ProcessManager::new(), task_manager: TaskManager::new(), foreground_stack: ForegroundStack::new(), + vm_sessions: VmSessionRegistry::new(), game_lifecycle_events: Vec::new(), window_manager: WindowManager::new(), log_service, @@ -144,6 +147,7 @@ mod tests { use crate::process::{ProcessKind, ProcessState}; use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState}; use crate::task::{TaskId, TaskState}; + use prometeu_hal::app_mode::AppMode; fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState { let task = os.task_manager.get(task_id).expect("task should exist"); @@ -198,6 +202,63 @@ mod tests { ); } + #[test] + fn vm_game_task_creates_vm_session() { + let mut os = SystemOS::new(None); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + let sessions = os.sessions(); + let session = sessions.vm_session_for_task(task_id).expect("VM session should exist"); + + assert_eq!(session.task_id, task_id); + assert_eq!(session.app_id, 42); + assert_eq!(session.title, "Sector Crawl"); + assert_eq!(session.app_mode, AppMode::Game); + assert_eq!(sessions.vm_session_count(), 1); + } + + #[test] + fn vm_shell_task_creates_vm_session() { + let mut os = SystemOS::new(None); + let task_id = os.sessions().create_vm_shell_task(7, "Settings"); + + let sessions = os.sessions(); + let session = sessions.vm_session_for_task(task_id).expect("VM session should exist"); + + assert_eq!(session.task_id, task_id); + assert_eq!(session.app_id, 7); + assert_eq!(session.title, "Settings"); + assert_eq!(session.app_mode, AppMode::Shell); + assert_eq!(sessions.vm_session_count(), 1); + } + + #[test] + fn native_shell_task_does_not_create_vm_session() { + let mut os = SystemOS::new(None); + let task_id = os.sessions().create_native_shell_task(7, "Settings"); + + let sessions = os.sessions(); + + assert!(sessions.vm_session_for_task(task_id).is_none()); + assert_eq!(sessions.vm_session_count(), 0); + } + + #[test] + fn same_resident_game_app_reuses_existing_vm_session() { + let mut os = SystemOS::new(None); + let first = os.sessions().create_vm_game_task(42, "Sector Crawl"); + let second = os.sessions().create_vm_game_task(42, "Sector Crawl"); + + let sessions = os.sessions(); + + assert_eq!(second, first); + assert_eq!( + sessions.resident_game_session_for_app(42).map(|session| session.task_id), + Some(first) + ); + assert_eq!(sessions.vm_session_count(), 1); + } + #[test] fn home_request_notifies_game_before_budget_suspends_it() { let mut os = SystemOS::new(None); diff --git a/crates/console/prometeu-system/src/services/mod.rs b/crates/console/prometeu-system/src/services/mod.rs index 4c32356b..aa7496a1 100644 --- a/crates/console/prometeu-system/src/services/mod.rs +++ b/crates/console/prometeu-system/src/services/mod.rs @@ -6,4 +6,5 @@ pub mod memcard; pub mod process; pub mod task; pub mod vm_runtime; +pub mod vm_session; pub mod windows; diff --git a/crates/console/prometeu-system/src/services/vm_session.rs b/crates/console/prometeu-system/src/services/vm_session.rs new file mode 100644 index 00000000..fac91798 --- /dev/null +++ b/crates/console/prometeu-system/src/services/vm_session.rs @@ -0,0 +1,195 @@ +use crate::process::{Process, ProcessId, ProcessKind}; +use crate::task::{Task, TaskId, TaskKind}; +use prometeu_hal::app_mode::AppMode; +use prometeu_vm::VirtualMachine; +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct VmSessionId(pub TaskId); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VmSessionError { + SessionAlreadyExists(TaskId), + TaskProcessMismatch { task_id: TaskId, task_process: ProcessId, process_id: ProcessId }, + TaskKindMismatch { task_id: TaskId, task_kind: TaskKind, process_kind: ProcessKind }, + ProcessIsNotVmBacked(ProcessId), +} + +pub struct VmSession { + pub id: VmSessionId, + pub task_id: TaskId, + pub process_id: ProcessId, + pub app_id: u32, + pub title: String, + pub app_version: String, + pub app_mode: AppMode, + pub vm: VirtualMachine, +} + +impl VmSession { + fn new( + task: &Task, + process: &Process, + app_mode: AppMode, + app_version: impl Into, + ) -> Self { + Self { + id: VmSessionId(task.id), + task_id: task.id, + process_id: process.id, + app_id: task.app_id, + title: task.title.clone(), + app_version: app_version.into(), + app_mode, + vm: VirtualMachine::default(), + } + } +} + +#[derive(Default)] +pub struct VmSessionRegistry { + sessions: HashMap, +} + +impl VmSessionRegistry { + pub fn new() -> Self { + Self { sessions: HashMap::new() } + } + + pub fn create_for_task( + &mut self, + task: &Task, + process: &Process, + app_version: impl Into, + ) -> Result { + if task.process_id != process.id { + return Err(VmSessionError::TaskProcessMismatch { + task_id: task.id, + task_process: task.process_id, + process_id: process.id, + }); + } + + let app_mode = app_mode_for_task_and_process(task, process)?; + let id = VmSessionId(task.id); + if self.sessions.contains_key(&id) { + return Err(VmSessionError::SessionAlreadyExists(task.id)); + } + + self.sessions.insert(id, VmSession::new(task, process, app_mode, app_version)); + Ok(id) + } + + pub fn get(&self, id: VmSessionId) -> Option<&VmSession> { + self.sessions.get(&id) + } + + pub fn get_mut(&mut self, id: VmSessionId) -> Option<&mut VmSession> { + self.sessions.get_mut(&id) + } + + pub fn for_task(&self, task_id: TaskId) -> Option<&VmSession> { + self.get(VmSessionId(task_id)) + } + + pub fn for_task_mut(&mut self, task_id: TaskId) -> Option<&mut VmSession> { + self.get_mut(VmSessionId(task_id)) + } + + pub fn resident_game(&self) -> Option<&VmSession> { + self.sessions.values().find(|session| session.app_mode == AppMode::Game) + } + + pub fn resident_game_for_app(&self, app_id: u32) -> Option<&VmSession> { + self.sessions + .values() + .find(|session| session.app_mode == AppMode::Game && session.app_id == app_id) + } + + pub fn len(&self) -> usize { + self.sessions.len() + } + + pub fn is_empty(&self) -> bool { + self.sessions.is_empty() + } +} + +fn app_mode_for_task_and_process( + task: &Task, + process: &Process, +) -> Result { + match (task.kind, process.kind) { + (TaskKind::Game, ProcessKind::VmGame) => Ok(AppMode::Game), + (TaskKind::Shell, ProcessKind::VmShell) => Ok(AppMode::Shell), + (_, ProcessKind::NativeShell) => Err(VmSessionError::ProcessIsNotVmBacked(process.id)), + _ => Err(VmSessionError::TaskKindMismatch { + task_id: task.id, + task_kind: task.kind, + process_kind: process.kind, + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::process::Process; + use crate::task::Task; + + #[test] + fn creates_vm_game_session_for_vm_game_task() { + let process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame); + let task = Task::new(TaskId(7), process.id, 42, "Stress", TaskKind::Game); + let mut registry = VmSessionRegistry::new(); + + let id = registry.create_for_task(&task, &process, "").expect("session should create"); + let session = registry.get(id).expect("session should exist"); + + assert_eq!(session.id, VmSessionId(task.id)); + assert_eq!(session.task_id, task.id); + assert_eq!(session.process_id, process.id); + assert_eq!(session.app_id, 42); + assert_eq!(session.title, "Stress"); + assert_eq!(session.app_mode, AppMode::Game); + } + + #[test] + fn creates_vm_shell_session_for_vm_shell_task() { + let process = Process::new(ProcessId(1), 7, "Shell", ProcessKind::VmShell); + let task = Task::new(TaskId(3), process.id, 7, "Shell", TaskKind::Shell); + let mut registry = VmSessionRegistry::new(); + + let id = registry.create_for_task(&task, &process, "1.0").expect("session should create"); + let session = registry.get(id).expect("session should exist"); + + assert_eq!(session.app_mode, AppMode::Shell); + assert_eq!(session.app_version, "1.0"); + } + + #[test] + fn rejects_native_shell_process() { + let process = Process::new(ProcessId(1), 7, "Shell", ProcessKind::NativeShell); + let task = Task::new(TaskId(3), process.id, 7, "Shell", TaskKind::Shell); + let mut registry = VmSessionRegistry::new(); + + assert_eq!( + registry.create_for_task(&task, &process, ""), + Err(VmSessionError::ProcessIsNotVmBacked(process.id)) + ); + } + + #[test] + fn finds_resident_game_by_app_id() { + let process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame); + let task = Task::new(TaskId(7), process.id, 42, "Stress", TaskKind::Game); + let mut registry = VmSessionRegistry::new(); + registry.create_for_task(&task, &process, "").expect("session should create"); + + assert_eq!( + registry.resident_game_for_app(42).map(|session| session.task_id), + Some(task.id) + ); + assert_eq!(registry.resident_game_for_app(43).map(|session| session.task_id), None); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 6b424383..23009172 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md b/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md index 06712327..fc1717db 100644 --- a/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md +++ b/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md @@ -2,7 +2,7 @@ id: PLN-0148 ticket: foreground-stack-game-pause-shell-vm-backed title: Introduce VM Session Registry and Identity -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From c3d6b70b0cb35b94c0920de77e5fb8d5f9e6a421 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:34:04 +0100 Subject: [PATCH 20/29] implements PLN-0149 --- .../src/os/facades/sessions.rs | 12 ++- .../prometeu-system/src/os/system_os.rs | 36 ++++++++ .../src/services/vm_session.rs | 92 ++++++++++++++++++- discussion/index.ndjson | 2 +- ...-move-vm-runtime-state-into-vm-sessions.md | 2 +- 5 files changed, 136 insertions(+), 8 deletions(-) diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs index 459194d7..d9a89ef2 100644 --- a/crates/console/prometeu-system/src/os/facades/sessions.rs +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -2,6 +2,7 @@ use crate::os::LifecycleError; use crate::os::SystemOS; use crate::task::TaskId; use crate::vm_session::{VmSession, VmSessionError}; +use std::sync::Arc; pub struct SessionsFacade<'a> { pub(in crate::os) os: &'a mut SystemOS, @@ -62,6 +63,10 @@ impl<'a> SessionsFacade<'a> { self.os.vm_sessions.for_task(task_id) } + pub fn vm_session_for_task_mut(&mut self, task_id: TaskId) -> Option<&mut VmSession> { + self.os.vm_sessions.for_task_mut(task_id) + } + pub fn resident_game_session_for_app(&self, app_id: u32) -> Option<&VmSession> { self.os.vm_sessions.resident_game_for_app(app_id) } @@ -79,6 +84,11 @@ impl<'a> SessionsFacade<'a> { .expect("session process should exist") .clone(); - self.os.vm_sessions.create_for_task(&task, &process, "").map(|_| ()) + let cap_config = self.os.vm_runtime.certifier.config; + let logs_count = Arc::clone(&self.os.log_service.logs_count); + self.os + .vm_sessions + .create_for_task(&task, &process, "", Some(cap_config), logs_count) + .map(|_| ()) } } diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 2398f2a7..09bb980c 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -259,6 +259,42 @@ mod tests { assert_eq!(sessions.vm_session_count(), 1); } + #[test] + fn vm_sessions_hold_independent_runtime_state() { + 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"); + + { + let mut sessions = os.sessions(); + let game_session = + sessions.vm_session_for_task_mut(game).expect("game VM session should exist"); + game_session.runtime.tick_index = 100; + game_session.open_files.insert(1, "/game.dat".to_string()); + } + + { + let mut sessions = os.sessions(); + let shell_session = + sessions.vm_session_for_task_mut(shell).expect("shell VM session should exist"); + shell_session.runtime.tick_index = 5; + shell_session.open_files.insert(1, "/shell.dat".to_string()); + } + + let sessions = os.sessions(); + let game_session = + sessions.vm_session_for_task(game).expect("game VM session should exist"); + let shell_session = + sessions.vm_session_for_task(shell).expect("shell VM session should exist"); + + assert_eq!(game_session.runtime.tick_index, 100); + assert_eq!(shell_session.runtime.tick_index, 5); + assert_eq!(game_session.open_files.get(&1).map(String::as_str), Some("/game.dat")); + assert_eq!(shell_session.open_files.get(&1).map(String::as_str), Some("/shell.dat")); + } + #[test] fn home_request_notifies_game_before_budget_suspends_it() { 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 fac91798..88a005ef 100644 --- a/crates/console/prometeu-system/src/services/vm_session.rs +++ b/crates/console/prometeu-system/src/services/vm_session.rs @@ -1,8 +1,13 @@ +use crate::VirtualMachineRuntime; +use crate::fs::FsState; use crate::process::{Process, ProcessId, ProcessKind}; use crate::task::{Task, TaskId, TaskKind}; use prometeu_hal::app_mode::AppMode; +use prometeu_hal::telemetry::CertificationConfig; use prometeu_vm::VirtualMachine; use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicU32; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct VmSessionId(pub TaskId); @@ -24,6 +29,10 @@ pub struct VmSession { pub app_version: String, pub app_mode: AppMode, pub vm: VirtualMachine, + pub runtime: VirtualMachineRuntime, + pub fs_state: FsState, + pub open_files: HashMap, + pub next_handle: u32, } impl VmSession { @@ -32,6 +41,8 @@ impl VmSession { process: &Process, app_mode: AppMode, app_version: impl Into, + cap_config: Option, + logs_count: Arc, ) -> Self { Self { id: VmSessionId(task.id), @@ -42,8 +53,17 @@ impl VmSession { app_version: app_version.into(), app_mode, vm: VirtualMachine::default(), + runtime: VirtualMachineRuntime::new_with_log_counter(cap_config, logs_count), + fs_state: FsState::Unmounted, + open_files: HashMap::new(), + next_handle: 1, } } + + pub fn clear_cartridge_service_state(&mut self) { + self.open_files.clear(); + self.next_handle = 1; + } } #[derive(Default)] @@ -61,6 +81,8 @@ impl VmSessionRegistry { task: &Task, process: &Process, app_version: impl Into, + cap_config: Option, + logs_count: Arc, ) -> Result { if task.process_id != process.id { return Err(VmSessionError::TaskProcessMismatch { @@ -76,7 +98,10 @@ impl VmSessionRegistry { return Err(VmSessionError::SessionAlreadyExists(task.id)); } - self.sessions.insert(id, VmSession::new(task, process, app_mode, app_version)); + self.sessions.insert( + id, + VmSession::new(task, process, app_mode, app_version, cap_config, logs_count), + ); Ok(id) } @@ -143,7 +168,9 @@ mod tests { let task = Task::new(TaskId(7), process.id, 42, "Stress", TaskKind::Game); let mut registry = VmSessionRegistry::new(); - let id = registry.create_for_task(&task, &process, "").expect("session should create"); + let id = registry + .create_for_task(&task, &process, "", None, Arc::new(AtomicU32::new(0))) + .expect("session should create"); let session = registry.get(id).expect("session should exist"); assert_eq!(session.id, VmSessionId(task.id)); @@ -152,6 +179,9 @@ mod tests { assert_eq!(session.app_id, 42); assert_eq!(session.title, "Stress"); assert_eq!(session.app_mode, AppMode::Game); + assert_eq!(session.runtime.current_app_id, 0); + assert_eq!(session.fs_state, FsState::Unmounted); + assert_eq!(session.next_handle, 1); } #[test] @@ -160,7 +190,9 @@ mod tests { let task = Task::new(TaskId(3), process.id, 7, "Shell", TaskKind::Shell); let mut registry = VmSessionRegistry::new(); - let id = registry.create_for_task(&task, &process, "1.0").expect("session should create"); + let id = registry + .create_for_task(&task, &process, "1.0", None, Arc::new(AtomicU32::new(0))) + .expect("session should create"); let session = registry.get(id).expect("session should exist"); assert_eq!(session.app_mode, AppMode::Shell); @@ -174,7 +206,7 @@ mod tests { let mut registry = VmSessionRegistry::new(); assert_eq!( - registry.create_for_task(&task, &process, ""), + registry.create_for_task(&task, &process, "", None, Arc::new(AtomicU32::new(0))), Err(VmSessionError::ProcessIsNotVmBacked(process.id)) ); } @@ -184,7 +216,9 @@ mod tests { let process = Process::new(ProcessId(1), 42, "Stress", ProcessKind::VmGame); let task = Task::new(TaskId(7), process.id, 42, "Stress", TaskKind::Game); let mut registry = VmSessionRegistry::new(); - registry.create_for_task(&task, &process, "").expect("session should create"); + registry + .create_for_task(&task, &process, "", None, Arc::new(AtomicU32::new(0))) + .expect("session should create"); assert_eq!( registry.resident_game_for_app(42).map(|session| session.task_id), @@ -192,4 +226,52 @@ mod tests { ); assert_eq!(registry.resident_game_for_app(43).map(|session| session.task_id), None); } + + #[test] + fn session_runtime_state_is_independent_per_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"); + + registry + .for_task_mut(game_task.id) + .expect("game session should exist") + .runtime + .tick_index = 11; + registry + .for_task_mut(shell_task.id) + .expect("shell session should exist") + .runtime + .tick_index = 3; + registry + .for_task_mut(game_task.id) + .expect("game session should exist") + .open_files + .insert(1, "/game.save".to_string()); + + assert_eq!( + registry.for_task(game_task.id).map(|session| session.runtime.tick_index), + Some(11) + ); + assert_eq!( + registry.for_task(shell_task.id).map(|session| session.runtime.tick_index), + Some(3) + ); + assert!( + registry + .for_task(shell_task.id) + .expect("shell session should exist") + .open_files + .is_empty() + ); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 23009172..50954d72 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md b/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md index d03b29f0..c7396a18 100644 --- a/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md +++ b/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md @@ -2,7 +2,7 @@ id: PLN-0149 ticket: foreground-stack-game-pause-shell-vm-backed title: Move VM Runtime State Into VM Sessions -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From fa2e329d4b43a3613df96866dda6b5fc2b366e64 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:36:24 +0100 Subject: [PATCH 21/29] implements PLN-0150 --- .../src/firmware/firmware.rs | 7 +- .../src/firmware/firmware_step_hub_home.rs | 12 ++- .../firmware/firmware_step_load_cartridge.rs | 35 ++++++--- .../src/os/facades/sessions.rs | 75 +++++++++++++++++++ discussion/index.ndjson | 2 +- ...e-cartridge-loading-through-vm-sessions.md | 2 +- 6 files changed, 116 insertions(+), 17 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index f0ff3ece..ddc5548a 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -230,7 +230,12 @@ impl Firmware { return None; } - let app_id = self.os.vm().current_app_id(); + let app_id = self + .os + .sessions() + .vm_session_for_task(task_id) + .map(|session| session.app_id) + .unwrap_or_else(|| self.os.vm().current_app_id()); self.os.vm().transition_render_owner(AppMode::Game, app_id); self.game_input_barrier_frames = self.game_input_barrier_frames.max(1); Some(FirmwareState::GameRunning(GameRunningStep::new(task_id))) 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 cede2f36..fb27d40a 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 @@ -40,13 +40,19 @@ impl HubHomeStep { Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { Ok(cartridge) => { if cartridge.app_mode == AppMode::Game - && ctx.os.lifecycle().resident_game_task().is_some() + && let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task() { - if ctx.os.vm().current_app_id() == cartridge.app_id { + 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); } - let resident_app_id = ctx.os.vm().current_app_id(); ctx.os.log( LogLevel::Warn, 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 bb26adfd..9fedebad 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 @@ -14,11 +14,13 @@ use prometeu_system::windows::WindowOwner; pub struct LoadCartridgeStep { pub cartridge: Cartridge, init_error: Option, + loaded_task_id: Option, + loaded_app_mode: Option, } impl LoadCartridgeStep { pub fn new(cartridge: Cartridge) -> Self { - Self { cartridge, init_error: None } + Self { cartridge, init_error: None, loaded_task_id: None, loaded_app_mode: None } } pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { @@ -36,7 +38,15 @@ impl LoadCartridgeStep { self.cartridge.assets.clone(), ); - self.init_error = ctx.os.vm().initialize(ctx.vm, &self.cartridge).err(); + match ctx.os.sessions().load_vm_cartridge(ctx.vm, &self.cartridge) { + Ok(loaded) => { + self.loaded_task_id = Some(loaded.task_id); + self.loaded_app_mode = Some(loaded.app_mode); + } + Err(report) => { + self.init_error = Some(report); + } + } } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { @@ -44,11 +54,18 @@ impl LoadCartridgeStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - if self.cartridge.app_mode == AppMode::Shell { - let task_id = ctx - .os - .sessions() - .create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone()); + let Some(task_id) = self.loaded_task_id.take() else { + let report = CrashReport::VmPanic { + message: format!( + "cartridge {} was not loaded into a VM session", + self.cartridge.title + ), + pc: None, + }; + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + }; + + if self.loaded_app_mode == Some(AppMode::Shell) { let id = ctx.os.windows().add_window( self.cartridge.title.clone(), WindowOwner::Task(task_id), @@ -59,10 +76,6 @@ impl LoadCartridgeStep { return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id))); } - let task_id = ctx - .os - .sessions() - .create_vm_game_task(self.cartridge.app_id, self.cartridge.title.clone()); Some(FirmwareState::GameRunning(GameRunningStep::new(task_id))) } diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs index d9a89ef2..3d41c2e4 100644 --- a/crates/console/prometeu-system/src/os/facades/sessions.rs +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -1,9 +1,20 @@ +use crate::CrashReport; use crate::os::LifecycleError; use crate::os::SystemOS; use crate::task::TaskId; use crate::vm_session::{VmSession, VmSessionError}; +use prometeu_hal::app_mode::AppMode; +use prometeu_hal::cartridge::Cartridge; +use prometeu_vm::VirtualMachine; use std::sync::Arc; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LoadedVmCartridge { + pub task_id: TaskId, + pub app_mode: AppMode, + pub reused_existing_session: bool, +} + pub struct SessionsFacade<'a> { pub(in crate::os) os: &'a mut SystemOS, } @@ -59,6 +70,42 @@ impl<'a> SessionsFacade<'a> { task_id } + pub fn load_vm_cartridge( + &mut self, + legacy_vm: &mut VirtualMachine, + cartridge: &Cartridge, + ) -> Result { + let existing_game_task = if cartridge.app_mode == AppMode::Game { + self.os + .vm_sessions + .resident_game_for_app(cartridge.app_id) + .map(|session| session.task_id) + } else { + None + }; + + let (task_id, reused_existing_session) = if let Some(task_id) = existing_game_task { + (task_id, true) + } else if cartridge.app_mode == AppMode::Shell { + (self.create_vm_shell_task(cartridge.app_id, cartridge.title.clone()), false) + } else { + let task_id = self + .try_create_vm_game_task(cartridge.app_id, cartridge.title.clone()) + .map_err(|error| CrashReport::VmPanic { + message: format!("failed to create VM game task: {error:?}"), + pc: None, + })?; + (task_id, false) + }; + + if !reused_existing_session { + self.initialize_session_cartridge(task_id, cartridge)?; + self.initialize_legacy_active_vm(legacy_vm, cartridge)?; + } + + Ok(LoadedVmCartridge { task_id, app_mode: cartridge.app_mode, reused_existing_session }) + } + pub fn vm_session_for_task(&self, task_id: TaskId) -> Option<&VmSession> { self.os.vm_sessions.for_task(task_id) } @@ -91,4 +138,32 @@ impl<'a> SessionsFacade<'a> { .create_for_task(&task, &process, "", Some(cap_config), logs_count) .map(|_| ()) } + + fn initialize_session_cartridge( + &mut self, + task_id: TaskId, + cartridge: &Cartridge, + ) -> Result<(), CrashReport> { + let session = self + .os + .vm_sessions + .for_task_mut(task_id) + .expect("loaded VM task should have a VM session"); + + session.app_id = cartridge.app_id; + session.title = cartridge.title.clone(); + session.app_version = cartridge.app_version.clone(); + session.app_mode = cartridge.app_mode; + session.clear_cartridge_service_state(); + session.runtime.initialize_vm(&mut self.os.log_service, &mut session.vm, cartridge) + } + + fn initialize_legacy_active_vm( + &mut self, + legacy_vm: &mut VirtualMachine, + cartridge: &Cartridge, + ) -> Result<(), CrashReport> { + self.os.clear_cartridge_service_state(); + self.os.vm_runtime.initialize_vm(&mut self.os.log_service, legacy_vm, cartridge) + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 50954d72..1047ae9a 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md b/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md index 7239a283..bdf0712e 100644 --- a/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md +++ b/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md @@ -2,7 +2,7 @@ id: PLN-0150 ticket: foreground-stack-game-pause-shell-vm-backed title: Route Cartridge Loading Through VM Sessions -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From f26731413f8b301625696df417c349b5e15da442 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:40:05 +0100 Subject: [PATCH 22/29] implements PLN-0151 --- .../firmware/firmware_step_game_running.rs | 4 +- .../firmware/firmware_step_shell_running.rs | 2 +- .../prometeu-system/src/os/facades/vm.rs | 167 ++++++++++++++++-- .../src/programs/prometeu_hub/prometeu_hub.rs | 24 +++ discussion/index.ndjson | 2 +- ...51-schedule-foreground-vm-session-ticks.md | 2 +- 6 files changed, 181 insertions(+), 20 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index a3c37323..677ea2ef 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -37,13 +37,11 @@ impl GameRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - ctx.os.vm().deliver_pending_game_lifecycle_events(); - let pause_requested = matches!( ctx.os.lifecycle().resident_game().map(|game| game.state), Some(ResidentGameState::PauseRequested { .. }) ); - let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform); + let result = ctx.os.vm().tick_session(self.task_id, ctx.signals, ctx.platform); if let Some(report) = result { let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs index 856bdd3f..d23244d5 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs @@ -76,7 +76,7 @@ impl ShellRunningStep { let outcome = match process_kind { ProcessKind::VmShell => { - ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform) + ctx.hub.update_vm_shell_profile(ctx.os, self.task_id, ctx.signals, ctx.platform) } ProcessKind::NativeShell => ctx.hub.update_native_shell_profile(ctx.os, ctx.platform), ProcessKind::VmGame => { diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 971b20cc..994c79af 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -1,5 +1,7 @@ use crate::CrashReport; use crate::os::{GameLifecycleEvent, SystemOS}; +use crate::task::TaskId; +use crate::vm_session::VmSession; use crate::{RenderWorkerConfig, RenderWorkerController}; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; @@ -46,6 +48,34 @@ impl<'a> VmFacade<'a> { ) } + pub fn tick_session( + &mut self, + task_id: TaskId, + signals: &InputSignals, + platform: &mut dyn RuntimePlatform, + ) -> Option { + self.deliver_pending_game_lifecycle_events_to_session(task_id); + + let Some(session) = self.os.vm_sessions.for_task_mut(task_id) else { + return Some(CrashReport::VmPanic { + message: format!("task {:?} has no VM session", task_id), + pc: None, + }); + }; + + session.runtime.tick( + &mut self.os.log_service, + &mut self.os.fs, + &mut session.fs_state, + &mut self.os.memcard, + &mut session.open_files, + &mut session.next_handle, + &mut session.vm, + signals, + platform, + ) + } + pub fn deliver_pending_game_lifecycle_events(&mut self) { if self.os.vm_runtime.current_cartridge_app_mode == AppMode::Game { let resident_game_task = self.os.foreground_stack.resident_game_task(); @@ -65,6 +95,32 @@ impl<'a> VmFacade<'a> { } } + fn deliver_pending_game_lifecycle_events_to_session(&mut self, task_id: TaskId) { + let Some(session) = self.os.vm_sessions.for_task(task_id) else { + return; + }; + + if session.runtime.current_cartridge_app_mode != AppMode::Game { + return; + } + + let mut retained = Vec::new(); + let mut delivered = Vec::new(); + + for event in self.os.game_lifecycle_events.drain(..) { + if event.task_id == task_id { + delivered.push(event); + } else { + retained.push(event); + } + } + + self.os.game_lifecycle_events = retained; + if let Some(session) = self.os.vm_sessions.for_task_mut(task_id) { + session.runtime.deliver_game_lifecycle_events(delivered); + } + } + pub fn reset(&mut self, vm: &mut VirtualMachine) { self.os.vm_runtime.reset(vm); } @@ -78,55 +134,106 @@ impl<'a> VmFacade<'a> { } pub fn paused(&self) -> bool { - self.os.vm_runtime.paused + self.active_session() + .map(|session| session.runtime.paused) + .unwrap_or(self.os.vm_runtime.paused) } pub fn set_paused(&mut self, paused: bool) { + if let Some(session) = self.active_session_mut() { + session.runtime.paused = paused; + return; + } + self.os.vm_runtime.paused = paused; } pub fn set_inspection_active(&mut self, active: bool) { + if let Some(session) = self.active_session_mut() { + session.runtime.inspection_active = active; + return; + } + self.os.vm_runtime.inspection_active = active; } pub fn request_debug_step(&mut self) { + if let Some(session) = self.active_session_mut() { + session.runtime.debug_step_request = true; + return; + } + self.os.vm_runtime.debug_step_request = true; } pub fn debug_step_requested(&self) -> bool { - self.os.vm_runtime.debug_step_request + self.active_session() + .map(|session| session.runtime.debug_step_request) + .unwrap_or(self.os.vm_runtime.debug_step_request) } pub fn logical_frame_active(&self) -> bool { - self.os.vm_runtime.logical_frame_active + self.active_session() + .map(|session| session.runtime.logical_frame_active) + .unwrap_or(self.os.vm_runtime.logical_frame_active) } pub fn delivered_game_lifecycle_events(&self) -> &[GameLifecycleEvent] { - self.os.vm_runtime.delivered_game_lifecycle_events() + self.active_session() + .map(|session| session.runtime.delivered_game_lifecycle_events()) + .unwrap_or_else(|| self.os.vm_runtime.delivered_game_lifecycle_events()) } pub fn take_delivered_game_lifecycle_events(&mut self) -> Vec { + if let Some(session) = self.active_session_mut() { + return session.runtime.take_delivered_game_lifecycle_events(); + } + self.os.vm_runtime.take_delivered_game_lifecycle_events() } pub fn tick_index(&self) -> u64 { - self.os.vm_runtime.tick_index + self.active_session() + .map(|session| session.runtime.tick_index) + .unwrap_or(self.os.vm_runtime.tick_index) } pub fn logical_frame_index(&self) -> u64 { - self.os.vm_runtime.logical_frame_index + self.active_session() + .map(|session| session.runtime.logical_frame_index) + .unwrap_or(self.os.vm_runtime.logical_frame_index) } pub fn last_frame_cpu_time_us(&self) -> u64 { - self.os.vm_runtime.last_frame_cpu_time_us + self.active_session() + .map(|session| session.runtime.last_frame_cpu_time_us) + .unwrap_or(self.os.vm_runtime.last_frame_cpu_time_us) } pub fn completed_logical_frames(&self) -> u64 { - self.os.vm_runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed).into() + self.active_session() + .map(|session| { + session + .runtime + .atomic_telemetry + .completed_logical_frames + .load(Ordering::Relaxed) + .into() + }) + .unwrap_or_else(|| { + self.os + .vm_runtime + .atomic_telemetry + .completed_logical_frames + .load(Ordering::Relaxed) + .into() + }) } pub fn telemetry_snapshot(&self) -> TelemetryFrame { - self.os.vm_runtime.atomic_telemetry.snapshot() + self.active_session() + .map(|session| session.runtime.atomic_telemetry.snapshot()) + .unwrap_or_else(|| self.os.vm_runtime.atomic_telemetry.snapshot()) } pub fn start_render_worker(&mut self, config: RenderWorkerConfig, backend: B, sink: S) @@ -164,22 +271,54 @@ impl<'a> VmFacade<'a> { } pub fn last_crash_report(&self) -> Option<&CrashReport> { - self.os.vm_runtime.last_crash_report.as_ref() + self.active_session() + .and_then(|session| session.runtime.last_crash_report.as_ref()) + .or(self.os.vm_runtime.last_crash_report.as_ref()) } pub fn current_app_id(&self) -> u32 { - self.os.vm_runtime.current_app_id + self.active_session() + .map(|session| session.runtime.current_app_id) + .unwrap_or(self.os.vm_runtime.current_app_id) } pub fn current_cartridge_title(&self) -> String { - self.os.vm_runtime.current_cartridge_title.clone() + self.active_session() + .map(|session| session.runtime.current_cartridge_title.clone()) + .unwrap_or_else(|| self.os.vm_runtime.current_cartridge_title.clone()) } pub fn current_cartridge_app_version(&self) -> String { - self.os.vm_runtime.current_cartridge_app_version.clone() + self.active_session() + .map(|session| session.runtime.current_cartridge_app_version.clone()) + .unwrap_or_else(|| self.os.vm_runtime.current_cartridge_app_version.clone()) } pub fn current_cartridge_app_mode(&self) -> AppMode { - self.os.vm_runtime.current_cartridge_app_mode + self.active_session() + .map(|session| session.runtime.current_cartridge_app_mode) + .unwrap_or(self.os.vm_runtime.current_cartridge_app_mode) + } + + fn active_session(&self) -> Option<&VmSession> { + self.os + .task_manager + .foreground_task() + .and_then(|task_id| self.os.vm_sessions.for_task(task_id)) + .or_else(|| { + self.os + .foreground_stack + .resident_game_task() + .and_then(|task_id| self.os.vm_sessions.for_task(task_id)) + }) + } + + fn active_session_mut(&mut self) -> Option<&mut VmSession> { + let task_id = self + .os + .task_manager + .foreground_task() + .or_else(|| self.os.foreground_stack.resident_game_task())?; + self.os.vm_sessions.for_task_mut(task_id) } } 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 9496fb58..d55546b1 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,3 +1,4 @@ +use crate::task::TaskId; use crate::{CrashReport, GameLibrary, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; @@ -194,6 +195,29 @@ impl PrometeuHub { SystemProfileUpdate { crash, action } } + pub fn update_vm_shell_profile( + &mut self, + os: &mut SystemOS, + task_id: TaskId, + signals: &InputSignals, + platform: &mut dyn RuntimePlatform, + ) -> SystemProfileUpdate { + let mut crash = None; + let mut action = self.gui_update(os, platform); + + if os.windows().focused_window().is_some() { + if platform.input().pad().start().down { + action = Some(SystemProfileAction::CloseShell); + } else if action != Some(SystemProfileAction::CloseShell) { + crash = os.vm().tick_session(task_id, signals, platform); + } + } + + self.render(os, platform); + + SystemProfileUpdate { crash, action } + } + pub fn update_native_shell_profile( &mut self, os: &mut SystemOS, diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1047ae9a..323d279f 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md b/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md index b07684c9..873f2796 100644 --- a/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md +++ b/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md @@ -2,7 +2,7 @@ id: PLN-0151 ticket: foreground-stack-game-pause-shell-vm-backed title: Schedule Foreground VM Session Ticks -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From b8fc5a0534900e69a00296f031fa8baed14accf7 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:41:35 +0100 Subject: [PATCH 23/29] implements PLN-0152 --- .../src/firmware/firmware.rs | 79 ++++++++++++++++++- discussion/index.ndjson | 2 +- ...e-game-and-vm-shell-session-coexistence.md | 2 +- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index ddc5548a..2218e969 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -391,9 +391,13 @@ mod tests { } fn valid_cartridge(app_mode: AppMode) -> Cartridge { + valid_cartridge_with(app_mode, 9, "Valid Cart") + } + + fn valid_cartridge_with(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge { Cartridge { - app_id: 9, - title: "Valid Cart".into(), + app_id, + title: title.into(), app_version: "1.0.0".into(), app_mode, capabilities: caps::NONE, @@ -404,6 +408,16 @@ mod tests { } } + fn vm_session_tick_index(firmware: &mut Firmware, task_id: TaskId) -> u64 { + firmware + .os + .sessions() + .vm_session_for_task(task_id) + .expect("VM session should exist") + .runtime + .tick_index + } + fn load_shell_running_firmware() -> (Firmware, TestPlatform, InputSignals, TaskId) { let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); @@ -1039,6 +1053,67 @@ mod tests { ); } + #[test] + fn resident_game_and_vm_shell_sessions_tick_independently() { + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge_with(AppMode::Game, 9, "Resident Game")); + firmware.tick(&signals, &mut platform); + + let game_task = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected GameRunning state, got {:?}", other), + }; + + firmware.tick(&signals, &mut platform); + let game_tick_before_home = vm_session_tick_index(&mut firmware, game_task); + + firmware.request_home_from_host(); + firmware.tick(&signals, &mut platform); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + let game_tick_after_suspend = vm_session_tick_index(&mut firmware, game_task); + assert_eq!(game_tick_after_suspend, game_tick_before_home + 1); + + firmware.load_cartridge(valid_cartridge_with(AppMode::Shell, 77, "VM Shell")); + firmware.tick(&signals, &mut platform); + + let shell_task = match &firmware.state { + FirmwareState::ShellRunning(step) => step.task_id, + other => panic!("expected ShellRunning state, got {:?}", other), + }; + + assert_ne!(shell_task, game_task); + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); + assert_eq!(firmware.os.lifecycle().task_state(game_task), Some(TaskState::Suspended)); + + let shell_tick_before = vm_session_tick_index(&mut firmware, shell_task); + for _ in 0..3 { + firmware.tick(&signals, &mut platform); + } + + assert_eq!(vm_session_tick_index(&mut firmware, game_task), game_tick_after_suspend); + assert_eq!(vm_session_tick_index(&mut firmware, shell_task), shell_tick_before + 3); + + let close_shell = InputSignals { start_signal: true, ..Default::default() }; + firmware.tick(&close_shell, &mut platform); + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(shell_task), Some(TaskState::Closed)); + assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(game_task)); + + assert!(firmware.resume_resident_game_from_home()); + firmware.tick(&signals, &mut platform); + + match &firmware.state { + FirmwareState::GameRunning(step) => assert_eq!(step.task_id, game_task), + other => panic!("expected GameRunning state, got {:?}", other), + } + assert!(vm_session_tick_index(&mut firmware, game_task) > game_tick_after_suspend); + } + #[test] fn direct_game_boot_does_not_depend_on_userland_run_cart_syscall() { assert!(Syscall::from_u32(0x0002).is_none()); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 323d279f..fb7c23a1 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md b/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md index 64cef273..8ae74cbc 100644 --- a/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md +++ b/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md @@ -2,7 +2,7 @@ id: PLN-0152 ticket: foreground-stack-game-pause-shell-vm-backed title: Validate Game and VM Shell Session Coexistence -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From e01b463e1f89f6a01f07107ca6a6f62147bc77bc Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 4 Jul 2026 19:42:59 +0100 Subject: [PATCH 24/29] implements PLN-0153 --- discussion/index.ndjson | 2 +- ...nership-and-background-ready-scheduling.md | 2 +- .../runtime/09-events-and-concurrency.md | 38 +++++++++++++++++-- .../12-firmware-pos-and-prometeuhub.md | 28 ++++++++++++-- docs/specs/runtime/14-boot-profiles.md | 5 +++ .../specs/runtime/16-host-abi-and-syscalls.md | 5 +++ 6 files changed, 71 insertions(+), 9 deletions(-) diff --git a/discussion/index.ndjson b/discussion/index.ndjson index fb7c23a1..751f9fc4 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"open","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md b/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md index 2d0f0e81..a5e6cd85 100644 --- a/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md +++ b/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md @@ -2,7 +2,7 @@ id: PLN-0153 ticket: foreground-stack-game-pause-shell-vm-backed title: Specify VM Session Ownership and Background-Ready Scheduling -status: open +status: done created: 2026-07-04 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] diff --git a/docs/specs/runtime/09-events-and-concurrency.md b/docs/specs/runtime/09-events-and-concurrency.md index 0e9a1606..4ec0e44a 100644 --- a/docs/specs/runtime/09-events-and-concurrency.md +++ b/docs/specs/runtime/09-events-and-concurrency.md @@ -133,7 +133,37 @@ 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 contract. -## 8 Render Worker Concurrency +## 8 VM Sessions and Execution Eligibility + +Every VM-backed process MUST own a distinct VM session. A VM session owns the +mutable execution context for that process, including the VM instance, +per-session runtime counters, lifecycle delivery bookkeeping, debug/pause state, +and transient handles opened through VM-facing services. + +Foreground ownership and execution eligibility are related but separate +concepts: + +- foreground ownership selects the visual/navigation owner; +- execution eligibility selects which sessions may receive VM ticks; +- v1 grants normal VM execution only to the foreground VM-backed task, plus the + bounded Game pause handoff tick defined above; +- a suspended resident Game preserves its VM session but MUST NOT receive + normal gameplay ticks, normal Game input, or frame pacing; +- a VM-backed Shell owns its own VM session and MUST NOT overwrite or advance + the resident Game session while the Shell is foreground; +- a native Shell does not own a VM session. + +The model is intentionally compatible with future background execution. Future +service or media processes may become execution-eligible without changing the +rule that mutable VM context belongs to the owning session. This chapter does +not grant any v1 background progress guarantee. + +Durable app data remains keyed by `app_id` where the relevant storage contract +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. + +## 9 Render Worker Concurrency The render worker is not a machine-visible event source and does not introduce guest callbacks. It is an implementation-side consumer of closed render @@ -160,7 +190,7 @@ Shutdown is explicit and bounded. A shutdown request wakes a waiting worker, causes pending work that will not be consumed to be discarded, and reports a typed failure if the worker cannot join within the configured timeout. -## 9 Async Asset and IO Work Lane +## 10 Async Asset and IO Work Lane The asset/IO async work lane is not a machine-visible event source and does not introduce guest callbacks. It is an implementation-side lane for asset @@ -184,7 +214,7 @@ does not publish resident graphics/audio/scene state directly. FS and game persistence services may consume this lane for IO-style work, but public FS API shape is defined by the FS/app-home contract, not by this chapter. -## 10 Determinism and Best Practices +## 11 Determinism and Best Practices PROMETEU encourages: @@ -199,7 +229,7 @@ PROMETEU discourages: - hidden timing channels; - ambiguous out-of-band execution. -## 11 Relationship to Other Specs +## 12 Relationship to Other Specs - [`09a-coroutines-and-cooperative-scheduling.md`](09a-coroutines-and-cooperative-scheduling.md) defines coroutine lifecycle and scheduling behavior. - [`10-debug-inspection-and-profiling.md`](10-debug-inspection-and-profiling.md) defines observability and diagnostics surfaces. diff --git a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md index d4c5782a..b75246c9 100644 --- a/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md +++ b/docs/specs/runtime/12-firmware-pos-and-prometeuhub.md @@ -50,13 +50,16 @@ The VM does not own the machine lifecycle. Firmware does. - **Suspended**: an OS-owned scheduler/runtime state. A suspended Game remains resident but does not receive normal gameplay ticks, normal Game input, or 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. ## 3 POS Responsibilities POS is responsible for: - deterministic reset into known machine state; -- VM initialization and teardown for cartridge execution; +- VM session creation, initialization, activation, and teardown for cartridge + execution; - input latching and logical-frame budgeting; - peripheral reset/orchestration; - fault capture and crash-flow transition; @@ -68,6 +71,10 @@ At the VM boundary, POS must preserve: - `FRAME_SYNC` as the canonical frame boundary; - deterministic transition from host tick to VM slice execution. +Firmware must not treat a single global VM object as the canonical execution +owner for all cartridges. The firmware state machine orchestrates macro-state; +POS/session services own VM-backed process context. + ## 4 PrometeuHub Responsibilities PrometeuHub is responsible for: @@ -204,19 +211,31 @@ boundaries. Game audio pauses with the Game VM in v1. Independent background audio, real background execution, multiple resident Games, multiple foreground Shells, and direct Game-to-Game switching are outside this contract. +The resident Game VM session remains allocated while Hub or Shell is foreground. +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. + ## 8 Cartridge Load Flow Current high-level flow: 1. POS receives a cartridge to load; 2. asset manager is initialized from the cartridge asset table, preload list, and packed asset bytes; -3. POS initializes the VM/runtime for the cartridge; -4. launch behavior branches by `AppMode`: +3. POS creates or selects the VM session for the cartridge; +4. POS initializes the session-owned VM/runtime for the cartridge; +5. launch behavior branches by `AppMode`: - `Game` -> transition to the game-running firmware state; - `System` -> create/focus a Hub window and return to the Hub state. 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 +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`. When Home launches a discovered Game entry, the request is a system-owned action @@ -239,6 +258,7 @@ The current firmware state model includes: - `HubHome` - `LoadCartridge` - `GameRunning` +- `ShellRunning` - `AppCrashes` These states express machine orchestration above the VM. They are not guest-visible bytecode states. @@ -286,6 +306,8 @@ Expected behavior: - Home remains foreground while the Game is resident; - launching and closing a Shell app returns to Home without losing the resident Game; +- a VM-backed Shell advances its own VM session and does not tick the suspended + 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. diff --git a/docs/specs/runtime/14-boot-profiles.md b/docs/specs/runtime/14-boot-profiles.md index 285763c9..a420804c 100644 --- a/docs/specs/runtime/14-boot-profiles.md +++ b/docs/specs/runtime/14-boot-profiles.md @@ -50,6 +50,8 @@ For a cartridge boot target: - `Game` cartridges transition into the game-running pipeline; - `System` cartridges transition into a Runtime/Hub pipeline dedicated to system UI and app hosting. +- VM-backed cartridges are initialized in a POS-owned VM session associated with + the created task/process. This preserves the distinction between machine firmware state and app execution mode. @@ -104,6 +106,9 @@ Game is already running. It does not change direct `--run`, debugger direct boot, or `--games-root` Home library startup semantics. Home/SystemOS requests 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. + ## 5 Firmware State Relationship Boot target selection feeds into firmware states such as: diff --git a/docs/specs/runtime/16-host-abi-and-syscalls.md b/docs/specs/runtime/16-host-abi-and-syscalls.md index c759a232..88a4749a 100644 --- a/docs/specs/runtime/16-host-abi-and-syscalls.md +++ b/docs/specs/runtime/16-host-abi-and-syscalls.md @@ -187,6 +187,11 @@ The game memcard profile uses module `mem` with status-first return shapes. The `mem` module is a game profile surface. It is not the public storage model for `System` profile apps. +Durable memcard data is keyed by `app_id` according to the save-memory +contract. Runtime handles, open file tables, pending lifecycle delivery, and +other mutable VM-facing syscall context are scoped to the owning VM session and +MUST NOT be used as durable app identity. + Canonical operations in v1 are: - `mem.slot_count() -> (status, count)` From 8098dbd7f4fac53721fd7648d3d781603c102ea2 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 00:16:50 +0100 Subject: [PATCH 25/29] VM Context Ownership for Resident Game and VM-Backed Shell --- discussion/index.ndjson | 4 +- ...debugger-operations-through-vm-sessions.md | 95 ++++++++++++++++++ ...etire-legacy-firmware-vm-runtime-bridge.md | 89 +++++++++++++++++ ...n-vm-session-lifecycle-error-boundaries.md | 96 +++++++++++++++++++ 4 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md create mode 100644 discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md create mode 100644 discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 751f9fc4..230d8da5 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":154,"LSN":52,"CLSN":1}} +{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":157,"LSN":52,"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":"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":"in_progress","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-04","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md b/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md new file mode 100644 index 00000000..3e125b3f --- /dev/null +++ b/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md @@ -0,0 +1,95 @@ +--- +id: PLN-0154 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Route Debugger Operations Through VM Sessions +status: open +created: 2026-07-05 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 requires VM-backed mutable execution state to be owned by VM sessions. Normal execution now ticks session-owned VMs, but desktop debugger commands still inspect and mutate `firmware.vm`, the transitional legacy VM. This leaves debugger state, breakpoints, single-step execution, PC snapshots, and stack snapshots inconsistent with the active VM session. + +## Objective + +Route host debugger operations through the active VM session rather than the legacy firmware VM. Debugger pause/resume/step/state/breakpoint operations must target the same VM context that foreground execution ticks. + +## Dependencies + +- Source decision: DEC-0038. +- Builds on PLN-0148 through PLN-0152. +- Should be implemented before PLN-0155 removes the legacy firmware VM bridge. + +## Scope + +- Add session-aware debugger accessors to `SystemOS`/`VmFacade`. +- Expose active VM session inspection and mutation operations needed by the debugger: + - PC; + - operand stack preview; + - insert breakpoint; + - remove breakpoint; + - list breakpoints; + - single-instruction debug step; + - active cartridge identity for handshake and state responses. +- Update `crates/host/prometeu-host-desktop-winit/src/debugger.rs` so it no longer reads or mutates `firmware.vm` for active VM state. +- Ensure debugger setup for direct debug boot initializes through normal cartridge/session loading or an explicit session-aware metadata path, not `os.vm().initialize(&mut firmware.vm, ...)`. +- Keep pause/resume/step-frame control mapped to active VM session runtime state. +- Add tests that would fail if debugger state came from the legacy VM while the session VM is active. + +## Non-Goals + +- Do not remove `Firmware.vm`; that is PLN-0155. +- Do not redesign the debugger protocol. +- Do not add multi-client debugging. +- Do not add background-session debugging beyond active/resident session semantics already supported by DEC-0038. + +## Execution Method + +1. Add focused `VmFacade` methods for active-session VM inspection: + - `active_pc`; + - `active_operand_stack_top`; + - `insert_active_breakpoint`; + - `remove_active_breakpoint`; + - `active_breakpoints_list`; + - `debug_step_active_session`. +2. Define active-session resolution explicitly: + - foreground VM session first; + - resident Game session when Hub has foreground but a resident Game is the active debug target; + - no legacy fallback for debugger mutation once a session exists. +3. Update `HostDebugger::setup_boot_target` so debug cartridge metadata is loaded without initializing `firmware.vm` as canonical state. +4. Update `HostDebugger::handle_command` for `Step`, `GetState`, `SetBreakpoint`, `ListBreakpoints`, and `ClearBreakpoint`. +5. Update `HostDebugger::stream_events` breakpoint-hit PC reporting to read from the active session. +6. Add tests under `crates/host/prometeu-host-desktop-winit/src/debugger.rs` proving debugger commands observe and mutate the session VM. +7. Run the host, firmware, and system test suites. + +## Acceptance Criteria + +- Debugger handshake reports cartridge identity from session/runtime state or preloaded metadata, not from a global VM initialized only for debugger setup. +- `GetState` returns PC and stack data from the active VM session. +- `Step` advances the active VM session, not `firmware.vm`. +- Breakpoint commands mutate the active VM session. +- Breakpoint-hit events report the active session PC. +- No debugger command path directly reads or mutates `firmware.vm` for active VM execution state. +- Existing debugger protocol tests pass. + +## Tests + +- `cargo test -p prometeu-host-desktop-winit` +- `cargo test -p prometeu-firmware` +- `cargo test -p prometeu-system` +- `cargo clippy -p prometeu-host-desktop-winit -p prometeu-firmware -p prometeu-system --all-targets -- -D warnings` +- Add tests for: + - `GetState` reads session VM PC; + - `Step` affects session VM; + - breakpoint insert/list/remove uses session VM; + - debugger setup does not initialize `firmware.vm` as the canonical app context. + +## Affected Artifacts + +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` +- `crates/console/prometeu-system/src/os/facades/vm.rs` +- `crates/console/prometeu-system/src/services/vm_session.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- Debugger-related tests in host and firmware crates diff --git a/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md b/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md new file mode 100644 index 00000000..28757dc1 --- /dev/null +++ b/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md @@ -0,0 +1,89 @@ +--- +id: PLN-0155 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Retire Legacy Firmware VM Runtime Bridge +status: open +created: 2026-07-05 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 states that `Firmware.vm` must not remain the canonical owner of VM-backed app context. PLN-0148 through PLN-0152 moved normal cartridge loading and ticking toward VM sessions, but a transitional legacy bridge still initializes and exposes the old global VM/runtime path for compatibility. + +## Objective + +Remove the legacy firmware VM/runtime bridge from normal runtime architecture. After this plan, VM-backed app execution, initialization, active runtime queries, and host-facing inspection must resolve through VM sessions or explicitly global render/runtime services. + +## Dependencies + +- Source decision: DEC-0038. +- Requires PLN-0154 so debugger operations no longer depend on `firmware.vm`. +- Builds on PLN-0148 through PLN-0153. + +## Scope + +- Remove `Firmware.vm` from `crates/console/prometeu-firmware/src/firmware/firmware.rs` if all callsites have migrated. +- Remove `PrometeuContext.vm` from `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs`. +- Remove `legacy_vm` from `SessionsFacade::load_vm_cartridge`. +- Remove `initialize_legacy_active_vm`. +- Remove or narrow global `VmFacade::initialize`, `tick`, `reset`, and `debug_step_instruction` methods that operate on caller-supplied `VirtualMachine`. +- Keep global render ownership, render worker lifecycle, telemetry publication, and log service where they are global SystemOS responsibilities. +- Preserve Hub/Home rendering behavior while avoiding any guest VM state stored in firmware. +- Update direct boot, debug boot, reset, and load-cartridge tests to prove session ownership is the only app VM path. + +## Non-Goals + +- Do not remove `VirtualMachineRuntime` itself. +- Do not redesign render worker ownership. +- Do not add background execution. +- Do not change debugger protocol shape beyond the PLN-0154 migration. + +## Execution Method + +1. Use `rg` to list every remaining `firmware.vm`, `ctx.vm`, `legacy_vm`, and `os.vm().initialize/tick/reset/debug_step_instruction` callsite. +2. Replace firmware context construction so state steps receive `SystemOS`, Hub, signals, platform, and boot target only. +3. Replace reset behavior with session registry/runtime reset semantics: + - clear VM sessions on firmware reset when booting fresh; + - preserve only explicitly resident sessions during lifecycle flows, not reset. +4. Update cartridge loading to initialize only the selected VM session. +5. Split `VmFacade` into: + - session-targeted VM execution/inspection methods; + - global render/runtime authority methods. +6. Delete compatibility methods that accept `&mut VirtualMachine` when no remaining production callsite exists. +7. Add tests or assertions proving no production code path initializes or ticks a global firmware VM. +8. Run full affected test suites and clippy. + +## Acceptance Criteria + +- `Firmware` no longer owns `pub vm: VirtualMachine`. +- `PrometeuContext` no longer carries a VM reference. +- `LoadCartridgeStep` initializes only session-owned VM context. +- `GameRunningStep` and VM-backed `ShellRunningStep` tick only sessions. +- Debugger, stats, and host runner access active session state or global render services explicitly. +- `rg "firmware\\.vm|ctx\\.vm|legacy_vm|os\\.vm\\(\\)\\.initialize\\(|os\\.vm\\(\\)\\.tick\\("` finds no production runtime dependency on the legacy global VM path. +- Tests continue to prove Game resident + VM Shell coexistence. + +## 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` +- Add or update tests for: + - reset clears session-owned VM contexts appropriately; + - direct boot creates session-owned VM context; + - debug boot uses session-owned VM context; + - no global VM tick path remains. + +## Affected Artifacts + +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_*.rs` +- `crates/console/prometeu-system/src/os/facades/sessions.rs` +- `crates/console/prometeu-system/src/os/facades/vm.rs` +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `crates/host/prometeu-host-desktop-winit/src/stats.rs` diff --git a/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md b/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md new file mode 100644 index 00000000..5ba1ca28 --- /dev/null +++ b/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md @@ -0,0 +1,96 @@ +--- +id: PLN-0156 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Harden VM Session Lifecycle Error Boundaries +status: open +created: 2026-07-05 +ref_decisions: [DEC-0038] +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +## Briefing + +DEC-0038 makes VM session services part of the runtime ownership boundary. Some current session creation paths still use `expect` for lifecycle failures and missing task/process/session invariants. That is acceptable for short-term scaffolding, but session services should expose typed errors at operational boundaries so callers can choose controlled crash, rejection, or recovery behavior. + +## Objective + +Replace operational `expect`/panic paths in VM session creation/loading with typed errors and controlled firmware outcomes. Keep internal invariant assertions only where impossible states have already been proven by prior checks. + +## Dependencies + +- Source decision: DEC-0038. +- Builds on PLN-0148 through PLN-0152. +- Can be implemented before or after PLN-0154. +- Should be implemented before broadening VM session scheduling or adding background-capable processes. + +## Scope + +- Add or extend typed errors for session/lifecycle operations: + - lifecycle rejection while creating Game/Shell task; + - missing task for session creation; + - missing process for session creation; + - duplicate session; + - VM-backed session missing during cartridge initialization; + - foreground Shell already exists. +- Convert `SessionsFacade::create_vm_shell_task` to a fallible equivalent and keep an infallible wrapper only for test/setup code if needed. +- Convert `create_vm_session_for_task` to avoid `expect` for task/process lookup. +- Convert `initialize_session_cartridge` to return typed missing-session errors instead of panicking. +- Map session/lifecycle errors into controlled `CrashReport` or launch rejection behavior in firmware. +- Preserve existing V1 policy: + - one resident Game; + - one foreground Shell; + - no Game-to-Game switching; + - native Shell creates no VM session. + +## Non-Goals + +- Do not change DEC-0038 ownership semantics. +- Do not add background execution. +- Do not redesign the foreground stack. +- Do not hide programmer invariants behind overly broad error swallowing. + +## Execution Method + +1. Introduce a `SessionError` or extend `VmSessionError` with operational cases that cross facade boundaries. +2. Update `SessionsFacade` methods: + - `try_create_vm_game_task`; + - new `try_create_vm_shell_task`; + - `load_vm_cartridge`; + - `create_vm_session_for_task`; + - `initialize_session_cartridge`. +3. Update firmware loading paths to map typed session errors into: + - controlled `CrashReport::VmPanic` for unrecoverable internal failures; + - logged rejection and no state transition for unsupported lifecycle transitions where appropriate. +4. Update tests that currently rely on panics or infallible creation. +5. Add focused tests for: + - duplicate foreground Shell rejection during VM Shell load; + - missing task/process session creation errors; + - missing VM session during initialization; + - native Shell still creates no VM session. +6. Run system, firmware, host tests and clippy. + +## Acceptance Criteria + +- VM session facade operational failures are represented by typed errors. +- `create_vm_shell_task` has a fallible path used by production cartridge loading. +- VM Shell load does not panic when lifecycle rejects the Shell; it returns a controlled error/outcome. +- Missing task/process/session conditions in session services return typed errors. +- Existing tests for resident Game, native Shell, VM Shell, and Game/Shell coexistence still pass. +- No new broad `unwrap`/`expect` appears in production session/lifecycle paths. + +## 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` +- Add tests for controlled errors from session creation/loading edge cases. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/facades/sessions.rs` +- `crates/console/prometeu-system/src/services/vm_session.rs` +- `crates/console/prometeu-system/src/os/lifecycle.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` +- Related system and firmware tests From 4e05daf2807f1125229588f15af1cfaac1a0af17 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 00:21:10 +0100 Subject: [PATCH 26/29] implements PLN-0154 --- .../prometeu-hal/src/debugger_protocol.rs | 2 +- .../prometeu-system/src/os/facades/vm.rs | 64 +++++++++++- .../prometeu-system/src/os/system_os.rs | 37 +++++++ .../src/debugger.rs | 99 ++++++++++++++++--- discussion/index.ndjson | 2 +- ...debugger-operations-through-vm-sessions.md | 2 +- 6 files changed, 182 insertions(+), 24 deletions(-) diff --git a/crates/console/prometeu-hal/src/debugger_protocol.rs b/crates/console/prometeu-hal/src/debugger_protocol.rs index e6428be7..d718a149 100644 --- a/crates/console/prometeu-hal/src/debugger_protocol.rs +++ b/crates/console/prometeu-hal/src/debugger_protocol.rs @@ -40,7 +40,7 @@ pub enum DebugResponse { Breakpoints { pcs: Vec }, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HandshakeCartridge { pub app_id: u32, pub title: String, diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 994c79af..87ba5cb8 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -3,6 +3,7 @@ use crate::os::{GameLifecycleEvent, SystemOS}; use crate::task::TaskId; use crate::vm_session::VmSession; use crate::{RenderWorkerConfig, RenderWorkerController}; +use prometeu_bytecode::Value; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; @@ -133,6 +134,56 @@ impl<'a> VmFacade<'a> { self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, platform) } + pub fn debug_step_active_session( + &mut self, + platform: &mut dyn RuntimePlatform, + ) -> Option { + let Some(task_id) = self.active_session_task_id() else { + return Some(CrashReport::VmPanic { + message: "debug step requested without an active VM session".to_string(), + pc: None, + }); + }; + let Some(session) = self.os.vm_sessions.for_task_mut(task_id) else { + return Some(CrashReport::VmPanic { + message: format!("active debug task {:?} has no VM session", task_id), + pc: None, + }); + }; + + session.runtime.debug_step_instruction(&mut self.os.log_service, &mut session.vm, platform) + } + + pub fn active_pc(&self) -> Option { + self.active_session().map(|session| session.vm.pc()) + } + + pub fn active_operand_stack_top(&self, n: usize) -> Vec { + self.active_session().map(|session| session.vm.operand_stack_top(n)).unwrap_or_default() + } + + pub fn insert_active_breakpoint(&mut self, pc: usize) -> bool { + if let Some(session) = self.active_session_mut() { + session.vm.insert_breakpoint(pc); + return true; + } + + false + } + + pub fn remove_active_breakpoint(&mut self, pc: usize) -> bool { + if let Some(session) = self.active_session_mut() { + session.vm.remove_breakpoint(pc); + return true; + } + + false + } + + pub fn active_breakpoints_list(&self) -> Vec { + self.active_session().map(|session| session.vm.breakpoints_list()).unwrap_or_default() + } + pub fn paused(&self) -> bool { self.active_session() .map(|session| session.runtime.paused) @@ -314,11 +365,14 @@ impl<'a> VmFacade<'a> { } fn active_session_mut(&mut self) -> Option<&mut VmSession> { - let task_id = self - .os - .task_manager - .foreground_task() - .or_else(|| self.os.foreground_stack.resident_game_task())?; + let task_id = self.active_session_task_id()?; self.os.vm_sessions.for_task_mut(task_id) } + + fn active_session_task_id(&self) -> Option { + self.os + .task_manager + .foreground_task() + .or_else(|| self.os.foreground_stack.resident_game_task()) + } } diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 09bb980c..35fb4ca4 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -295,6 +295,43 @@ mod tests { assert_eq!(shell_session.open_files.get(&1).map(String::as_str), Some("/shell.dat")); } + #[test] + fn vm_debug_operations_target_foreground_shell_before_resident_game() { + 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"); + + { + let mut sessions = os.sessions(); + let game_session = + sessions.vm_session_for_task_mut(game).expect("game VM session should exist"); + game_session.vm.insert_breakpoint(10); + } + + assert!(os.vm().insert_active_breakpoint(20)); + assert_eq!(os.vm().active_breakpoints_list(), vec![20]); + + let sessions = os.sessions(); + assert_eq!( + sessions + .vm_session_for_task(game) + .expect("game VM session should exist") + .vm + .breakpoints_list(), + vec![10] + ); + assert_eq!( + sessions + .vm_session_for_task(shell) + .expect("shell VM session should exist") + .vm + .breakpoints_list(), + vec![20] + ); + } + #[test] fn home_request_notifies_game_before_budget_suspends_it() { let mut os = SystemOS::new(None); diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index 9bb1ea92..c14df166 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -28,6 +28,8 @@ pub struct HostDebugger { last_telemetry_frame: u64, /// Last fault summary sent to the debugger client. last_fault_summary: Option, + /// Debug cartridge metadata captured before the VM session exists. + debug_cartridge: Option, } impl Default for HostDebugger { @@ -46,19 +48,25 @@ impl HostDebugger { last_log_seq: 0, last_telemetry_frame: 0, last_fault_summary: None, + debug_cartridge: None, } } /// Configures the debugger based on the boot target. /// If debug mode is enabled, it binds to the specified TCP port. - pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) { + pub fn setup_boot_target(&mut self, boot_target: &BootTarget, _firmware: &mut Firmware) { if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target { self.waiting_for_start = true; // Pre-load cartridge metadata so the Handshake message can contain - // valid information about the App being debugged. + // valid information before the VM session is created. if let Ok(cartridge) = CartridgeLoader::load(path) { - let _ = firmware.os.vm().initialize(&mut firmware.vm, &cartridge); + self.debug_cartridge = Some(HandshakeCartridge { + app_id: cartridge.app_id, + title: cartridge.title, + app_version: cartridge.app_version, + app_mode: cartridge.app_mode, + }); } match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { @@ -116,12 +124,7 @@ impl HostDebugger { let handshake = DebugResponse::Handshake { protocol_version: DEVTOOLS_PROTOCOL_VERSION, runtime_version: "0.1".to_string(), - cartridge: HandshakeCartridge { - app_id: firmware.os.vm().current_app_id(), - title: firmware.os.vm().current_cartridge_title(), - app_version: firmware.os.vm().current_cartridge_app_version(), - app_mode: firmware.os.vm().current_cartridge_app_mode(), - }, + cartridge: self.handshake_cartridge(firmware), }; self.send_response(handshake); } else { @@ -201,7 +204,7 @@ impl HostDebugger { DebugCommand::Step => { // Execute exactly one instruction and keep paused. firmware.os.vm().set_paused(true); - let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, platform); + let _ = firmware.os.vm().debug_step_active_session(platform); } DebugCommand::StepFrame => { // Execute until the end of the current logical frame. @@ -210,10 +213,10 @@ impl HostDebugger { } DebugCommand::GetState => { // Return detailed VM register and stack state. - let stack_top = firmware.vm.operand_stack_top(10); + let stack_top = firmware.os.vm().active_operand_stack_top(10); let resp = DebugResponse::GetState { - pc: firmware.vm.pc(), + pc: firmware.os.vm().active_pc().unwrap_or(0), stack_top, frame_index: firmware.os.vm().logical_frame_index(), app_id: firmware.os.vm().current_app_id(), @@ -221,18 +224,37 @@ impl HostDebugger { self.send_response(resp); } DebugCommand::SetBreakpoint { pc } => { - firmware.vm.insert_breakpoint(pc); + let _ = firmware.os.vm().insert_active_breakpoint(pc); } DebugCommand::ListBreakpoints => { - let pcs = firmware.vm.breakpoints_list(); + let pcs = firmware.os.vm().active_breakpoints_list(); self.send_response(DebugResponse::Breakpoints { pcs }); } DebugCommand::ClearBreakpoint { pc } => { - firmware.vm.remove_breakpoint(pc); + let _ = firmware.os.vm().remove_active_breakpoint(pc); } } } + fn handshake_cartridge(&self, firmware: &mut Firmware) -> HandshakeCartridge { + let app_id = firmware.os.vm().current_app_id(); + if app_id != 0 { + return HandshakeCartridge { + app_id, + title: firmware.os.vm().current_cartridge_title(), + app_version: firmware.os.vm().current_cartridge_app_version(), + app_mode: firmware.os.vm().current_cartridge_app_mode(), + }; + } + + self.debug_cartridge.clone().unwrap_or_else(|| HandshakeCartridge { + app_id, + title: firmware.os.vm().current_cartridge_title(), + app_version: firmware.os.vm().current_cartridge_app_version(), + app_mode: firmware.os.vm().current_cartridge_app_mode(), + }) + } + pub(crate) fn cert_event_from_snapshot( tag: u16, telemetry: TelemetryFrame, @@ -315,7 +337,7 @@ impl HostDebugger { // Map specific internal log tags to protocol events. if event.tag == 0xDEB1 { self.send_event(DebugEvent::BreakpointHit { - pc: firmware.vm.pc(), + pc: firmware.os.vm().active_pc().unwrap_or(0), frame_index: firmware.os.vm().logical_frame_index(), }); } @@ -409,6 +431,51 @@ mod tests { assert!(firmware.os.vm().debug_step_requested()); } + #[test] + fn breakpoint_commands_mutate_active_vm_session_not_legacy_vm() { + let mut debugger = HostDebugger::new(); + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + let task_id = firmware.os.sessions().create_vm_game_task(42, "Sector Crawl"); + firmware.vm.insert_breakpoint(9); + + debugger.handle_command( + DebugCommand::SetBreakpoint { pc: 123 }, + &mut firmware, + &mut platform, + ); + + assert_eq!(firmware.vm.breakpoints_list(), vec![9]); + assert_eq!( + firmware + .os + .sessions() + .vm_session_for_task(task_id) + .expect("game VM session should exist") + .vm + .breakpoints_list(), + vec![123] + ); + + debugger.handle_command( + DebugCommand::ClearBreakpoint { pc: 123 }, + &mut firmware, + &mut platform, + ); + + assert_eq!(firmware.vm.breakpoints_list(), vec![9]); + assert!( + firmware + .os + .sessions() + .vm_session_for_task(task_id) + .expect("game VM session should exist") + .vm + .breakpoints_list() + .is_empty() + ); + } + #[test] fn handle_command_start_leaves_waiting_for_start_mode() { let mut debugger = HostDebugger::new(); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 230d8da5..d1685785 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md b/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md index 3e125b3f..45bfaac8 100644 --- a/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md +++ b/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md @@ -2,7 +2,7 @@ id: PLN-0154 ticket: foreground-stack-game-pause-shell-vm-backed title: Route Debugger Operations Through VM Sessions -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From e613ab43c9cdfd8a3030cb08725dfa1f9a9c347f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 00:26:16 +0100 Subject: [PATCH 27/29] implements PLN-0155 --- .../src/firmware/firmware.rs | 25 ++++++-- .../src/firmware/firmware_step_hub_home.rs | 2 +- .../firmware/firmware_step_load_cartridge.rs | 2 +- .../src/firmware/firmware_step_reset.rs | 3 +- .../src/firmware/prometeu_context.rs | 2 - .../src/os/facades/sessions.rs | 16 ++--- .../prometeu-system/src/os/facades/vm.rs | 64 +------------------ .../src/programs/prometeu_hub/prometeu_hub.rs | 14 +--- .../src/services/vm_session.rs | 4 ++ .../src/debugger.rs | 5 +- discussion/index.ndjson | 2 +- ...etire-legacy-firmware-vm-runtime-bridge.md | 2 +- 12 files changed, 39 insertions(+), 102 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 2218e969..153fb92f 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -9,7 +9,6 @@ use prometeu_hal::log::LogSource; use prometeu_hal::telemetry::CertificationConfig; use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_system::{PrometeuHub, ResidentGameState, SystemOS}; -use prometeu_vm::VirtualMachine; /// PROMETEU Firmware. /// @@ -27,8 +26,6 @@ use prometeu_vm::VirtualMachine; /// 2. Delegates the logic update to the current active state. /// 3. Handles state transitions (e.g., from Loading to Playing). pub struct Firmware { - /// The execution engine (PVM) for user applications. - pub vm: VirtualMachine, /// The underlying OS services (Syscalls, Filesystem, Telemetry). pub os: SystemOS, /// The internal state of the system launcher (Hub). @@ -46,7 +43,6 @@ impl Firmware { /// Initializes the firmware in the `Reset` state. pub fn new(cap_config: Option) -> Self { Self { - vm: VirtualMachine::default(), os: SystemOS::new(cap_config), hub: PrometeuHub::new(), state: FirmwareState::Reset(ResetStep), @@ -122,7 +118,6 @@ impl Firmware { /// Dispatches the `on_enter` event to the current state implementation. fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { let mut req = PrometeuContext { - vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, @@ -150,7 +145,6 @@ impl Firmware { platform: &mut dyn RuntimePlatform, ) -> Option { let mut req = PrometeuContext { - vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, @@ -173,7 +167,6 @@ impl Firmware { /// Dispatches the `on_exit` event to the current state implementation. fn on_exit(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) { let mut req = PrometeuContext { - vm: &mut self.vm, os: &mut self.os, hub: &mut self.hub, boot_target: &self.boot_target, @@ -532,6 +525,24 @@ mod tests { assert!(matches!(firmware.state, FirmwareState::LaunchHub(_))); } + #[test] + fn reset_clears_vm_sessions_and_runtime_identity() { + let mut firmware = Firmware::new(None); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::Game)); + firmware.tick(&signals, &mut platform); + + assert_eq!(firmware.os.sessions().vm_session_count(), 1); + assert_eq!(firmware.os.vm().current_cartridge_title(), "Valid Cart"); + + firmware.change_state(FirmwareState::Reset(ResetStep), &signals, &mut platform); + + assert_eq!(firmware.os.sessions().vm_session_count(), 0); + assert_eq!(firmware.os.vm().current_cartridge_title(), ""); + } + #[test] fn load_cartridge_routes_system_apps_to_system_running() { let mut firmware = Firmware::new(None); 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 fb27d40a..cc5479f7 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 @@ -20,7 +20,7 @@ impl HubHomeStep { } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { - let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform); + let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.platform); if let Some(report) = outcome.crash { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } 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 9fedebad..397f9886 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 @@ -38,7 +38,7 @@ impl LoadCartridgeStep { self.cartridge.assets.clone(), ); - match ctx.os.sessions().load_vm_cartridge(ctx.vm, &self.cartridge) { + match ctx.os.sessions().load_vm_cartridge(&self.cartridge) { Ok(loaded) => { self.loaded_task_id = Some(loaded.task_id); self.loaded_app_mode = Some(loaded.app_mode); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs index 586e3026..6159e2b5 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs @@ -9,7 +9,8 @@ pub struct ResetStep; impl ResetStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string()); - ctx.os.vm().reset(ctx.vm); + ctx.os.sessions().clear_vm_sessions(); + ctx.os.vm().reset_global_runtime_state(); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { diff --git a/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs b/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs index 9c37beed..9f7dc628 100644 --- a/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs +++ b/crates/console/prometeu-firmware/src/firmware/prometeu_context.rs @@ -1,10 +1,8 @@ use crate::firmware::boot_target::BootTarget; use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_system::{PrometeuHub, SystemOS}; -use prometeu_vm::VirtualMachine; pub struct PrometeuContext<'a> { - pub vm: &'a mut VirtualMachine, pub os: &'a mut SystemOS, pub hub: &'a mut PrometeuHub, pub boot_target: &'a BootTarget, diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs index 3d41c2e4..0db29dc1 100644 --- a/crates/console/prometeu-system/src/os/facades/sessions.rs +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -5,7 +5,6 @@ use crate::task::TaskId; use crate::vm_session::{VmSession, VmSessionError}; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::Cartridge; -use prometeu_vm::VirtualMachine; use std::sync::Arc; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -72,7 +71,6 @@ impl<'a> SessionsFacade<'a> { pub fn load_vm_cartridge( &mut self, - legacy_vm: &mut VirtualMachine, cartridge: &Cartridge, ) -> Result { let existing_game_task = if cartridge.app_mode == AppMode::Game { @@ -100,7 +98,6 @@ impl<'a> SessionsFacade<'a> { if !reused_existing_session { self.initialize_session_cartridge(task_id, cartridge)?; - self.initialize_legacy_active_vm(legacy_vm, cartridge)?; } Ok(LoadedVmCartridge { task_id, app_mode: cartridge.app_mode, reused_existing_session }) @@ -122,6 +119,10 @@ impl<'a> SessionsFacade<'a> { self.os.vm_sessions.len() } + pub fn clear_vm_sessions(&mut self) { + self.os.vm_sessions.clear(); + } + fn create_vm_session_for_task(&mut self, task_id: TaskId) -> Result<(), VmSessionError> { let task = self.os.task_manager.get(task_id).expect("session task should exist").clone(); let process = self @@ -157,13 +158,4 @@ impl<'a> SessionsFacade<'a> { session.clear_cartridge_service_state(); session.runtime.initialize_vm(&mut self.os.log_service, &mut session.vm, cartridge) } - - fn initialize_legacy_active_vm( - &mut self, - legacy_vm: &mut VirtualMachine, - cartridge: &Cartridge, - ) -> Result<(), CrashReport> { - self.os.clear_cartridge_service_state(); - self.os.vm_runtime.initialize_vm(&mut self.os.log_service, legacy_vm, cartridge) - } } diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs index 87ba5cb8..367ac0f5 100644 --- a/crates/console/prometeu-system/src/os/facades/vm.rs +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -5,13 +5,11 @@ use crate::vm_session::VmSession; use crate::{RenderWorkerConfig, RenderWorkerController}; use prometeu_bytecode::Value; use prometeu_hal::app_mode::AppMode; -use prometeu_hal::cartridge::Cartridge; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; use prometeu_hal::{ FrameId, InputSignals, RenderOwnership, RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RuntimePlatform, }; -use prometeu_vm::VirtualMachine; use std::sync::atomic::Ordering; pub struct VmFacade<'a> { @@ -19,36 +17,6 @@ pub struct VmFacade<'a> { } impl<'a> VmFacade<'a> { - pub fn initialize( - &mut self, - vm: &mut VirtualMachine, - cartridge: &Cartridge, - ) -> Result<(), CrashReport> { - self.os.clear_cartridge_service_state(); - self.os.vm_runtime.initialize_vm(&mut self.os.log_service, vm, cartridge) - } - - pub fn tick( - &mut self, - vm: &mut VirtualMachine, - signals: &InputSignals, - platform: &mut dyn RuntimePlatform, - ) -> Option { - self.deliver_pending_game_lifecycle_events(); - - self.os.vm_runtime.tick( - &mut self.os.log_service, - &mut self.os.fs, - &mut self.os.fs_state, - &mut self.os.memcard, - &mut self.os.open_files, - &mut self.os.next_handle, - vm, - signals, - platform, - ) - } - pub fn tick_session( &mut self, task_id: TaskId, @@ -77,25 +45,6 @@ impl<'a> VmFacade<'a> { ) } - pub fn deliver_pending_game_lifecycle_events(&mut self) { - if self.os.vm_runtime.current_cartridge_app_mode == AppMode::Game { - let resident_game_task = self.os.foreground_stack.resident_game_task(); - let mut retained = Vec::new(); - let mut delivered = Vec::new(); - - for event in self.os.game_lifecycle_events.drain(..) { - if Some(event.task_id) == resident_game_task { - delivered.push(event); - } else { - retained.push(event); - } - } - - self.os.game_lifecycle_events = retained; - self.os.vm_runtime.deliver_game_lifecycle_events(delivered); - } - } - fn deliver_pending_game_lifecycle_events_to_session(&mut self, task_id: TaskId) { let Some(session) = self.os.vm_sessions.for_task(task_id) else { return; @@ -122,16 +71,9 @@ impl<'a> VmFacade<'a> { } } - pub fn reset(&mut self, vm: &mut VirtualMachine) { - self.os.vm_runtime.reset(vm); - } - - pub fn debug_step_instruction( - &mut self, - vm: &mut VirtualMachine, - platform: &mut dyn RuntimePlatform, - ) -> Option { - self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, platform) + pub fn reset_global_runtime_state(&mut self) { + self.os.clear_cartridge_service_state(); + self.os.vm_runtime.clear_cartridge_state(); } pub fn debug_step_active_session( 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 d55546b1..28038739 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 @@ -6,7 +6,6 @@ use prometeu_hal::{ FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform, ShellUiFramePacket, }; -use prometeu_vm::VirtualMachine; use std::path::PathBuf; const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 }; @@ -175,24 +174,17 @@ impl PrometeuHub { pub fn update_shell_profile( &mut self, os: &mut SystemOS, - vm: &mut VirtualMachine, - signals: &InputSignals, platform: &mut dyn RuntimePlatform, ) -> SystemProfileUpdate { - let mut crash = None; let mut action = self.gui_update(os, platform); - if os.windows().focused_window().is_some() { - if platform.input().pad().start().down { - action = Some(SystemProfileAction::CloseShell); - } else if action != Some(SystemProfileAction::CloseShell) { - crash = os.vm().tick(vm, signals, platform); - } + if os.windows().focused_window().is_some() && platform.input().pad().start().down { + action = Some(SystemProfileAction::CloseShell); } self.render(os, platform); - SystemProfileUpdate { crash, action } + SystemProfileUpdate { crash: None, action } } pub fn update_vm_shell_profile( diff --git a/crates/console/prometeu-system/src/services/vm_session.rs b/crates/console/prometeu-system/src/services/vm_session.rs index 88a005ef..caa023f3 100644 --- a/crates/console/prometeu-system/src/services/vm_session.rs +++ b/crates/console/prometeu-system/src/services/vm_session.rs @@ -138,6 +138,10 @@ impl VmSessionRegistry { pub fn is_empty(&self) -> bool { self.sessions.is_empty() } + + pub fn clear(&mut self) { + self.sessions.clear(); + } } fn app_mode_for_task_and_process( diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index c14df166..dc75992a 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -432,12 +432,11 @@ mod tests { } #[test] - fn breakpoint_commands_mutate_active_vm_session_not_legacy_vm() { + fn breakpoint_commands_mutate_active_vm_session() { let mut debugger = HostDebugger::new(); let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); let task_id = firmware.os.sessions().create_vm_game_task(42, "Sector Crawl"); - firmware.vm.insert_breakpoint(9); debugger.handle_command( DebugCommand::SetBreakpoint { pc: 123 }, @@ -445,7 +444,6 @@ mod tests { &mut platform, ); - assert_eq!(firmware.vm.breakpoints_list(), vec![9]); assert_eq!( firmware .os @@ -463,7 +461,6 @@ mod tests { &mut platform, ); - assert_eq!(firmware.vm.breakpoints_list(), vec![9]); assert!( firmware .os diff --git a/discussion/index.ndjson b/discussion/index.ndjson index d1685785..1cc34561 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md b/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md index 28757dc1..df572e05 100644 --- a/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md +++ b/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md @@ -2,7 +2,7 @@ id: PLN-0155 ticket: foreground-stack-game-pause-shell-vm-backed title: Retire Legacy Firmware VM Runtime Bridge -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From c34bbaf936fa6195857142ca9716e4e91d9a0d14 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 00:30:56 +0100 Subject: [PATCH 28/29] implements PLN-0156 --- .../src/firmware/firmware_step_hub_home.rs | 17 ++- .../firmware/firmware_step_load_cartridge.rs | 2 +- crates/console/prometeu-system/src/lib.rs | 2 +- .../prometeu-system/src/os/facades/mod.rs | 2 +- .../src/os/facades/sessions.rs | 112 +++++++++++++----- crates/console/prometeu-system/src/os/mod.rs | 4 +- .../prometeu-system/src/os/system_os.rs | 74 +++++++++++- .../src/debugger.rs | 6 +- discussion/index.ndjson | 2 +- ...n-vm-session-lifecycle-error-boundaries.md | 2 +- 10 files changed, 181 insertions(+), 42 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 cc5479f7..f00f3966 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 @@ -27,7 +27,22 @@ impl HubHomeStep { match outcome.action { Some(SystemProfileAction::LaunchNativeShell(app)) => { - let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title()); + let task_id = match ctx + .os + .sessions() + .try_create_native_shell_task(app.app_id(), app.title()) + { + Ok(task_id) => task_id, + Err(error) => { + ctx.os.log( + LogLevel::Warn, + LogSource::Hub, + 0, + format!("Failed to launch native shell {}: {:?}", app.title(), error), + ); + return None; + } + }; let id = ctx.os.windows().add_window( app.title().to_string(), WindowOwner::Task(task_id), 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 397f9886..80307186 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 @@ -44,7 +44,7 @@ impl LoadCartridgeStep { self.loaded_app_mode = Some(loaded.app_mode); } Err(report) => { - self.init_error = Some(report); + self.init_error = Some(report.into_crash_report("load VM cartridge into session")); } } } diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 3baf7d46..20be73b7 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -6,7 +6,7 @@ mod services; pub use crash_report::CrashReport; pub use os::{ DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, - LifecycleOperation, SystemOS, + LifecycleOperation, SessionError, SystemOS, }; pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate}; pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink}; diff --git a/crates/console/prometeu-system/src/os/facades/mod.rs b/crates/console/prometeu-system/src/os/facades/mod.rs index be54cf66..dff51238 100644 --- a/crates/console/prometeu-system/src/os/facades/mod.rs +++ b/crates/console/prometeu-system/src/os/facades/mod.rs @@ -6,6 +6,6 @@ mod window; pub use fs::FsFacade; pub use lifecycle::LifecycleFacade; -pub use sessions::SessionsFacade; +pub use sessions::{SessionError, SessionsFacade}; pub use vm::VmFacade; pub use window::WindowFacade; diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs index 0db29dc1..e88fd6b6 100644 --- a/crates/console/prometeu-system/src/os/facades/sessions.rs +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -1,6 +1,7 @@ use crate::CrashReport; use crate::os::LifecycleError; use crate::os::SystemOS; +use crate::process::ProcessId; use crate::task::TaskId; use crate::vm_session::{VmSession, VmSessionError}; use prometeu_hal::app_mode::AppMode; @@ -14,21 +15,55 @@ pub struct LoadedVmCartridge { pub reused_existing_session: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SessionError { + Lifecycle(LifecycleError), + VmSession(VmSessionError), + TaskNotFound(TaskId), + ProcessNotFound(ProcessId), + MissingVmSession(TaskId), + Initialization(CrashReport), +} + +impl SessionError { + pub fn into_crash_report(self, operation: &str) -> CrashReport { + match self { + Self::Initialization(report) => report, + error => { + CrashReport::VmPanic { message: format!("{operation} failed: {error:?}"), pc: None } + } + } + } +} + +impl From for SessionError { + fn from(error: LifecycleError) -> Self { + Self::Lifecycle(error) + } +} + +impl From for SessionError { + fn from(error: VmSessionError) -> Self { + Self::VmSession(error) + } +} + pub struct SessionsFacade<'a> { pub(in crate::os) os: &'a mut SystemOS, } impl<'a> SessionsFacade<'a> { + #[cfg(test)] pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into) -> TaskId { self.try_create_vm_game_task(app_id, title) - .expect("newly created game task should have an associated process") + .expect("test/setup VM game creation should succeed") } pub fn try_create_vm_game_task( &mut self, app_id: u32, title: impl Into, - ) -> Result { + ) -> Result { if let Some(session) = self.os.vm_sessions.resident_game_for_app(app_id) { return Ok(session.task_id); } @@ -37,42 +72,54 @@ impl<'a> SessionsFacade<'a> { let process_id = self.os.process_manager.spawn_vm_game(app_id, title.clone()); let task_id = self.os.task_manager.create_game_task(process_id, app_id, title); self.os.lifecycle().set_game_foreground_task(task_id)?; - self.create_vm_session_for_task(task_id) - .expect("newly created game task should create a VM session"); + self.try_create_vm_session_for_task(task_id)?; Ok(task_id) } + #[cfg(test)] pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { + self.try_create_vm_shell_task(app_id, title) + .expect("test/setup VM shell creation should succeed") + } + + pub fn try_create_vm_shell_task( + &mut self, + app_id: u32, + title: impl Into, + ) -> Result { let title = title.into(); let process_id = self.os.process_manager.spawn_vm_shell(app_id, title.clone()); let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title); - self.os - .lifecycle() - .set_shell_foreground_task(task_id) - .expect("newly created shell task should have an associated process"); - self.create_vm_session_for_task(task_id) - .expect("newly created shell task should create a VM session"); + self.os.lifecycle().set_shell_foreground_task(task_id)?; + self.try_create_vm_session_for_task(task_id)?; - task_id + Ok(task_id) } + #[cfg(test)] pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { + self.try_create_native_shell_task(app_id, title) + .expect("test/setup native shell creation should succeed") + } + + pub fn try_create_native_shell_task( + &mut self, + app_id: u32, + title: impl Into, + ) -> Result { let title = title.into(); let process_id = self.os.process_manager.spawn_native_shell(app_id, title.clone()); let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title); - self.os - .lifecycle() - .set_shell_foreground_task(task_id) - .expect("newly created native shell task should have an associated process"); + self.os.lifecycle().set_shell_foreground_task(task_id)?; - task_id + Ok(task_id) } pub fn load_vm_cartridge( &mut self, cartridge: &Cartridge, - ) -> Result { + ) -> Result { let existing_game_task = if cartridge.app_mode == AppMode::Game { self.os .vm_sessions @@ -85,19 +132,15 @@ impl<'a> SessionsFacade<'a> { let (task_id, reused_existing_session) = if let Some(task_id) = existing_game_task { (task_id, true) } else if cartridge.app_mode == AppMode::Shell { - (self.create_vm_shell_task(cartridge.app_id, cartridge.title.clone()), false) + (self.try_create_vm_shell_task(cartridge.app_id, cartridge.title.clone())?, false) } else { - let task_id = self - .try_create_vm_game_task(cartridge.app_id, cartridge.title.clone()) - .map_err(|error| CrashReport::VmPanic { - message: format!("failed to create VM game task: {error:?}"), - pc: None, - })?; + let task_id = + self.try_create_vm_game_task(cartridge.app_id, cartridge.title.clone())?; (task_id, false) }; if !reused_existing_session { - self.initialize_session_cartridge(task_id, cartridge)?; + self.try_initialize_session_cartridge(task_id, cartridge)?; } Ok(LoadedVmCartridge { task_id, app_mode: cartridge.app_mode, reused_existing_session }) @@ -123,13 +166,14 @@ impl<'a> SessionsFacade<'a> { self.os.vm_sessions.clear(); } - fn create_vm_session_for_task(&mut self, task_id: TaskId) -> Result<(), VmSessionError> { - let task = self.os.task_manager.get(task_id).expect("session task should exist").clone(); + pub fn try_create_vm_session_for_task(&mut self, task_id: TaskId) -> Result<(), SessionError> { + let task = + self.os.task_manager.get(task_id).ok_or(SessionError::TaskNotFound(task_id))?.clone(); let process = self .os .process_manager .get(task.process_id) - .expect("session process should exist") + .ok_or(SessionError::ProcessNotFound(task.process_id))? .clone(); let cap_config = self.os.vm_runtime.certifier.config; @@ -138,24 +182,28 @@ impl<'a> SessionsFacade<'a> { .vm_sessions .create_for_task(&task, &process, "", Some(cap_config), logs_count) .map(|_| ()) + .map_err(SessionError::from) } - fn initialize_session_cartridge( + pub fn try_initialize_session_cartridge( &mut self, task_id: TaskId, cartridge: &Cartridge, - ) -> Result<(), CrashReport> { + ) -> Result<(), SessionError> { let session = self .os .vm_sessions .for_task_mut(task_id) - .expect("loaded VM task should have a VM session"); + .ok_or(SessionError::MissingVmSession(task_id))?; session.app_id = cartridge.app_id; session.title = cartridge.title.clone(); session.app_version = cartridge.app_version.clone(); session.app_mode = cartridge.app_mode; session.clear_cartridge_service_state(); - session.runtime.initialize_vm(&mut self.os.log_service, &mut session.vm, cartridge) + session + .runtime + .initialize_vm(&mut self.os.log_service, &mut session.vm, cartridge) + .map_err(SessionError::Initialization) } } diff --git a/crates/console/prometeu-system/src/os/mod.rs b/crates/console/prometeu-system/src/os/mod.rs index f14a8b5e..665ef4cc 100644 --- a/crates/console/prometeu-system/src/os/mod.rs +++ b/crates/console/prometeu-system/src/os/mod.rs @@ -2,7 +2,9 @@ mod facades; mod lifecycle; mod system_os; -pub use facades::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade}; +pub use facades::{ + FsFacade, LifecycleFacade, SessionError, SessionsFacade, VmFacade, WindowFacade, +}; pub use lifecycle::{ DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError, LifecycleOperation, diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 35fb4ca4..a39cb2d3 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -143,17 +143,33 @@ impl SystemOS { #[cfg(test)] mod tests { use super::*; - use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation}; - use crate::process::{ProcessKind, ProcessState}; + use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError}; + use crate::process::{ProcessId, ProcessKind, ProcessState}; use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState}; use crate::task::{TaskId, TaskState}; use prometeu_hal::app_mode::AppMode; + use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge}; + use prometeu_hal::syscalls::caps; fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState { let task = os.task_manager.get(task_id).expect("task should exist"); os.process_manager.get(task.process_id).expect("process should exist").state } + fn session_test_cartridge(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge { + Cartridge { + app_id, + title: title.to_string(), + app_version: "1.0.0".to_string(), + app_mode, + capabilities: caps::NONE, + program: vec![], + assets: AssetsPayloadSource::empty(), + asset_table: vec![], + preload: vec![], + } + } + #[test] fn set_foreground_task_marks_task_and_process_running() { let mut os = SystemOS::new(None); @@ -453,6 +469,60 @@ mod tests { ); } + #[test] + fn vm_shell_load_returns_typed_error_when_foreground_shell_exists() { + let mut os = SystemOS::new(None); + let first = os.sessions().create_vm_shell_task(7, "Settings"); + let cartridge = session_test_cartridge(AppMode::Shell, 8, "Files"); + + let error = os.sessions().load_vm_cartridge(&cartridge).expect_err("load should fail"); + + match error { + SessionError::Lifecycle(LifecycleError::ForegroundShellAlreadyExists { + existing, + requested, + }) => { + assert_eq!(existing, first); + assert_ne!(requested, first); + } + other => panic!("expected foreground shell lifecycle error, got {:?}", other), + } + } + + #[test] + fn vm_session_creation_returns_typed_error_for_missing_task() { + let mut os = SystemOS::new(None); + + assert_eq!( + os.sessions().try_create_vm_session_for_task(TaskId(999)), + Err(SessionError::TaskNotFound(TaskId(999))) + ); + } + + #[test] + fn vm_session_creation_returns_typed_error_for_missing_process() { + let mut os = SystemOS::new(None); + let task_id = os.task_manager.create_game_task(ProcessId(999), 42, "Broken"); + + assert_eq!( + os.sessions().try_create_vm_session_for_task(task_id), + Err(SessionError::ProcessNotFound(ProcessId(999))) + ); + } + + #[test] + fn session_cartridge_initialization_returns_typed_error_for_missing_session() { + let mut os = SystemOS::new(None); + let process_id = os.process_manager.spawn_vm_game(42, "Sector Crawl"); + let task_id = os.task_manager.create_game_task(process_id, 42, "Sector Crawl"); + let cartridge = session_test_cartridge(AppMode::Game, 42, "Sector Crawl"); + + assert_eq!( + os.sessions().try_initialize_session_cartridge(task_id, &cartridge), + Err(SessionError::MissingVmSession(task_id)) + ); + } + #[test] fn closing_foreground_shell_returns_to_hub() { let mut os = SystemOS::new(None); diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index dc75992a..fac30e51 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -436,7 +436,11 @@ mod tests { let mut debugger = HostDebugger::new(); let mut firmware = Firmware::new(None); let mut platform = TestPlatform::new(); - let task_id = firmware.os.sessions().create_vm_game_task(42, "Sector Crawl"); + let task_id = firmware + .os + .sessions() + .try_create_vm_game_task(42, "Sector Crawl") + .expect("debugger test should create a game session"); debugger.handle_command( DebugCommand::SetBreakpoint { pc: 123 }, diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1cc34561..641805c9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -2,7 +2,7 @@ {"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":"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":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0041","status":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md b/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md index 5ba1ca28..b84a935d 100644 --- a/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md +++ b/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md @@ -2,7 +2,7 @@ id: PLN-0156 ticket: foreground-stack-game-pause-shell-vm-backed title: Harden VM Session Lifecycle Error Boundaries -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0038] tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] From 6dc8cb537277117e7d6c5bfadd5b97598ee58ff5 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 5 Jul 2026 00:36:15 +0100 Subject: [PATCH 29/29] VM Context Ownership for Resident Game and VM-Backed Shell --- discussion/index.ndjson | 4 +- ...p-and-vm-session-ownership-are-separate.md | 145 +++++++++++++ ...ground-stack-game-pause-shell-vm-backed.md | 178 ---------------- ...p-for-resident-game-and-vm-backed-shell.md | 186 ---------------- ...oreground-stack-and-game-pause-contract.md | 191 ----------------- ...ssion-ownership-for-vm-backed-processes.md | 199 ------------------ ...te-desktop-home-key-outside-guest-input.md | 96 --------- ...plement-systemos-foreground-stack-state.md | 116 ---------- ...cify-foreground-pause-and-home-contract.md | 114 ---------- ...alidate-game-home-shell-game-end-to-end.md | 106 ---------- ...eliver-game-pause-resume-and-suspension.md | 110 ---------- ...render-audio-and-input-pause-boundaries.md | 104 --------- ...ver-game-lifecycle-events-to-vm-runtime.md | 127 ----------- ...ive-pause-budget-tick-before-suspension.md | 122 ----------- ...sident-game-when-shell-takes-foreground.md | 124 ----------- ...tralize-resident-game-resume-transition.md | 149 ------------- ...lidate-lifecycle-process-lookup-helpers.md | 119 ----------- ...ve-native-shell-profile-update-into-hub.md | 144 ------------- ...roduce-vm-session-registry-and-identity.md | 92 -------- ...-move-vm-runtime-state-into-vm-sessions.md | 99 --------- ...e-cartridge-loading-through-vm-sessions.md | 90 -------- ...51-schedule-foreground-vm-session-ticks.md | 92 -------- ...e-game-and-vm-shell-session-coexistence.md | 86 -------- ...nership-and-background-ready-scheduling.md | 82 -------- ...debugger-operations-through-vm-sessions.md | 95 --------- ...etire-legacy-firmware-vm-runtime-bridge.md | 89 -------- ...n-vm-session-lifecycle-error-boundaries.md | 96 --------- 27 files changed, 147 insertions(+), 3008 deletions(-) create mode 100644 discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md delete mode 100644 discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md delete mode 100644 discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md delete mode 100644 discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md delete mode 100644 discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md delete mode 100644 discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md delete mode 100644 discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md delete mode 100644 discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md delete mode 100644 discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md delete mode 100644 discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md delete mode 100644 discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md delete mode 100644 discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md delete mode 100644 discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md delete mode 100644 discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md delete mode 100644 discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md delete mode 100644 discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md delete mode 100644 discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md delete mode 100644 discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md delete mode 100644 discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md delete mode 100644 discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md delete mode 100644 discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md delete mode 100644 discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md delete mode 100644 discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md delete mode 100644 discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md delete mode 100644 discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md delete mode 100644 discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 641805c9..519b2791 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,8 +1,8 @@ -{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":39,"PLN":157,"LSN":52,"CLSN":1}} +{"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":"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":"in_progress","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":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-07-03"},{"id":"AGD-0046","file":"AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04"}],"decisions":[{"id":"DEC-0037","file":"DEC-0037-foreground-stack-and-game-pause-contract.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0041"},{"id":"DEC-0038","file":"DEC-0038-vm-session-ownership-for-vm-backed-processes.md","status":"accepted","created_at":"2026-07-04","updated_at":"2026-07-04","ref_agenda":"AGD-0046"}],"plans":[{"id":"PLN-0136","file":"PLN-0136-route-desktop-home-key-outside-guest-input.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0137","file":"PLN-0137-implement-systemos-foreground-stack-state.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0138","file":"PLN-0138-specify-foreground-pause-and-home-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0139","file":"PLN-0139-validate-game-home-shell-game-end-to-end.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0140","file":"PLN-0140-deliver-game-pause-resume-and-suspension.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0141","file":"PLN-0141-integrate-render-audio-and-input-pause-boundaries.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0037"]},{"id":"PLN-0142","file":"PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0143","file":"PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0144","file":"PLN-0144-suspend-resident-game-when-shell-takes-foreground.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0145","file":"PLN-0145-centralize-resident-game-resume-transition.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0146","file":"PLN-0146-consolidate-lifecycle-process-lookup-helpers.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0147","file":"PLN-0147-move-native-shell-profile-update-into-hub.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0037"]},{"id":"PLN-0148","file":"PLN-0148-introduce-vm-session-registry-and-identity.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0149","file":"PLN-0149-move-vm-runtime-state-into-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0150","file":"PLN-0150-route-cartridge-loading-through-vm-sessions.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0151","file":"PLN-0151-schedule-foreground-vm-session-ticks.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0152","file":"PLN-0152-validate-game-and-vm-shell-session-coexistence.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0153","file":"PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md","status":"done","created_at":"2026-07-04","updated_at":"2026-07-04","ref_decisions":["DEC-0038"]},{"id":"PLN-0154","file":"PLN-0154-route-debugger-operations-through-vm-sessions.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0155","file":"PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]},{"id":"PLN-0156","file":"PLN-0156-harden-vm-session-lifecycle-error-boundaries.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0038"]}],"lessons":[]} +{"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"}]} {"type":"discussion","id":"DSC-0042","status":"done","ticket":"real-render-worker-establishment","title":"Real Render Worker Establishment","created_at":"2026-06-06","updated_at":"2026-06-20","tags":["runtime","renderer","worker","concurrency","host","hal","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0042-real-render-worker-establishment/LSN-0049-render-worker-publication-boundary.md","status":"done","created_at":"2026-06-20","updated_at":"2026-06-20"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md b/discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md new file mode 100644 index 00000000..c33ccd00 --- /dev/null +++ b/discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md @@ -0,0 +1,145 @@ +--- +id: LSN-0052 +ticket: foreground-stack-game-pause-shell-vm-backed +title: Foreground Ownership and VM Session Ownership Are Separate +created: 2026-07-05 +tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] +--- + +# LSN-0052: Foreground Ownership and VM Session Ownership Are Separate + +## Context + +DSC-0041 closed the `Game -> Home/Shell -> same Game` lifecycle for the first +runtime implementation that supports both native Shell apps and VM-backed Shell +apps. The important architectural shift was not only pausing the Game. The +runtime also had to stop treating one firmware-owned VM as the place where all +guest execution state lives. + +The final model separates two questions: + +- Who owns the foreground visual/input authority right now? +- Which VM-backed process owns a mutable VM execution context? + +Foreground ownership is global SystemOS policy. VM execution state belongs to a +session associated with a task/process. A suspended resident Game is not a saved +snapshot and not a special global VM. It is a live VM session that is temporarily +ineligible for normal foreground ticks, input, frame pacing, and render +publication. + +## Key Decisions + +### Foreground Stack and Game Pause Contract + +**What:** PROMETEU uses a single foreground owner in v1. Hub/Home is the root +Shell owner, a Game may remain resident, and one foreground Shell app may run in +front of the resident Game. Pressing desktop `Esc` is a host/SystemOS Home +request, not guest input. + +**Why:** This preserves console-like navigation without turning the runtime into +a general multitasking scheduler. It lets the system pause and suspend a Game, +run Hub or Shell, then resume the same Game deterministically. + +**Trade-offs:** The model intentionally rejects multiple resident Games, direct +Shell-to-Game return, background execution, and Game-to-Game switching in v1. +Those features remain future work, but the lifecycle structure now has a place +to add them. + +### VM Session Ownership for VM-Backed Processes + +**What:** VM-backed mutable execution state is owned by VM sessions. A +VM-backed Game and a VM-backed Shell never share the same VM, stack, heap, PC, +open handles, cartridge identity, or VM-scoped runtime state. + +**Why:** A single firmware-owned VM cannot represent a suspended resident Game +and a foreground VM Shell at the same time. Loading the Shell into that global +VM would overwrite the Game context that the lifecycle model says is resident. + +**Trade-offs:** Session ownership is more explicit than one global VM, but it +keeps the model simpler than snapshot/restore. It also prepares the runtime for +future background-capable sessions without committing to background scheduling +yet. + +## Patterns and Algorithms + +### Pause Is Cooperative, Suspension Is OS Authority + +The Game receives lifecycle events such as pause and resume, but the OS owns the +actual scheduling state. If the Game does not cooperate within the bounded pause +budget, SystemOS can suspend it anyway. On resume, the Game receives a foreground +restore event and may still keep its internal pause screen while it +synchronizes. + +This distinction prevents userland from owning system navigation. The Game may +observe and react. It cannot veto Home. + +### Foreground Owner Is Not the Same as Tick Eligibility + +Foreground ownership controls input, visual authority, render ownership, and +normal frame pacing. Tick eligibility controls whether a VM session may execute. +In v1, only the foreground VM session receives normal ticks, and the resident +Game does not tick while Shell or Hub is foreground. + +The important design detail is that the data model does not assume this must be +true forever. Sessions can later become background-eligible without changing who +owns render/input foreground. + +### VM Sessions Are the Unit of Mutable Guest State + +Each VM-backed task/process maps to a `VmSession`. The session owns the VM, +runtime state, session-scoped filesystem state, open file handles, next handle +allocation, cartridge identity, lifecycle delivery state, and debug state. + +Cartridge loading creates or reuses a VM session through SystemOS session +services. Firmware orchestrates macro states such as `LoadCartridge`, +`GameRunning`, `HubHome`, and `ShellRunning`, but it no longer owns a canonical +guest VM. + +### Debugger and Host Inspection Must Follow the Active Session + +Host debugging must inspect and mutate the active session VM, not a transitional +global VM. PC, operand stack, breakpoints, debug-step execution, cartridge +identity, and breakpoint-hit events all need to resolve through active session +state. + +This keeps debugger behavior aligned with actual execution. A debugger that +observes a different VM than the scheduler ticks is worse than no debugger: it +creates false confidence and hides lifecycle bugs. + +### Operational Failures Should Cross Facades as Typed Errors + +Session creation and loading are operational boundaries. Missing task, missing +process, duplicate VM session, missing VM session during initialization, and +foreground Shell rejection should be typed errors. Firmware can then choose a +controlled crash report, a launch rejection, or a logged no-transition outcome. + +Panics are reserved for test/setup wrappers or impossible internal invariants +that have already been proven by prior checks. + +## Pitfalls + +- Do not use a global VM as a compatibility bridge after session ownership is + introduced. It will eventually become stale or contradictory. +- Do not let Home/SystemOS be guest input. It changes OS authority, so it must + bypass guest pad state. +- Do not present old frames after foreground ownership changes. Render ownership + and epoch validation are part of lifecycle correctness, not renderer polish. +- Do not model pause and suspension as the same state. Pause is visible to the + Game; suspension is the scheduler mechanism. +- Do not encode v1 limits such as one resident Game or one foreground Shell by + collapsing storage into one VM. Express those limits as lifecycle/session + policy. +- Do not let debugger state drift from scheduler state. The debugger must use + the same active session as normal execution. + +## Takeaways + +- Foreground ownership is global policy; VM execution state is session-owned. +- A suspended Game is a preserved session, not a serialized VM and not a global + VM waiting to be reused. +- Shell apps can be native or VM-backed, but only VM-backed processes need VM + sessions. +- Session ownership is the foundation for future background-capable processes, + even before background execution exists. +- Typed session/lifecycle errors make launch and recovery behavior explicit at + firmware boundaries. diff --git a/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md b/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md deleted file mode 100644 index 65520de4..00000000 --- a/discussion/workflow/agendas/AGD-0041-foreground-stack-game-pause-shell-vm-backed.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -id: AGD-0041 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Foreground Stack, Game Pause, and VM-Backed Shell Coexistence -status: accepted -created: 2026-06-05 -resolved: 2026-07-03 -decision: DEC-0037 -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Contexto - -Durante a discussao da `DSC-0040` sobre separar VM e render worker, surgiu um caso que nao pertence somente ao render: `Game -> Shell -> Game`. - -O caso minimo e: - -- um Game esta rodando; -- o usuario aperta Home; -- o Game deve sair do foreground sem ser encerrado; -- o Hub/Shell assume a tela; -- o usuario pode abrir um app Shell, inclusive VM-backed; -- depois o usuario retorna ao mesmo Game. - -O render worker precisa saber qual visual owner pode apresentar frames, mas a decisao principal e de lifecycle: quais VMs existem, quais podem executar, o que significa pausar um Game, e como Shell apps coexistem com um Game pausado. - -Material relacionado: - -- `DSC-0031`: separacao entre perfis Game e System/Shell. -- `DSC-0032`: SystemOS como autoridade de lifecycle/process/task. -- `DSC-0035`: janelas Shell pertencem a tasks e definem liveness do app. -- `DSC-0036`: Hub/Shell prova fatias de lifecycle. -- `DSC-0040`: render ownership/epoch durante transicoes e paralelismo VM/render. - -## Problema - -Precisamos definir o contrato de foreground para impedir ambiguidades como: - -- Game pausado continuando a executar frames logicos enquanto Shell esta no foreground; -- Shell VM-backed competindo com Game por pacing, input, superficie ou render ownership; -- multiplos Shells foreground ou multiplos Games ativos sem uma politica explicita; -- retorno ao Game sem clareza sobre estado pausado, input, render invalidation e retomada da VM. - -Sem esse contrato, a separacao VM/render da `DSC-0040` pode ficar tecnicamente correta, mas o SystemOS ainda pode permitir estados incoerentes. - -## Pontos Criticos - -- Existe apenas um foreground visual owner por vez. -- Background execution e outro assunto e deve ficar fora da primeira decisao, salvo se for necessario definir uma proibicao explicita. -- Shell VM-backed nao deve herdar politica de game loop so por usar VM. -- Game pausado provavelmente deve reter estado, mas nao deve consumir frames logicos. -- O Hub/Shell precisa ser capaz de rodar enquanto o Game esta suspenso. -- A transicao deve invalidar render submissions antigas para impedir present de frames obsoletos. -- O modelo precisa caber no SystemOS/lifecycle existente, nao diretamente no render worker. - -## Opcoes - -### Opcao A - Foreground unico, Game suspenso, Shell unico - -- **Abordagem:** permitir no maximo um Game carregado e no maximo um Shell foreground; ao apertar Home, o Game entra em estado pausado/suspenso e Shell/Hub assume o foreground. Shell VM-backed pode executar apenas enquanto for o foreground Shell. -- **Pro:** modelo simples, previsivel e alinhado com console; reduz concorrencia real entre VMs; combina com a regra de render ownership unico. -- **Contra:** nao cobre multitarefa/background; futuras features de overlay ou apps persistentes precisarao expandir o contrato. -- **Manutenibilidade:** alta; SystemOS pode modelar uma pilha pequena de foreground/suspended sem virar scheduler generico. - -### Opcao B - Multiplas VMs residentes, somente foreground executa - -- **Abordagem:** permitir que Game e Shell VM-backed coexistam como VMs residentes, mas somente a entidade foreground executa. O Game pausado fica residente, sem tick; Shell VM-backed roda sob lifecycle Shell. -- **Pro:** preserva estado e permite retorno rapido; nao exige background execution. -- **Contra:** precisa contrato claro de memoria, ownership de input, foco, cancelamento e fechamento. -- **Manutenibilidade:** boa se os estados forem explicitos; ruim se a residencia for confundida com execucao em background. - -### Opcao C - Encerrar Game ao entrar no Shell VM-backed - -- **Abordagem:** Home fecha/salva o Game antes de abrir Shell VM-backed, evitando coexistencia. -- **Pro:** simplifica o runtime. -- **Contra:** quebra a expectativa de console moderno; torna `Game -> Shell -> Game` um reload, nao uma pausa. -- **Manutenibilidade:** simples no curto prazo, mas provavelmente gera mais excecoes e UX ruim depois. - -### Opcao D - Background real desde ja - -- **Abordagem:** permitir Game pausado ou Shell apps rodando em background com politicas de prioridade. -- **Pro:** cobre casos futuros ricos. -- **Contra:** amplia demais escopo: scheduler, budget, energia, audio, input, IO e memoria. -- **Manutenibilidade:** baixa para v1; mistura a decisao de foreground com multitarefa real. - -## Sugestao / Recomendacao - -Recomendo seguir com a combinacao das opcoes A e B: - -- somente um Game residente por vez; -- somente um Shell foreground por vez; -- Game pode ficar pausado/suspenso enquanto Shell esta foreground; -- Shell VM-backed pode existir junto com Game pausado, mas segue lifecycle Shell; -- somente o owner foreground executa VM/tick/event loop conforme sua politica; -- background execution fica explicitamente fora de escopo; -- render ownership muda junto com foreground owner e deve invalidar submissions antigas via epoch/generation, conforme `DSC-0040`. - -Isso preserva o ciclo `Game -> Home/Shell -> Game` sem transformar o runtime em multitarefa geral agora. - -## Perguntas em Aberto - -- [x] Qual e o nome canonico dos estados de Game fora do foreground: `Paused`, `Suspended`, ambos, ou outro? -- [x] Ao apertar Home, o Game deve receber um evento/interrupt/trap de pausa antes de parar de executar? -- [x] O Hub e um Shell especial sempre presente ou uma task Shell comum? -- [x] Um app Shell VM-backed pode substituir o Hub no foreground ou o Hub permanece como owner raiz? -- [x] Quando o Shell VM-backed fecha, o retorno vai sempre para Hub ou pode voltar diretamente ao Game? -- [x] O input do Game e drenado/limpo ao pausar e ao retomar? -- [x] Audio do Game pausa junto com a VM ou tem politica propria? -- [x] Existe limite de memoria/residencia para manter Game pausado enquanto Shell app roda? -- [x] O retorno ao Game exige novo frame antes de trocar a superficie, ou a tela pode manter Hub/Shell ate a primeira submission valida? -- [x] Quais eventos de lifecycle precisam ser visiveis para firmware, OS, VM e host? - -## Resolucao - -Fica aceito o modelo `Game -> Home/Shell -> mesmo Game` com foreground unico e -residencia limitada: - -- somente um Game pode estar residente por vez; -- somente um Shell pode ser foreground por vez; -- Hub e o Shell root/especial e permanece owner raiz/back-stack; -- um Shell VM-backed pode assumir o foreground visual, mas segue lifecycle de - Shell e nao herda politica de game loop; -- fechar um Shell VM-backed retorna ao Hub em v1; -- background execution real fica fora de escopo; -- no host desktop, `Esc` e a tecla principal para solicitar Home/SystemOS e a - tecla fisica `Home` pode existir como alias; -- a acao Home/SystemOS e interceptada pelo host/SystemOS e nao entra no pad - logico visivel ao Game; -- `Paused` e o estado/evento cooperativo visivel ao Game; -- `Suspended` e o estado operacional imposto pelo OS/scheduler; -- ao sair do foreground, o OS notifica o Game para pausar e concede um budget - curto de reacao; -- se o Game nao reagir ao pause dentro do budget, o OS suspende a VM mesmo - assim; -- ao retornar, o OS reativa a VM, notifica o Game para resume/foreground - restore e permite que o Game sincronize antes de voltar ao gameplay; -- input pendente do Game e drenado/limpo nas fronteiras de pause/resume; -- audio do Game pausa junto com a VM em v1; -- retorno ao Game deve manter Hub/Shell visivel ate a primeira render submission - valida do Game retomado; -- render ownership muda junto com o foreground owner e invalida submissions - antigas via epoch/generation; -- firmware observa transicoes macro; OS observa lifecycle completo; VM observa - pause/resume; host observa owner/render epoch e estado de apresentacao. - -## Criterio para Encerrar - -A agenda pode ser encerrada quando tivermos uma decisao clara sobre: - -- cardinalidade de Games e Shells residentes/foreground; -- semantica de Game pausado/suspenso; -- politica de execucao para Shell VM-backed durante Game pausado; -- regra de foreground visual owner e render invalidation; -- comportamento minimo do ciclo `Game -> Shell -> Game`; -- itens explicitamente fora de escopo, especialmente background execution real. - -## Discussao - -- 2026-06-05: Agenda criada a partir da `DSC-0040`, quando o caso de Shell VM-backed aberto sobre um Game pausado mostrou que a discussao pertence ao lifecycle/SystemOS, nao apenas ao render worker. -- 2026-07-03: Direcao provisoria aceita para as perguntas 2-10: - Game deve receber evento de pausa com budget curto antes da suspensao; Hub e - Shell root/especial; Shell VM-backed pode assumir foreground visual, mas Hub - permanece como owner raiz/back-stack; fechar Shell VM-backed retorna ao Hub em - v1; input do Game deve ser drenado/limpo em pause/resume; audio do Game pausa - junto com VM em v1; apenas um Game pausado residente e permitido; retorno ao - Game deve manter Hub/Shell visivel ate a primeira submission valida; firmware - ve transicoes macro, OS ve lifecycle completo, VM ve pause/resume, host ve - owner/render epoch e estado de apresentacao. -- 2026-07-03: Pergunta 1 permanece aberta para esclarecer se `Paused` e - `Suspended` sao estados distintos, nomes para camadas diferentes, ou se v1 - deve expor somente um deles. -- 2026-07-03: Direcao proposta para pergunta 1: `Paused` e o estado/evento - cooperativo visivel ao Game; `Suspended` e o estado operacional imposto pelo - OS. Ao sair do foreground, o OS notifica o Game para pausar e concede um - budget curto de reacao; se o Game nao reagir, o OS suspende a VM mesmo assim. - Ao retornar, o OS notifica o Game antes de recoloca-lo no foreground visual, - permitindo sincronizacao/revalidacao. O Game nao controla a suspensao, mas - pode observar `pause`/`resume` para ajustar seu estado interno. diff --git a/discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md b/discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md deleted file mode 100644 index 9e6306b9..00000000 --- a/discussion/workflow/agendas/AGD-0046-vm-context-ownership-for-resident-game-and-vm-backed-shell.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -id: AGD-0046 -ticket: foreground-stack-game-pause-shell-vm-backed -title: VM Context Ownership for Resident Game and VM-Backed Shell -status: accepted -created: 2026-07-04 -resolved: -decision: DEC-0038 -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Contexto - -`DEC-0037` define que o v1 deve suportar o fluxo `Game -> Home/Shell -> same -Game`, com o Game residente pausado/suspenso enquanto Hub ou Shell possuem o -foreground. A implementacao atual ja impede que Shell nativo ticke o Game -suspenso, porque Shell nativo nao usa a VM. - -A lacuna aparece quando o Shell tambem eh VM-backed. Hoje `Firmware` possui um -unico `VirtualMachine` global (`Firmware.vm`). O Game residente e um Shell -VM-backed competem por esse mesmo contexto. Se o Shell VM-backed inicializa uma -nova cartridge nesse objeto, ele sobrescreve o contexto do Game que deveria -permanecer residente/suspenso. - -Isso significa que o modelo de foreground/task/process ja avancou, mas o modelo -de contexto VM ainda esta acoplado ao firmware global. - -## Problema - -Precisamos decidir onde vive o contexto de execucao VM quando existem, ao mesmo -tempo, um Game residente suspenso e um Shell VM-backed foreground. - -O problema nao eh apenas pausar ticks. Para coexistencia real, cada VM-backed -process precisa preservar seu proprio estado: PC, stack, heap, globals, -cartridge identity, runtime services escopados, render ownership metadata e -eventos lifecycle pendentes. - -Enquanto `Firmware.vm` for unico, "suspender" um Game VM nao preserva o -contexto se outra cartridge VM-backed for carregada. - -## Pontos Criticos - -- `DEC-0037` diz que um Game suspenso nao recebe ticks/input/frame pacing, mas - continua residente para voltar ao mesmo Game. -- Shell nativo nao precisa de VM context proprio, mas Shell VM-backed precisa. -- `SystemOS` ja eh autoridade de lifecycle/process/task, entao eh candidato - natural para possuir ou localizar contextos VM por sessao. -- `Firmware` deve orquestrar macro estados, mas nao deveria ser dono unico do - contexto de todos os processos VM-backed. -- `VirtualMachineRuntime` contem estado atualmente misto: identidade da - cartridge atual, render manager, lifecycle delivery, fs/memcard/open handles - e telemetria. Precisamos separar o que eh global do OS do que eh por sessao - VM-backed. -- A solucao deve preservar a restricao v1: no maximo um Game residente e no - maximo um Shell foreground. Ela nao deve abrir multitasking geral. -- Game-to-Game switching continua fora desta agenda e pertence a `DSC-0043`. - -## Opcoes - -### Opcao A - Manter VM global no Firmware e bloquear Shell VM-backed com Game residente - -- **Abordagem:** documentar que Shell VM-backed nao pode abrir enquanto um Game - VM esta residente; apenas Shell nativo coexiste com Game suspenso. -- **Pro:** menor implementacao imediata. -- **Contra:** contradiz a intencao de coexistencia de VM-backed Shell em - `DEC-0037` e deixa a arquitetura incompleta. -- **Manutenibilidade:** baixa, porque o sistema teria regras diferentes para - Shell nativo e Shell VM-backed sem uma fronteira clara de sessao. - -### Opcao B - Serializar o Game VM ao suspender e restaurar ao resumir - -- **Abordagem:** ao suspender Game, extrair snapshot do `VirtualMachine` e do - runtime associado; ao resumir, restaurar snapshot depois do Shell VM-backed. -- **Pro:** preserva a ideia de um unico VM executor ativo. -- **Contra:** exige serializacao profunda e correta de todo o estado VM/runtime, - incluindo handles, assets, render state e side effects. Isso eh muito mais - complexo que a necessidade v1. -- **Manutenibilidade:** baixa a media; snapshots viram contrato dificil e - fragil antes de termos necessidade de hibernacao real. - -### Opcao C - Tornar o contexto VM propriedade da sessao/processo VM-backed - -- **Abordagem:** cada processo VM-backed possui seu proprio `VirtualMachine` e - seu contexto runtime VM-scoped. O scheduler/firmware seleciona qual sessao - recebe tick conforme foreground/lifecycle. Game suspenso simplesmente deixa - de ser tickado, mas seu contexto permanece armazenado na sessao Game. -- **Pro:** alinha com task/process/lifecycle, suporta Game residente + Shell - VM-backed sem sobrescrever contexto, e prepara o terreno para futuras sessoes - sem implementar multitasking geral agora. -- **Contra:** exige separar estado hoje concentrado em `Firmware.vm` e - `SystemOS.vm_runtime`, decidindo o que eh global do OS e o que eh por sessao. -- **Manutenibilidade:** alta, porque a posse do contexto acompanha o processo - que executa a VM. - -### Opcao D - Criar apenas dois slots VM fixos: Game VM e Shell VM - -- **Abordagem:** manter um slot `game_vm` e um slot `shell_vm`, em vez de uma VM - por processo/sessao. -- **Pro:** mais simples que uma tabela geral de sessoes. -- **Contra:** codifica uma limitacao acidental. Ainda precisariamos decidir - ownership, runtime scoped state e lifecycle por slot. Pode virar refactor - duplo quando mais de um Shell ou Game switching aparecer. -- **Manutenibilidade:** media; aceitavel como stepping stone somente se modelado - internamente como sessao, nao como campos especiais permanentes. - -## Sugestao / Recomendacao - -Seguir a Opcao C: contexto VM deve pertencer a uma sessao/processo VM-backed, -nao ao `Firmware` global. - -Para v1, a implementacao pode continuar limitada a uma sessao Game residente e -uma sessao Shell foreground, mas o modelo deve ser "session-owned VM context". -Isso permite que Shell VM-backed rode enquanto o Game fica suspenso sem perder -PC/stack/heap/estado runtime. - -O firmware deve deixar de possuir a VM como estado global de todos os apps e -passar a pedir ao SystemOS/runtime scheduler o contexto VM ativo para o owner -foreground. `SystemOS` deve continuar sendo a autoridade de lifecycle; o -runtime deve separar estado VM-scoped de estado global de OS/render/telemetria -conforme necessario. - -## Perguntas em Aberto - -- [ ] Qual objeto deve representar a sessao VM-backed: `Process`, novo - `VmSession`, ou uma tabela separada ligada a `ProcessId`/`TaskId`? -- [ ] Quais campos de `VirtualMachineRuntime` sao por sessao VM-backed e quais - devem permanecer globais em `SystemOS`? -- [ ] O render manager fica global por foreground owner ou dentro de cada - contexto VM com handoff global? -- [ ] Handles de FS, memcard, assets e open files devem ser por sessao, - por app_id, ou globais com namespace por processo? -- [ ] Como `LoadCartridgeStep` deve mudar: criar sessao VM nova, ativar sessao - existente, ou delegar tudo para uma facade de sessions? -- [ ] Como preservar os testes e invariantes de `DEC-0037` durante a migracao - incremental sem abrir multitasking geral? -- [ ] Como processos background-capable, como um player de musica, entram no - modelo sem confundir foreground visual ownership com elegibilidade de - execucao? -- [ ] Cada Shell VM-backed deve ter uma VM propria, mesmo quando existe mais de - um Shell em memoria no futuro? - -## Criterio para Encerrar - -Esta agenda pode virar decision quando definirmos: - -- o proprietario canonico do contexto VM; -- a fronteira entre estado VM-scoped e estado global do OS; -- como firmware seleciona/ticka a sessao VM ativa; -- como Game residente suspenso e Shell VM-backed foreground coexistem; -- quais restricoes v1 permanecem explicitamente fora de escopo. - -## Discussion - -- 2026-07-04: Aberta a partir da observacao de que Shell nativo coexiste com - Game suspenso, mas Shell VM-backed ainda conflita com o `Firmware.vm` unico. -- 2026-07-04: Discussao adicional separou foreground ownership de background - execution. A recomendacao atual eh que todo processo VM-backed tenha sua - propria `VmSession`; processos background-capable devem ser modelados como - sessoes com classe/politica de scheduling propria, nao como foreground owners. - -## Resolution - -Resolvido em 2026-07-04: - -- O modelo deve seguir a Opcao C: todo processo VM-backed deve possuir uma - `VmSession` propria, ligada ao lifecycle/task/process, em vez de depender de - um `Firmware.vm` global. -- Cada Shell VM-backed deve ter sua propria sessao VM. Ele pode compartilhar a - engine/runtime e caches imutaveis, mas nao stack, heap, PC, globals, handles - ou identidade mutavel de execucao. -- O Game residente suspenso preserva seu contexto porque sua `VmSession` - continua viva; ele apenas deixa de ser elegivel para ticks normais enquanto - Hub/Shell possui foreground. -- Foreground ownership e background execution sao conceitos separados. - Foreground ownership decide input visual, render ownership e frame pacing - normal. Execution eligibility decide quais sessoes podem receber ticks sem - serem foreground. -- Background execution, como player de musica ou background services, nao entra - na primeira implementacao desta agenda. Mesmo assim, o modelo de `VmSession` - deve ser obrigatoriamente compativel com essa evolucao em um futuro proximo. -- Render ownership permanece autoridade global do SystemOS. Sessoes VM-backed - produzem frames, mas nao decidem se podem apresentar. -- `LoadCartridgeStep` nao deve inicializar uma VM global diretamente. Ele deve - delegar criacao/ativacao de sessao para uma facade de sessions/scheduler. - -Esta agenda esta pronta para virar decision normativa. diff --git a/discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md b/discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md deleted file mode 100644 index 5e2d8765..00000000 --- a/discussion/workflow/decisions/DEC-0037-foreground-stack-and-game-pause-contract.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: DEC-0037 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Foreground Stack and Game Pause Contract -status: accepted -created: 2026-07-03 -ref_agenda: AGD-0041 -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Status - -Accepted. - -## Contexto - -`AGD-0041` defines the minimum lifecycle contract for `Game -> Home/Shell -> -same Game`. - -The issue is not only rendering. The render worker needs a single valid visual -owner, but SystemOS must decide which VMs may exist, which owner receives input, -which owner may tick, what happens to a Game when Home takes foreground, and how -a VM-backed Shell app coexists with a paused Game. - -This decision builds on: - -- `DSC-0031`: runtime mode separation between Game and System/Shell; -- `DSC-0032`: SystemOS lifecycle/process/task authority; -- `DSC-0035`: task-owned Shell windows and Shell liveness; -- `DSC-0036`: Hub/Shell as a lifecycle proving surface; -- `DSC-0040`: render ownership/epoch and stale submission invalidation. - -## Decisao - -PROMETEU MUST use a single-foreground-owner model for v1. - -At most one Game MAY be resident at a time. At most one Shell app MAY be the -foreground visual owner at a time. - -The Hub MUST be treated as the root/special Shell owner for Home. A VM-backed -Shell app MAY replace the Hub as the foreground visual owner, but the Hub -remains the root/back-stack controller. In v1, closing a VM-backed Shell app -MUST return to Hub/Home, not directly to the Game. - -When the user requests Home while a Game is running, SystemOS MUST start a -cooperative pause transition: - -- the Game receives a pause lifecycle notification; -- the Game is given a short bounded budget to react; -- after that budget, SystemOS MAY suspend the Game VM even if the Game did not - cooperate; -- the Game remains resident but MUST NOT receive normal gameplay ticks, normal - game input, or frame pacing while suspended; -- the Shell/Hub foreground owner MAY execute according to Shell lifecycle rules. - -`Paused` is the Game-visible lifecycle state/event. `Suspended` is the -OS/scheduler operational state. A Game may observe pause/resume, but it MUST NOT -control whether the OS suspends or resumes its VM. - -When returning to the same Game, SystemOS MUST reactivate the Game VM and send a -resume or foreground-restore lifecycle notification before restoring visual -foreground. The Game MAY remain internally paused while it synchronizes. The -system MUST keep Hub/Shell visible until the resumed Game produces a valid new -render submission for the current ownership epoch. - -Input pending for the Game MUST be drained or cleared across pause/resume -boundaries. Game audio MUST pause with the Game VM in v1. Independent background -audio policy is out of scope. - -Render ownership MUST move with the foreground owner. Foreground transitions -MUST invalidate stale render submissions through ownership epoch/generation so -old Game frames cannot be presented after Shell takes foreground, and old Shell -frames cannot be presented after the Game resumes. - -On the desktop host, `Esc` MUST be the primary keyboard request for Home/SystemOS. -The physical `Home` key MAY be supported as an alias. This Home request is a -host/SystemOS control action and MUST NOT be exposed as a normal guest pad -button or userland input capability. - -Real background execution, multiple resident Games, multiple foreground Shells, -Game-to-Game switching, Shell app persistence policy, memory eviction policy, -and rich multitasking are out of scope for this decision. - -## Rationale - -The chosen model preserves the expected console-like flow: a Game can be -paused, Home can run, Shell apps can open, and the same Game can resume without -turning the runtime into a general multitasking scheduler. - -Separating `Paused` from `Suspended` keeps authority clear. `Paused` is the -contract the Game can observe and react to. `Suspended` is the OS mechanism that -prevents the paused Game from continuing to consume ticks, input, audio, and -render ownership while another owner is foreground. - -Keeping Hub as root/back-stack prevents VM-backed Shell apps from becoming -independent lifecycle roots. Returning to Hub before resuming Game keeps v1 -navigation deterministic and leaves more complex direct-return behavior for a -future decision. - -Keeping Hub/Shell visible until the Game submits a new valid frame avoids -presenting stale pixels from the previous Game epoch. This aligns lifecycle -state with the render ownership model from `DSC-0040`. - -Using `Esc` as desktop Home makes the control available on compact keyboards. -Supporting the physical `Home` key as an alias is acceptable, but the portable -contract cannot depend on it. - -## Invariantes / Contrato - -- There is exactly one foreground visual owner at a time. -- V1 supports at most one resident Game. -- V1 supports at most one foreground Shell app. -- Hub/Home is the root Shell owner and back-stack controller. -- VM-backed Shell apps follow Shell lifecycle, not Game loop lifecycle. -- A paused Game may remain resident, but a suspended Game does not receive - normal gameplay ticks, normal game input, or frame pacing. -- Pause/resume are cooperative Game-visible lifecycle events. -- Suspend/resume scheduling authority belongs to SystemOS. -- SystemOS may suspend a Game after the pause budget even if the Game does not - cooperate. -- Input is cleared or barriered across Game pause/resume boundaries. -- Game audio pauses with the Game VM in v1. -- Foreground transitions advance render ownership epoch/generation. -- Visual foreground returns to Game only after a valid current-epoch Game render - submission exists. -- Desktop `Esc` is a host/SystemOS Home request; physical `Home` may alias it. -- Home/SystemOS is not a guest pad button and is not userland input. -- Background execution and general multitasking are explicitly excluded. - -## Impactos - -- **Spec:** runtime lifecycle, firmware/Hub, input, and render ownership specs - must document foreground ownership, pause/suspend semantics, Home key mapping, - and stale frame invalidation. -- **Runtime / SystemOS:** SystemOS needs explicit foreground owner, Hub root, - Shell foreground, Game paused, and Game suspended state. -- **VM:** VM execution must support pause/resume lifecycle delivery and OS-owned - suspension of ticks. -- **Firmware:** firmware should observe macro transitions such as GameRunning, - Hub/Home, Shell foreground, pause request, and resume request without owning - every internal OS detail. -- **Host:** host must route desktop `Esc` and optional physical `Home` to a - SystemOS Home request instead of guest pad input. -- **Input:** Game input must be drained or barriered during pause/resume and - Home/SystemOS must stay outside the guest-visible pad ABI. -- **Audio:** v1 audio follows Game pause/suspend and does not continue as - independent background audio. -- **Renderer:** render publication must reject stale submissions after ownership - epoch changes and keep previous foreground visible until the resumed Game - publishes a valid current-epoch frame. -- **Tests:** coverage must prove pause budget behavior, non-foreground VM - non-execution, input clearing, host Home routing, Shell return-to-Hub, and - stale render rejection. - -## Alternativas Descartadas - -- **Terminate Game when entering Shell:** rejected because it makes - `Game -> Shell -> Game` a reload rather than a pause/resume flow. -- **Real background execution now:** rejected because it pulls in scheduler, - budget, energy, audio, input, IO, memory, and persistence policy beyond v1. -- **Multiple resident Games:** rejected for v1 because Game-to-Game switching is - owned by `DSC-0043 / AGD-0044`. -- **Expose Home as guest input:** rejected because Home changes system - foreground authority and must not be handled as ordinary game input. -- **Return directly from Shell app to Game in v1:** rejected to keep Hub as the - deterministic root/back-stack owner. - -## Referencias - -- Source agenda: `AGD-0041` -- Runtime mode separation: `DSC-0031` -- SystemOS lifecycle authority: `DSC-0032` -- Shell task/window lifecycle: `DSC-0035` -- Hub UI direction: `DSC-0036` -- Render ownership and stale submission handling: `DSC-0040` -- Future Game A -> Home -> Game B switching: `DSC-0043 / AGD-0044` - -## Propagacao Necessaria - -- Update canonical runtime specs for foreground ownership and pause/suspend. -- Specify desktop Home request mapping in host/input documentation. -- Add plan(s) for SystemOS foreground stack state and Game pause/suspend. -- Add plan(s) for VM lifecycle event delivery and forced suspension budget. -- Add plan(s) for host `Esc`/`Home` routing outside guest pad input. -- Add plan(s) for render epoch integration during Game/Shell transitions. -- Add tests covering lifecycle, input, host routing, and render publication - invariants. - -## Revision Log - -- 2026-07-03: Initial decision from `AGD-0041`. diff --git a/discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md b/discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md deleted file mode 100644 index f923823c..00000000 --- a/discussion/workflow/decisions/DEC-0038-vm-session-ownership-for-vm-backed-processes.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -id: DEC-0038 -ticket: foreground-stack-game-pause-shell-vm-backed -title: VM Session Ownership for VM-Backed Processes -status: accepted -created: 2026-07-04 -ref_agenda: AGD-0046 -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Status - -Open for review. - -## Contexto - -`DEC-0037` established the foreground stack and Game pause contract for -`Game -> Home/Shell -> same Game`. It requires a suspended resident Game to stop -receiving normal gameplay ticks, input, and frame pacing while remaining -resident for later resume. - -The current implementation works for native Shell apps because native Shell -apps do not use the VM. It does not fully support VM-backed Shell coexistence: -`Firmware` owns a single global `VirtualMachine` (`Firmware.vm`), so loading a -VM-backed Shell can overwrite the suspended Game context that should remain -resident. - -`AGD-0046` closes this gap by separating VM execution context ownership from -foreground ownership. A foreground owner decides who receives visual/input -authority. A VM execution context decides where PC, stack, heap, globals, -handles, cartridge identity, lifecycle delivery state, and VM-scoped runtime -state live. - -## Decisao - -PROMETEU MUST make VM execution context session-owned for VM-backed processes. - -Every VM-backed process MUST have its own `VmSession` or equivalent -session-owned context. A VM-backed Game and a VM-backed Shell MUST NOT share the -same mutable `VirtualMachine` instance, stack, heap, PC, globals, open handles, -cartridge identity, or VM-scoped lifecycle state. - -`Firmware.vm` MUST NOT remain the canonical owner of all VM-backed app context. -Firmware MAY continue to orchestrate macro states such as loading, Home, Shell, -and GameRunning, but it MUST delegate VM session creation, activation, lookup, -and ticking to SystemOS/session services. - -For v1, the system MAY still enforce: - -- at most one resident Game; -- at most one foreground Shell; -- no Game-to-Game switching; -- no general background execution. - -Those restrictions MUST NOT be implemented by relying on a single global VM -context. They must be expressed as lifecycle/session policy. - -Each VM-backed Shell MUST receive its own VM session. If future Shell -persistence allows more than one Shell app to remain in memory, each retained -VM-backed Shell MUST still have distinct mutable VM context. - -A suspended resident Game MUST preserve its VM session. Suspension means the -session is not eligible for normal foreground ticks, normal Game input, or -normal frame pacing. It does not mean serializing, deleting, or overwriting the -VM context. - -Foreground ownership and execution eligibility MUST be separate concepts: - -- foreground ownership controls visual owner, primary input, render ownership, - and normal foreground frame pacing; -- execution eligibility controls which sessions may receive ticks or restricted - callbacks while not foreground. - -Background execution, including music players or background services, is out of -scope for the first implementation of this decision. However, the session model -MUST be compatible with adding background-capable scheduling soon. The initial -model MUST NOT bake in assumptions that only the foreground session can ever be -eligible to execute. - -Render ownership MUST remain a global SystemOS authority. VM sessions may -produce render submissions, but they MUST NOT decide whether those submissions -can be presented. SystemOS/render ownership policy remains responsible for -active owner, epoch/generation validity, stale submission rejection, and final -publication authority. - -`LoadCartridgeStep` MUST stop initializing a global firmware VM directly as the -long-term model. Cartridge load/activation MUST be delegated to a sessions or -VM-session facade that can create a new VM session, activate an existing VM -session, or reject an unsupported transition according to lifecycle policy. - -## Rationale - -The foreground stack is already task/process-oriented. VM execution context must -follow the same ownership model. Otherwise the system can mark a Game as -resident and suspended while the only real VM context has already been reused -for a Shell. - -Session ownership solves the actual coexistence problem without requiring VM -snapshot/restore. A suspended Game is simply a live session that is not scheduled -for normal ticks. A VM-backed Shell is another session with its own mutable -state. - -Keeping background execution out of the first implementation avoids expanding -this work into a general scheduler. At the same time, making execution -eligibility separate from foreground ownership prevents an immediate future -background audio/player feature from requiring another foundational refactor. - -Keeping render ownership global preserves the contract from `DEC-0037` and the -render ownership model from `DSC-0040`: sessions can generate frames, but only -the current foreground/render owner can publish valid current-epoch output. - -## Invariantes / Contrato - -- VM-backed mutable execution state is owned by a VM session, not by a global - firmware VM. -- A VM-backed Game and VM-backed Shell never share mutable VM context. -- A suspended resident Game retains its VM session. -- Suspension prevents normal foreground Game ticks/input/frame pacing; it does - not destroy or overwrite VM state. -- Native Shell apps do not require VM sessions. -- VM-backed Shell apps always require VM sessions. -- Each VM-backed Shell gets its own mutable VM session. -- Foreground ownership and execution eligibility are distinct. -- General background execution is out of scope for the initial implementation, - but the model must support adding it without replacing session ownership. -- SystemOS remains lifecycle and render ownership authority. -- Render ownership and stale frame rejection remain global policy, not per-VM - session policy. -- Cartridge load/activation must go through session services rather than - initializing one global firmware VM. -- V1 restrictions on one resident Game and one foreground Shell remain policy, - not a consequence of single-VM storage. - -## Impactos - -- **Runtime / SystemOS:** add a VM session model or equivalent service that - stores VM-backed process execution context and maps it to task/process - lifecycle. -- **Firmware:** remove `Firmware.vm` as the canonical VM context owner for all - apps. Firmware should request session creation/activation/tick from SystemOS. -- **VM:** support multiple distinct `VirtualMachine` instances or equivalent - isolated VM contexts in one OS lifetime. -- **Lifecycle:** task/process states must control session scheduling - eligibility without conflating foreground ownership with future background - execution. -- **Hub / Shell:** VM-backed Shell load must create or activate a Shell VM - session without overwriting a resident Game session. -- **Game:** Game pause/suspend/resume must preserve the Game VM session and - deliver lifecycle events to that session. -- **Renderer:** render manager stays global and validates ownership/epoch for - submissions produced by sessions. -- **FS / Memcard / Assets:** mutable handles and per-app runtime handles must - be evaluated for session scoping. Durable memcard/app data can remain keyed by - `app_id`; open handles and staging state should not accidentally leak across - sessions. -- **Tests:** add coverage proving VM-backed Shell creation does not overwrite a - suspended Game session, and that only eligible sessions tick. - -## Alternativas Descartadas - -- **Keep one global firmware VM and block VM-backed Shell when a Game is - resident:** rejected because it leaves `DEC-0037` incomplete and creates - divergent Shell behavior. -- **Serialize/snapshot the Game VM on suspension:** rejected because snapshot - correctness is harder than needed for v1 and would include VM, handles, - render state, assets, and side effects. -- **Hard-code two VM slots (`game_vm`, `shell_vm`):** rejected as a permanent - model because it encodes a short-term limit as architecture. It may only be - acceptable as an internal stepping stone if represented through the same - session abstraction. -- **Treat background-capable apps as hidden foreground owners:** rejected - because it would violate foreground ownership semantics and make render/input - authority ambiguous. - -## Referencias - -- Source agenda: `AGD-0046` -- Foreground stack and Game pause contract: `DEC-0037` -- Runtime mode separation: `DSC-0031` -- SystemOS lifecycle/process/task authority: `DSC-0032` -- VM render parallel execution boundary: `DSC-0040` -- Future Game-to-Game switching: `DSC-0043` - -## Propagacao Necessaria - -- Add implementation plans for introducing VM sessions and moving VM context - out of `Firmware`. -- Add implementation plans for separating VM-scoped runtime state from global - SystemOS/render state. -- Update runtime/firmware specs to describe session-owned VM context and the - distinction between foreground ownership and execution eligibility. -- Update tests for Game resident suspension, VM-backed Shell creation, and - session isolation. -- Keep future background execution as out of scope for first implementation, - but document that the model must support background-capable scheduling. - -## Revision Log - -- 2026-07-04: Initial decision draft from `AGD-0046`. diff --git a/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md b/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md deleted file mode 100644 index aaeb9fc2..00000000 --- a/discussion/workflow/plans/PLN-0136-route-desktop-home-key-outside-guest-input.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -id: PLN-0136 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Route Desktop Home Key Outside Guest Input -status: done -created: 2026-07-03 -ref_decisions: [DEC-0037] -tags: [host, input, systemos, keyboard, lifecycle] ---- - -## Briefing - -`DEC-0037` makes desktop `Esc` the primary Home/SystemOS request and allows the -physical `Home` key as an alias. This request must be intercepted by the -host/SystemOS and must not appear as a guest pad button or input intrinsic. - -This plan adds the host input route for the Home request without changing the -guest-visible pad surface. - -## Decisions of Origin - -- `DEC-0037` - Foreground Stack and Game Pause Contract. - -## Target - -Route desktop `Esc` and optional physical `Home` key to a SystemOS Home request -outside `InputSignals`. - -## Scope - -- Extend `crates/host/prometeu-host-desktop-winit/src/input.rs` or a nearby - host input module with a host/system control signal separate from - `prometeu_hal::InputSignals`. -- Map `KeyCode::Escape` to Home/SystemOS request. -- Map `KeyCode::Home` as an alias. -- Ensure neither key mutates `start_signal`, `select_signal`, or any pad field. -- Route the request through `crates/host/prometeu-host-desktop-winit/src/runner.rs` - into firmware/SystemOS. -- Add tests proving `Esc` and `Home` do not affect guest pad state. - -## Out of Scope - -- No pause/resume lifecycle implementation. -- No render ownership gating. -- No guest-visible Home button. -- No gamepad hardware Home mapping unless a future plan defines controller - system controls separately. - -## Execution Plan - -1. Define host control state. - Add a small host-only type such as `HostSystemControls` with a - `home_requested` edge or event. Store it in `HostInputHandler` or in the - runner next to the existing input handler. - -2. Update keyboard handling. - In `HostInputHandler::handle_event`, recognize `KeyCode::Escape` and - `KeyCode::Home` on press. Set the host control event and return without - modifying `InputSignals`. - -3. Preserve pad mapping. - Keep existing pad mappings unchanged: - arrows, `A/D/W/S`, `Q/E`, `Z`, and Shift. Do not reuse `Start` or `Select` - for Home. - -4. Route to firmware/SystemOS. - In `crates/host/prometeu-host-desktop-winit/src/runner.rs`, consume the - host Home event before or during `about_to_wait` and call the firmware or - SystemOS method introduced by `PLN-0137` / `PLN-0140`. - -5. Add tests. - Add host input tests for `Esc`, physical `Home`, and existing pad mappings. - Tests must prove Home produces a system event and no guest pad mutation. - -## Acceptance Criteria - -- Pressing desktop `Esc` emits a Home/SystemOS host control event. -- Pressing physical `Home` emits the same event. -- Neither key affects `InputSignals`. -- Existing guest pad keyboard mappings still work. -- The runner consumes the Home request as a system action. -- The implementation does not add a guest syscall, intrinsic, or pad button. - -## Tests / Validation - -- Add unit tests in `prometeu-host-desktop-winit` for host input mapping. -- Run `cargo test -p prometeu-host-desktop-winit input`. -- Run `cargo test -p prometeu-hal input` if pad signal behavior changes. -- Run `discussion validate`. - -## Risks - -- `Esc` may conflict with debugger or window-close expectations. Keep debugger - start handling on `D` unchanged and do not map `Esc` to process exit. -- Physical `Home` is not available on every keyboard, so it must remain an - alias rather than the primary portable mapping. diff --git a/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md b/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md deleted file mode 100644 index 51c1e4b8..00000000 --- a/discussion/workflow/plans/PLN-0137-implement-systemos-foreground-stack-state.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -id: PLN-0137 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Implement SystemOS Foreground Stack State -status: done -created: 2026-07-03 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, foreground, shell, game] ---- - -## Briefing - -`DEC-0037` requires SystemOS to own foreground authority. The system must know -which owner is foreground, keep Hub/Home as root, allow only one resident Game, -allow only one foreground Shell, and return from VM-backed Shell apps to Hub in -v1. - -This plan implements the SystemOS state model only. It does not yet deliver -Game pause/resume events, host keyboard routing, or render/audio/input boundary -changes. - -## Decisions of Origin - -- `DEC-0037` - Foreground Stack and Game Pause Contract. - -## Target - -Add an explicit SystemOS foreground stack model that can represent Hub, a -foreground Shell task, and a paused/suspended resident Game. - -## Scope - -- Add or extend SystemOS-owned foreground state under - `crates/console/prometeu-system/src/os/` or - `crates/console/prometeu-system/src/services/`. -- Represent v1 foreground owners: - - Hub/Home root; - - Shell task foreground; - - Game foreground; - - Game paused/suspended resident. -- Enforce at most one resident Game and at most one foreground Shell. -- Keep Hub as root/back-stack controller. -- Ensure Shell close returns to Hub/Home in v1. -- Expose focused queries through existing or new facades, likely - `crates/console/prometeu-system/src/os/facades/lifecycle.rs` and - `crates/console/prometeu-system/src/os/facades/sessions.rs`. - -## Out of Scope - -- No VM pause/resume event delivery. -- No host `Esc`/`Home` keyboard mapping. -- No render epoch implementation. -- No Game-to-Game switching. -- No background execution. -- No memory eviction for the resident Game. - -## Execution Plan - -1. Define foreground model types. - Add enums/structs for foreground owner and resident Game state in the most - local SystemOS module. The model must distinguish Game foreground, Game - paused/suspended, Hub foreground, and Shell task foreground. - -2. Add SystemOS storage. - Store the foreground model in `SystemOS` from - `crates/console/prometeu-system/src/os/system_os.rs` with initialization to - Hub/Home root. - -3. Wire lifecycle facade operations. - Extend lifecycle/session facades so firmware can request: - - set Game foreground; - - request Home from Game; - - set Shell foreground from Hub; - - close Shell and return to Hub; - - query current foreground owner; - - query resident Game task. - -4. Enforce cardinality. - Add checks that reject a second resident Game and a second foreground Shell - with typed lifecycle errors. Use existing `LifecycleError` style or add - precise variants. - -5. Integrate existing Shell close behavior. - Update `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` - only where needed so closing a Shell task routes through the foreground model - and ends in `FirmwareState::HubHome`. - -6. Add tests. - Cover initial Hub foreground, Game foreground registration, Home request - moving Game to resident paused/suspended state, Shell foreground activation, - Shell close returning to Hub, and cardinality rejection. - -## Acceptance Criteria - -- SystemOS has an explicit foreground owner model. -- Hub/Home is the initialized root foreground owner. -- Only one resident Game can exist in v1. -- Only one Shell task can be foreground in v1. -- VM-backed Shell close returns to Hub/Home. -- Foreground queries are available to firmware and later plans. -- No implementation path enables background execution. - -## Tests / Validation - -- Add focused unit tests in `prometeu-system` around the new model and facade. -- Run `cargo test -p prometeu-system lifecycle foreground`. -- Run `cargo test -p prometeu-firmware shell`. -- Run `discussion validate`. - -## Risks - -- Existing `TaskState::Foreground` and `TaskState::Suspended` may look like the - complete model. Do not overload them until foreground ownership and Hub root - authority are explicit. -- Shell windows already have focus semantics. Keep window focus as visual/task - evidence, not the sole source of SystemOS foreground truth. diff --git a/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md b/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md deleted file mode 100644 index 6065559f..00000000 --- a/discussion/workflow/plans/PLN-0138-specify-foreground-pause-and-home-contract.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -id: PLN-0138 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Specify Foreground Pause and Home Contract -status: done -created: 2026-07-03 -ref_decisions: [DEC-0037] -tags: [runtime, specs, lifecycle, input, renderer, foreground] ---- - -## Briefing - -`DEC-0037` establishes the v1 foreground stack contract: one foreground visual -owner, one resident Game, one foreground Shell, Hub as root, Game-visible -pause/resume, OS-owned suspension, desktop `Esc` as Home/SystemOS request, and -render epoch invalidation across foreground transitions. - -This plan updates the canonical runtime specs before implementation so code -plans cannot drift into guest-owned Home input, background execution, or stale -frame presentation. - -## Decisions of Origin - -- `DEC-0037` - Foreground Stack and Game Pause Contract. - -## Target - -Document the foreground, pause/suspend, Home request, input, audio, and render -ownership contract in the canonical runtime specs. - -## Scope - -- Update `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` with: - - Hub as root/special Shell owner; - - one foreground visual owner; - - one resident Game; - - one foreground Shell; - - `Game -> Home/Shell -> same Game` lifecycle; - - VM-backed Shell app return-to-Hub behavior. -- Update `docs/specs/runtime/06-input-peripheral.md` with: - - Home/SystemOS as host/system control, not pad input; - - desktop `Esc` as primary Home request; - - physical `Home` key as optional alias; - - input clear/barrier requirements across Game pause/resume. -- Update `docs/specs/runtime/09-events-and-concurrency.md` with: - - pause notification budget; - - OS-owned suspension after the budget; - - resume/foreground-restore notification before visual return. -- Update `docs/specs/runtime/14-boot-profiles.md` only to clarify that this - foreground contract is not Game-to-Game switching and does not change direct - `--run` or `--games-root` boot semantics. -- Update renderer-related spec text in the most local existing runtime spec if - present, otherwise add a short subsection to `12-firmware-pos-and-prometeuhub.md` - describing ownership epoch invalidation and first-valid-frame gating. - -## Out of Scope - -- No Rust implementation. -- No new guest ABI or pad button. -- No background execution policy. -- No Game A -> Home -> Game B switching. -- No memory eviction policy for resident Games. - -## Execution Plan - -1. Edit `12-firmware-pos-and-prometeuhub.md`. - Define foreground owner cardinality, Hub root ownership, Shell foreground - behavior, Game resident/suspended behavior, return-to-Hub from Shell, and the - requirement to keep Hub/Shell visible until a resumed Game submits a valid - current-epoch frame. - -2. Edit `06-input-peripheral.md`. - State that Home/SystemOS is outside the guest-visible pad and input - intrinsic surface. Document desktop `Esc` as the primary host mapping and - physical `Home` as an optional alias. - -3. Edit `09-events-and-concurrency.md`. - Add the pause/resume event ordering: Home request, pause notification, - bounded reaction budget, OS suspension, resume/foreground-restore - notification, first-valid-frame visual return. - -4. Edit `14-boot-profiles.md`. - Cross-reference the foreground contract and explicitly keep direct boot, - games-root Home launch, and future Game-to-Game switching separate. - -5. Run text checks. - Search for wording that implies Home is a guest input button, a guest ABI, or - background execution. Replace misleading text with the `DEC-0037` contract. - -## Acceptance Criteria - -- Specs define `Paused` as Game-visible and `Suspended` as OS-owned. -- Specs state that desktop `Esc` is the primary Home request and is not guest - pad input. -- Specs state that the Game receives pause/resume notifications but does not - control OS suspension. -- Specs state that Hub is the root Shell owner and Shell app close returns to - Hub in v1. -- Specs state that visual return to Game waits for a valid current-epoch render - submission. -- Specs explicitly exclude background execution and Game-to-Game switching. - -## Tests / Validation - -- Run `discussion validate`. -- Run `rg -n "Home|Esc|Suspended|Paused|foreground|background execution" docs/specs/runtime`. -- Verify no edited spec describes Home/SystemOS as a guest-visible pad button. - -## Risks - -- The spec could accidentally reopen Game-to-Game switching; keep that scoped to - `DSC-0043 / AGD-0044`. -- The input spec could confuse physical keyboard mapping with VM input; keep - host/system controls outside the guest input surface. diff --git a/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md b/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md deleted file mode 100644 index 9df5d7fb..00000000 --- a/discussion/workflow/plans/PLN-0139-validate-game-home-shell-game-end-to-end.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -id: PLN-0139 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Validate Game Home Shell Game End To End -status: done -created: 2026-07-03 -ref_decisions: [DEC-0037] -tags: [runtime, validation, firmware, host, lifecycle] ---- - -## Briefing - -`DEC-0037` is not complete until the product flow is proven end to end: -a Game runs, the user requests Home, the Game is paused/suspended, Hub or a -VM-backed Shell takes foreground, closing Shell returns to Hub, and the same -Game can resume only after a valid current-epoch frame exists. - -This plan adds final validation after the spec, SystemOS, VM, host input, and -render/audio/input boundary plans are implemented. - -## Decisions of Origin - -- `DEC-0037` - Foreground Stack and Game Pause Contract. - -## Target - -Prove the complete `Game -> Home/Shell -> same Game` lifecycle and prevent -regressions around guest input, foreground authority, and stale rendering. - -## Scope - -- Add integration-style tests for: - - Game running to Home request; - - pause notification and forced suspension; - - Hub foreground while Game is resident; - - VM-backed Shell foreground while Game is suspended; - - Shell close returning to Hub; - - resume request returning to same Game; - - first-valid-frame visual restore; - - host `Esc`/`Home` not entering guest input. -- Add manual smoke instructions for desktop host. -- Confirm direct `--run` and games-root Home launch remain separate. - -## Out of Scope - -- No new implementation beyond tests and developer-facing validation notes. -- No Game A -> Home -> Game B switching. -- No performance tuning. -- No polished UI. - -## Execution Plan - -1. Add firmware/SystemOS integration tests. - Use the most stable existing seams in `crates/console/prometeu-firmware/src/firmware/firmware.rs` - and `crates/console/prometeu-system` tests to drive Game foreground, Home - request, Shell foreground, Shell close, and Game resume. - -2. Add host input integration tests. - Ensure `Esc` and physical `Home` create a host Home event and do not mutate - guest `InputSignals`. - -3. Add render publication validation. - Assert stale submissions from the pre-pause Game epoch are rejected after - Home takes foreground and that the Game is not visually restored until a new - current-epoch submission exists. - -4. Add audio/input boundary validation. - Assert Game input is cleared or barriered across pause/resume. Assert Game - audio pause follows Game suspension. - -5. Add direct boot and games-root regression checks. - Ensure direct `--run ` remains independent from Home request handling - and that games-root Home launch still enters the Game path. - -6. Document manual smoke commands. - Add or update developer-facing spec text with release-mode commands: - - `cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console` - - `cargo run --release -p prometeu-host-desktop-winit -- --games-root test-cartridges` - Include the expected `Esc -> Home -> Shell -> Hub -> resume Game` behavior - once implemented. - -## Acceptance Criteria - -- Automated tests cover `Game -> Home -> same Game`. -- Automated tests cover `Game -> Home -> Shell -> Hub -> same Game`. -- Tests prove Home request is host/SystemOS control, not guest pad input. -- Tests prove suspended Game does not tick. -- Tests prove stale render submissions are not presented. -- Tests prove visual return waits for a current-epoch Game frame. -- Direct `--run` and games-root Home launch regressions pass. - -## Tests / Validation - -- Run `cargo test -p prometeu-system`. -- Run `cargo test -p prometeu-firmware`. -- Run `cargo test -p prometeu-host-desktop-winit`. -- Run any targeted render worker tests affected by ownership epoch changes. -- Run `discussion validate`. - -## Risks - -- The current test seams may not expose enough host-driven lifecycle behavior. - If needed, add narrow test-only helpers rather than testing through sleeps or - real-time window loops. -- End-to-end validation can become flaky if it depends on wall-clock timing. - Use deterministic ticks and explicit state transitions. diff --git a/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md b/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md deleted file mode 100644 index e7ff2832..00000000 --- a/discussion/workflow/plans/PLN-0140-deliver-game-pause-resume-and-suspension.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -id: PLN-0140 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Deliver Game Pause Resume and Suspension -status: done -created: 2026-07-03 -ref_decisions: [DEC-0037] -tags: [runtime, os, vm, lifecycle, pause, suspension] ---- - -## Briefing - -`DEC-0037` separates Game-visible `Paused` from OS-owned `Suspended`. When Home -is requested during Game execution, SystemOS must notify the Game, give it a -short bounded budget, then suspend the VM even if the Game does not cooperate. -When returning, SystemOS must reactivate the VM and send a resume or -foreground-restore notification before the visual owner returns to Game. - -This plan implements the pause/resume and suspension mechanics after the -foreground state model exists. - -## Decisions of Origin - -- `DEC-0037` - Foreground Stack and Game Pause Contract. - -## Target - -Add Game pause/resume lifecycle delivery and OS-enforced VM suspension for the -same resident Game. - -## Scope - -- Add a Game lifecycle event model for pause and resume/foreground-restore. -- Add a bounded pause reaction budget owned by SystemOS. -- Suspend the Game VM after the budget even if the Game does not cooperate. -- Ensure a suspended Game receives no normal gameplay ticks. -- Reactivate the VM and deliver resume/foreground-restore before Game visual - foreground restoration. -- Integrate with `GameRunningStep` and VM runtime tick control. - -## Out of Scope - -- No render first-valid-frame gating; that is in `PLN-0141`. -- No host keyboard routing; that is in `PLN-0136`. -- No new guest syscall. -- No background execution. -- No Game-to-Game switching. - -## Execution Plan - -1. Add lifecycle event representation. - Define pause/resume events in the most local runtime lifecycle module, - likely under `crates/console/prometeu-system/src/os/lifecycle.rs` or a new - SystemOS lifecycle submodule. Events must be observable by the VM/runtime - path without giving the Game authority over suspension. - -2. Add pause transition state. - Extend the foreground state from `PLN-0137` with a `PauseRequested` or - equivalent transition state that records the target Game task and the - remaining pause budget. - -3. Gate VM ticks. - Update `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` - and `crates/console/prometeu-system/src/services/vm_runtime/tick.rs` so a - suspended Game does not receive normal ticks, normal input, or frame pacing. - -4. Deliver pause notification. - Add the mechanism that makes the pause event visible to the running Game - before suspension. Use an existing VM event channel if present; otherwise add - a narrow lifecycle-event queue owned by SystemOS/VM runtime. - -5. Enforce the budget. - Advance the pause budget on firmware ticks. When it expires, transition the - Game task/process to suspended through SystemOS lifecycle regardless of Game - cooperation. - -6. Deliver resume notification. - On return to the resident Game, transition from suspended to resume-requested, - reactivate VM execution, deliver resume/foreground-restore, and leave visual - restoration to the render boundary plan. - -7. Add tests. - Cover cooperative pause, non-cooperative forced suspension, no tick while - suspended, resume notification delivery, and no Game-owned ability to cancel - OS suspension. - -## Acceptance Criteria - -- Game receives pause before suspension. -- SystemOS suspends after a bounded budget even if the Game does not cooperate. -- Suspended Game does not receive normal gameplay ticks. -- Resume/foreground-restore is delivered before visual return to Game. -- Game cannot prevent OS suspension through guest-visible behavior. -- Existing debugger pause behavior remains distinct from lifecycle suspension. - -## Tests / Validation - -- Add unit tests in `prometeu-system` for lifecycle transition state. -- Add firmware tests around `GameRunningStep` pause request and suspension. -- Add VM runtime tests proving suspended state suppresses normal ticks. -- Run `cargo test -p prometeu-system`. -- Run `cargo test -p prometeu-firmware`. -- Run `discussion validate`. - -## Risks - -- Debugger pause already exists in VM runtime. Keep debug pause separate from - lifecycle pause/suspend. -- A lifecycle event queue could become a general app event system. Keep this - plan limited to pause/resume required by `DEC-0037`. diff --git a/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md b/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md deleted file mode 100644 index b10f2d88..00000000 --- a/discussion/workflow/plans/PLN-0141-integrate-render-audio-and-input-pause-boundaries.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: PLN-0141 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Integrate Render Audio and Input Pause Boundaries -status: done -created: 2026-07-03 -ref_decisions: [DEC-0037] -tags: [runtime, renderer, audio, input, foreground, lifecycle] ---- - -## Briefing - -`DEC-0037` requires foreground transitions to invalidate stale render -submissions, keep Hub/Shell visible until the resumed Game publishes a valid -current-epoch frame, clear or barrier Game input across pause/resume, and pause -Game audio with the Game VM in v1. - -This plan integrates those boundary effects after SystemOS foreground state and -Game pause/resume mechanics exist. - -## Decisions of Origin - -- `DEC-0037` - Foreground Stack and Game Pause Contract. - -## Target - -Make foreground transitions safe at the render, input, and audio boundaries. - -## Scope - -- Advance render ownership epoch/generation whenever foreground owner changes. -- Discard stale Game frames after Shell/Hub takes foreground. -- Discard stale Shell frames after Game resumes. -- Keep Hub/Shell visible until a resumed Game publishes a valid current-epoch - submission. -- Clear or barrier Game input across pause/resume boundaries. -- Pause Game audio with the Game VM in v1. -- Add telemetry/tests for stale discards and first-valid-frame gating. - -## Out of Scope - -- No independent background audio policy. -- No overlay rendering policy. -- No Game-to-Game switch rendering. -- No full memory eviction behavior. - -## Execution Plan - -1. Extend render ownership transition calls. - Update `crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs` - and call sites in `tick.rs` so foreground owner changes explicitly advance - ownership epoch/generation, not only app mode/app id changes. - -2. Add foreground restore gating. - Add state in SystemOS or firmware that records when visual return to Game is - pending. The state must remain in Hub/Shell presentation until a - current-epoch Game submission is observed. - -3. Update host presentation selection. - Update `crates/host/prometeu-host-desktop-winit/src/runner.rs` if necessary - so `should_present_worker_frame` and fallback framebuffer presentation obey - the foreground restore gate. - -4. Add stale discard coverage. - Extend render manager and render worker tests to prove stale submissions - from the previous owner are discarded after epoch change. - -5. Add input boundary clearing. - Add a SystemOS or firmware input barrier when Game pause starts and when - Game resume starts. The barrier must prevent held or pressed Game input from - leaking across the lifecycle boundary. - -6. Add audio pause integration. - Ensure Game pause/suspend produces the existing host audio pause command path - or a new explicit lifecycle audio pause signal. Keep this separate from - debugger pause if the observable cause matters in tests. - -7. Add tests. - Cover stale frame discard, first-valid-frame visual return, input clearing, - and Game audio pause/resume behavior. - -## Acceptance Criteria - -- Foreground owner changes advance render ownership epoch/generation. -- Old Game frames cannot present after Hub/Shell takes foreground. -- Old Hub/Shell frames cannot present after Game resumes. -- Visual return to Game waits for a valid current-epoch Game submission. -- Game input does not leak across pause/resume boundaries. -- Game audio pauses with Game suspension in v1. - -## Tests / Validation - -- Run `cargo test -p prometeu-system render foreground`. -- Run `cargo test -p prometeu-firmware pause`. -- Run `cargo test -p prometeu-host-desktop-winit presentation`. -- Run `discussion validate`. - -## Risks - -- Existing render epoch logic may already key off app mode/app id. Foreground - owner changes must be explicit so Shell/Hub and resumed Game cannot share a - stale epoch by accident. -- Audio pause currently follows VM paused/debugger state in the host. Keep Game - lifecycle pause distinct enough that tests can prove the decision contract. diff --git a/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md b/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md deleted file mode 100644 index 72101b29..00000000 --- a/discussion/workflow/plans/PLN-0142-deliver-game-lifecycle-events-to-vm-runtime.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -id: PLN-0142 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Deliver Game Lifecycle Events To VM Runtime -status: done -created: 2026-07-04 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -`DEC-0037` requires pause/resume to be Game-visible lifecycle events. The -current implementation records `GameLifecycleEvent` values in SystemOS, but no -runtime or VM execution path consumes them. Tests currently prove that the OS -queue contains events, not that the Game/runtime can observe them before -suspension or after resume. - -## Objective - -Deliver Game lifecycle pause/resume events from SystemOS into the VM runtime -boundary in a deterministic, testable way, without exposing Home as guest pad -input or a userland syscall. - -## Dependencies - -- Source decision: `DEC-0037`. -- Fixes the lifecycle event delivery gap found after `PLN-0140` and - `PLN-0141`. -- `PLN-0143` depends on this plan if it needs to prove that the pause event is - consumed before the pause budget expires. - -## Scope - -Included: - -- Define the runtime-facing lifecycle event delivery API for `Pause` and - `ResumeForeground`. -- Move events from the SystemOS queue into the VM/runtime tick boundary. -- Make delivered events observable in tests without creating a broad app event - system. -- Preserve the distinction between lifecycle pause and debugger pause. -- Ensure events are delivered only to the resident Game task they target. - -Excluded: - -- No guest Home input, pad field, intrinsic, or syscall. -- No general userland event bus. -- No background execution while suspended. -- No new Shell lifecycle event model. - -## Non-Goals - -- Do not implement Game-to-Game switching. -- Do not change renderer ownership policy except where tests need to assert - lifecycle event ordering. -- Do not add wall-clock timers or asynchronous callbacks. - -## Execution Method - -1. Add a narrow VM runtime lifecycle inbox. - - What changes: introduce a small data structure for pending/delivered Game - lifecycle events in `VirtualMachineRuntime`. - - How it changes: add methods such as `queue_game_lifecycle_events`, - `take_delivered_game_lifecycle_events`, or a more idiomatic local name - that preserves deterministic frame-boundary delivery. - - File(s): `crates/console/prometeu-system/src/services/vm_runtime/mod.rs`, - `crates/console/prometeu-system/src/services/vm_runtime/tick.rs`, and - focused tests under `crates/console/prometeu-system/src/services/vm_runtime/`. - -2. Route SystemOS events into the VM facade before ticking. - - What changes: `VmFacade::tick` must drain lifecycle events from SystemOS - and pass only events targeted at the currently running resident Game into - the VM runtime lifecycle inbox. - - How it changes: use the existing `LifecycleFacade::take_game_lifecycle_events` - or replace it with an equivalent internal method if borrowing requires a - cleaner API. Do not leave events only observable through - `pending_game_lifecycle_events`. - - File(s): `crates/console/prometeu-system/src/os/facades/vm.rs`, - `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. - -3. Expose deterministic test visibility. - - What changes: tests must be able to assert that `Pause` and - `ResumeForeground` reached the VM runtime boundary. - - How it changes: add test-only or public diagnostic accessors on - `VmFacade`/`VirtualMachineRuntime` for delivered lifecycle events, scoped - to lifecycle validation rather than a guest ABI. - - File(s): `crates/console/prometeu-system/src/os/facades/vm.rs`, - `crates/console/prometeu-system/src/services/vm_runtime/tests.rs`. - -4. Update firmware tests to assert delivery, not only queue presence. - - What changes: existing firmware tests that inspect - `pending_game_lifecycle_events` should assert VM-runtime delivery at the - tick boundary. - - How it changes: after a Game pause/resume flow, verify that the VM/runtime - observed the lifecycle event in order. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. - -## Acceptance Criteria - -- [ ] `Pause` is moved from SystemOS into the VM runtime boundary before - suspension is allowed to complete. -- [ ] `ResumeForeground` is moved into the VM runtime boundary before the Game - is considered visually restored. -- [ ] Tests fail if events remain only in `SystemOS.game_lifecycle_events`. -- [ ] Lifecycle event delivery does not mutate `InputSignals`. -- [ ] Debugger pause remains separate from lifecycle pause. - -## Tests - -- Unit tests in `prometeu-system` proving VM runtime lifecycle event delivery - and drain semantics. -- Firmware tests proving Home request delivers `Pause` to the VM/runtime before - suspension. -- Firmware tests proving resident Game resume delivers `ResumeForeground` - before Game foreground restoration. -- Run `cargo test -p prometeu-system`. -- Run `cargo test -p prometeu-firmware`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/os/facades/vm.rs` -- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` -- `crates/console/prometeu-system/src/services/vm_runtime/mod.rs` -- `crates/console/prometeu-system/src/services/vm_runtime/tick.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md b/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md deleted file mode 100644 index 95252d59..00000000 --- a/discussion/workflow/plans/PLN-0143-guarantee-cooperative-pause-budget-tick-before-suspension.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -id: PLN-0143 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Guarantee Cooperative Pause Budget Tick Before Suspension -status: done -created: 2026-07-04 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -`DEC-0037` requires a Game-visible `Paused` event followed by a bounded -cooperative reaction budget before SystemOS imposes `Suspended`. The current -implementation uses a one-tick budget and advances it before `vm.tick` in -`GameRunningStep`, so the Game can be suspended without any runtime tick in -which it could observe or react to pause. - -## Objective - -Guarantee at least one deterministic VM/runtime delivery point for the Game to -observe pause before forced suspension, while preserving SystemOS authority to -suspend the Game after the budget. - -## Dependencies - -- Source decision: `DEC-0037`. -- Should be executed after or together with `PLN-0142`, because budget tests - must prove delivered lifecycle events, not only queued OS events. - -## Scope - -Included: - -- Fix pause budget ordering so the pause budget cannot expire before the VM - runtime has a delivery point for `Pause`. -- Define the budget in deterministic machine ticks/frames, not wall-clock time. -- Ensure a Game receives no normal gameplay ticks after it reaches - `PausedSuspended`. -- Preserve the existing forced-suspension authority when the budget expires. -- Update tests that currently assert no VM tick occurs immediately after Home. - -Excluded: - -- No guest-controlled cancellation of suspension. -- No background execution after suspension. -- No extension of the pause budget based on host timing. -- No direct visual-return behavior; render restore gating remains covered by - the existing render boundary work. - -## Non-Goals - -- Do not add a general scheduler. -- Do not make pause cooperation mandatory for suspension. -- Do not conflate lifecycle pause with debugger pause. - -## Execution Method - -1. Redefine pause budget state transitions. - - What changes: `ResidentGameState::PauseRequested` must represent a state - in which one or more delivery ticks remain before `PausedSuspended`. - - How it changes: adjust `ForegroundStack::advance_pause_budget` or split it - into explicit methods such as `mark_pause_delivered` and - `advance_pause_budget_after_delivery`. - - File(s): `crates/console/prometeu-system/src/services/foreground.rs`. - -2. Move budget advancement after lifecycle delivery. - - What changes: `GameRunningStep::on_update` must not suspend before the VM - runtime has a chance to receive and process the pause lifecycle event. - - How it changes: reorder the GameRunning update so the event delivery path - runs first, then the budget is advanced, and only a subsequent eligible - boundary can force suspension. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs`, - `crates/console/prometeu-system/src/os/facades/vm.rs`. - -3. Preserve no-tick-after-suspended behavior. - - What changes: once `ResidentGameState::PausedSuspended` and - `TaskState::Suspended` are reached, the Game must not receive normal ticks, - normal input, or frame pacing. - - How it changes: keep the existing early transition to Hub after suspension - expires, but ensure it occurs after the cooperative pause window rather - than before it. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs`. - -4. Update tests to match the corrected semantics. - - What changes: tests must stop asserting that the first tick after Home does - zero VM work purely because Home was requested. - - How it changes: assert that the pause event was delivered during the - cooperative window, then assert that a later deterministic tick forces - suspension and no further Game ticks occur. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`, - `crates/console/prometeu-system/src/os/system_os.rs`. - -## Acceptance Criteria - -- [ ] A Game receives a deterministic pause delivery point before SystemOS - suspends it. -- [ ] SystemOS still suspends the Game after the bounded budget even if the - Game does not cooperate. -- [ ] A suspended Game receives no normal gameplay ticks, normal Game input, or - frame pacing. -- [ ] Tests prove the order: Home request -> pause delivery -> budget expiry -> - suspension. -- [ ] Debugger pause behavior is unchanged. - -## Tests - -- Unit tests in `prometeu-system` for pause budget transitions. -- Firmware tests for non-cooperative forced suspension after at least one - delivered pause boundary. -- Regression tests proving suspended Game tick count does not advance. -- Run `cargo test -p prometeu-system foreground lifecycle`. -- Run `cargo test -p prometeu-firmware pause`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/foreground.rs` -- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` -- `crates/console/prometeu-system/src/os/facades/vm.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md b/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md deleted file mode 100644 index 1bec2807..00000000 --- a/discussion/workflow/plans/PLN-0144-suspend-resident-game-when-shell-takes-foreground.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -id: PLN-0144 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Suspend Resident Game When Shell Takes Foreground -status: done -created: 2026-07-04 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -`DEC-0037` says a suspended resident Game must not receive normal gameplay -ticks, normal Game input, or frame pacing while Hub/Shell owns foreground. The -current `set_shell_foreground` path can mark the resident Game as -`PausedSuspended` in the foreground stack while `TaskManager::set_foreground` -only demotes the previous foreground Game task to `Background`. This creates a -state mismatch if a Shell is opened while the Game has not already gone through -the Home suspension path. - -## Objective - -Make Shell foreground acquisition either require an already suspended resident -Game or perform the necessary OS-owned suspension consistently across -foreground stack, task manager, and process manager. - -## Dependencies - -- Source decision: `DEC-0037`. -- Can be implemented independently of `PLN-0142` and `PLN-0143`, but tests - should be rerun with those fixes if they are executed first. - -## Scope - -Included: - -- Close the mismatch between `ResidentGameState::PausedSuspended` and - `TaskState::Background`. -- Ensure Shell foreground cannot leave a resident Game running or backgrounded. -- Cover both `create_vm_shell_task` and `create_native_shell_task` paths. -- Preserve the rule that only one foreground Shell may exist. -- Keep the normal Home -> Shell flow working. - -Excluded: - -- No direct Shell -> Game return behavior; v1 still returns Shell close to Hub. -- No background Game execution. -- No multiple resident Games. -- No Shell persistence policy changes. - -## Non-Goals - -- Do not broaden Shell lifecycle semantics. -- Do not implement Game-to-Game switching. -- Do not add UI controls for resume. - -## Execution Method - -1. Define the legal Shell foreground precondition. - - What changes: decide in code whether Shell foreground requires the - resident Game to already be `PausedSuspended`, or whether - `set_shell_foreground_task` is responsible for suspending it. - - How it changes: prefer centralizing the invariant in - `LifecycleFacade::set_shell_foreground_task`, because it owns task/process - transitions. - - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`, - `crates/console/prometeu-system/src/services/foreground.rs`. - -2. Synchronize task/process state when Shell takes foreground. - - What changes: if a resident Game exists and is not already suspended, - mark the Game task and process `Suspended` before or during Shell - foreground transition. - - How it changes: use `process_id_for_task` for the resident Game and call - `task_manager.mark_suspended` and `process_manager.mark_suspended` in the - same lifecycle operation that sets Shell foreground. - - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. - -3. Tighten foreground stack behavior. - - What changes: `ForegroundStack::set_shell_foreground` must not silently - hide invalid state transitions. - - How it changes: either reject Shell foreground when the resident Game is - still `Foreground`/`PauseRequested`, or return enough information for the - lifecycle facade to suspend the resident Game explicitly. - - File(s): `crates/console/prometeu-system/src/services/foreground.rs`. - -4. Add regression tests for reachable session paths. - - What changes: create tests that call `create_vm_shell_task` and - `create_native_shell_task` while a Game task is foreground. - - How it changes: assert foreground owner is Shell, resident Game remains - recorded, Game task state is `Suspended`, and Game process state is - `Suspended`. - - File(s): `crates/console/prometeu-system/src/os/system_os.rs`, - `crates/console/prometeu-firmware/src/firmware/firmware.rs` if the - firmware Home/Shell path needs coverage. - -## Acceptance Criteria - -- [ ] A Shell foreground transition cannot leave the resident Game in - `TaskState::Background`. -- [ ] Foreground stack state, task state, and process state agree that the - resident Game is suspended while Shell is foreground. -- [ ] `create_vm_shell_task` and `create_native_shell_task` both preserve the - invariant. -- [ ] Opening a second foreground Shell remains rejected. -- [ ] Closing Shell still returns to Hub/Home, not directly to Game. - -## Tests - -- Unit tests in `prometeu-system` for `set_shell_foreground_task` with a - resident foreground Game. -- Session facade tests for VM Shell and native Shell creation while a Game is - resident. -- Existing firmware Shell close tests must continue to pass. -- Run `cargo test -p prometeu-system foreground lifecycle shell`. -- Run `cargo test -p prometeu-firmware shell`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/foreground.rs` -- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` -- `crates/console/prometeu-system/src/os/facades/sessions.rs` -- `crates/console/prometeu-system/src/os/system_os.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` diff --git a/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md b/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md deleted file mode 100644 index e73b3070..00000000 --- a/discussion/workflow/plans/PLN-0145-centralize-resident-game-resume-transition.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -id: PLN-0145 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Centralize Resident Game Resume Transition -status: done -created: 2026-07-04 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -`DEC-0037` requires returning to the same resident Game to reactivate the Game -VM, restore render ownership, send a resume/foreground lifecycle notification, -and clear or barrier pending Game input. That transition currently exists in -more than one firmware path: `Firmware::resume_resident_game_from_home` and the -Hub library click path that resumes the selected resident Game by `app_id`. - -The behavior is correct, but the duplication is fragile. Any future change to -resume ordering, input barriers, render ownership, or lifecycle event delivery -could fix one path while leaving the other path stale. - -## Objective - -Create one firmware-owned resident Game resume helper and route every -Home/Hub-to-Game resume path through it. - -## Dependencies - -- Source decision: `DEC-0037`. -- Depends on the accepted v1 identity rule that `app_id` is unique for all - games and may be used to decide whether a Hub game selection refers to the - current resident Game. -- Should preserve the behavior introduced by `PLN-0142`, `PLN-0143`, and - `PLN-0144`. - -## Scope - -Included: - -- Centralize resident Game resume orchestration in `Firmware`. -- Preserve resume lifecycle event queuing through `LifecycleFacade::resume_task`. -- Preserve render ownership transition back to `AppMode::Game`. -- Preserve input barrier behavior across all Home/Hub-to-Game resume paths. -- Preserve the Hub rule that selecting the same resident Game resumes it, while - selecting a different Game remains blocked by v1 scope. -- Keep tests proving same-task resume, lifecycle event delivery, and input - barriering. - -Excluded: - -- No Game-to-Game switching. -- No multiple resident Games. -- No change to `app_id` uniqueness policy. -- No direct Shell-to-Game close behavior. - -## Non-Goals - -- Do not move foreground authority from SystemOS into firmware. -- Do not change pause budget or suspend behavior. -- Do not add UI affordances beyond the existing Hub game row selection. - -## Execution Method - -1. Introduce a single resident Game resume helper. - - What changes: create a private firmware method that performs the complete - resident Game resume operation and returns the target `TaskId` or a typed - failure. - - How it changes: move the shared sequence currently duplicated across - `resume_resident_game_from_home` and `HubHomeStep`: read resident task, - call `LifecycleFacade::resume_task`, transition VM render ownership to - `AppMode::Game`, install `GameRunningStep`, clear state initialization, - and arm the input barrier. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. - -2. Route explicit resume through the helper. - - What changes: `Firmware::resume_resident_game_from_home` becomes a thin - wrapper around the centralized helper. - - How it changes: preserve its public `bool` return contract for existing - tests and call sites. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. - -3. Route Hub same-game selection through the helper. - - What changes: `HubHomeStep` must no longer hand-roll resume task, render - ownership, and input clearing. - - How it changes: expose a state transition request that lets the firmware - perform the centralized helper, or restructure the Hub action handling so - the parent `Firmware` owns this transition. Keep `HubHomeStep` responsible - only for recognizing that the selected cartridge matches the resident - `app_id`. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`, - `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`, - `crates/console/prometeu-firmware/src/firmware/firmware_state.rs` if a new - transition variant is needed. - -4. Keep different-game launch blocked under v1. - - What changes: selecting a different `app_id` while a resident Game exists - continues to log and remain in Hub. - - How it changes: keep the existing warning behavior, but ensure it does not - duplicate resume side effects. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`. - -5. Update regression tests. - - What changes: tests should prove both explicit resume and Hub same-game - selection use the same observable transition. - - How it changes: assert same `TaskId`, foreground owner restored to Game, - pending resume lifecycle event exists before the next Game tick, delivered - resume event after the Game tick, and input barrier suppresses held input. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. - -## Acceptance Criteria - -- [ ] There is exactly one implementation of the resident Game resume sequence. -- [ ] `Firmware::resume_resident_game_from_home` preserves its external - behavior. -- [ ] Hub selection of the same resident Game resumes the existing `TaskId` - without creating or loading a new Game task. -- [ ] The resume path always queues and delivers the Game lifecycle - `ResumeForeground` event. -- [ ] The resume path always barriers held Game input. -- [ ] Selecting a different Game while one Game is resident remains blocked in - v1. - -## Tests - -- Unit/integration tests in `prometeu-firmware` for explicit resume from Home. -- Unit/integration tests in `prometeu-firmware` for Hub same-game selection - after Home suspension. -- Regression assertion that held input does not leak into the Game immediately - after resume. -- Run `cargo test -p prometeu-firmware resident_game_resume`. -- Run `cargo test -p prometeu-firmware hub_home_clicking_resident_game`. -- Run `cargo test -p prometeu-system`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_state.rs` - -## Risks - -- The current step/state architecture gives `HubHomeStep` a `PrometeuContext` - but not direct access to private `Firmware` fields. If a new transition - variant is introduced, it must stay narrow and must not turn state steps into - owners of lifecycle authority. -- Duplicating less logic must not accidentally weaken the input barrier that was - added for Hub-to-Game transitions. diff --git a/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md b/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md deleted file mode 100644 index 22bb498b..00000000 --- a/discussion/workflow/plans/PLN-0146-consolidate-lifecycle-process-lookup-helpers.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -id: PLN-0146 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Consolidate Lifecycle Process Lookup Helpers -status: done -created: 2026-07-04 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -`DEC-0037` depends on SystemOS lifecycle authority being internally -consistent. The lifecycle facade now exposes both process state and process kind -for a task so firmware can distinguish VM-backed Shell tasks from native Shell -tasks. Both accessors repeat the same task-to-process lookup pattern. - -The duplication is small, but this facade is now part of the foreground -contract surface. Keeping lookup and error mapping in one place reduces the -chance that future lifecycle accessors drift in behavior. - -## Objective - -Consolidate task-to-process lookup in `LifecycleFacade` behind one private -helper, while preserving the public lifecycle facade behavior and errors. - -## Dependencies - -- Source decision: `DEC-0037`. -- Depends on the process/task authority from SystemOS lifecycle work. -- Can be implemented independently of `PLN-0145` and `PLN-0147`. - -## Scope - -Included: - -- Add one private helper that resolves a `TaskId` to its backing process. -- Route `process_state_for_task` and `process_kind_for_task` through that - helper. -- Preserve `LifecycleError::TaskNotFound` and - `LifecycleError::ProcessNotFound` behavior exactly. -- Keep the current process/task manager ownership boundaries intact. -- Keep the current non-architectural cleanup in `set_foreground_task` if it is - still present in the working tree. - -Excluded: - -- No new public process manager API. -- No changes to task or process IDs. -- No changes to Shell/Game lifecycle semantics. -- No broader facade refactor outside lifecycle lookup. - -## Non-Goals - -- Do not expose raw process manager access through SystemOS. -- Do not change `ProcessKind` or `TaskKind` definitions. -- Do not alter foreground stack behavior. - -## Execution Method - -1. Add a private lifecycle lookup helper. - - What changes: add a private function near `process_id_for_task` that - returns a process reference for a `TaskId`. - - How it changes: the helper should call `process_id_for_task`, then - retrieve the process from `process_manager`, returning - `LifecycleError::ProcessNotFound(process_id)` if the process is missing. - - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. - -2. Route state and kind accessors through the helper. - - What changes: `LifecycleFacade::process_state_for_task` and - `LifecycleFacade::process_kind_for_task` stop duplicating the process - manager lookup. - - How it changes: both methods call the helper and map the process reference - to `ProcessState` or `ProcessKind`. - - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. - -3. Preserve existing foreground code cleanup. - - What changes: keep `set_foreground_task` as an expression-returning match - rather than using redundant `return` statements. - - How it changes: ensure formatting and clippy stay clean. - - File(s): `crates/console/prometeu-system/src/os/facades/lifecycle.rs`. - -4. Tighten tests around lookup behavior. - - What changes: extend or keep tests proving VM Shell and native Shell - process kinds are observable through lifecycle. - - How it changes: add missing-task and missing-process coverage only if the - helper changes the observable path enough to justify it. - - File(s): `crates/console/prometeu-system/src/os/system_os.rs`. - -## Acceptance Criteria - -- [ ] `process_state_for_task` and `process_kind_for_task` share one private - task-to-process lookup implementation. -- [ ] Missing task and missing process errors remain unchanged. -- [ ] Existing lifecycle tests continue to pass. -- [ ] Firmware can still distinguish `ProcessKind::VmShell` from - `ProcessKind::NativeShell`. -- [ ] Clippy reports no warnings for the touched crates. - -## Tests - -- Unit tests in `prometeu-system` for shell process kind lookup. -- Existing lifecycle missing task/process tests must continue to pass. -- Run `cargo test -p prometeu-system lifecycle process_kind`. -- Run `cargo test -p prometeu-firmware shell`. -- Run `cargo clippy -p prometeu-system -p prometeu-firmware --all-targets -- -D warnings`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` -- `crates/console/prometeu-system/src/os/system_os.rs` - -## Risks - -- Over-generalizing the helper could leak process manager concepts outside the - lifecycle facade. Keep it private and narrowly typed. -- This is intentionally small cleanup. It must not become a broad SystemOS - facade redesign. diff --git a/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md b/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md deleted file mode 100644 index 502c06fd..00000000 --- a/discussion/workflow/plans/PLN-0147-move-native-shell-profile-update-into-hub.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -id: PLN-0147 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Move Native Shell Profile Update Into Hub -status: done -created: 2026-07-04 -ref_decisions: [DEC-0037] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -`DEC-0037` says a suspended Game must not receive normal gameplay ticks while -Hub or Shell owns foreground. A recent fix made `ShellRunningStep` branch on -`ProcessKind`: VM-backed Shell tasks use the VM-backed shell profile path, while -native Shell tasks call `PrometeuHub::gui_update` and `PrometeuHub::render` -without ticking the VM. - -That behavior is correct, but the native Shell update sequence now lives in -firmware rather than in the Hub profile API. This duplicates part of -`PrometeuHub::update_shell_profile` and creates a drift risk if Shell close, -render, or input rules change later. - -## Objective - -Move native Shell profile updating into `PrometeuHub` so `ShellRunningStep` -chooses the correct profile API without reimplementing Hub behavior. - -## Dependencies - -- Source decision: `DEC-0037`. -- Depends on the `ProcessKind::VmShell` vs `ProcessKind::NativeShell` - distinction available through `LifecycleFacade::process_kind_for_task`. -- May be implemented before or after `PLN-0146`, but should use the consolidated - lookup helper if `PLN-0146` has already landed. - -## Scope - -Included: - -- Add a Hub-owned native Shell update API that does not tick the VM. -- Keep VM-backed Shell update behavior in the existing VM-backed profile path. -- Keep Shell close handling consistent between native and VM-backed Shells. -- Keep native Shell rendering in the Hub/Shell UI path. -- Keep firmware responsible for choosing which profile applies to the current - Shell task. -- Preserve regression coverage that a suspended resident Game does not advance - while native Shell is foreground. - -Excluded: - -- No change to VM-backed Shell execution semantics. -- No change to native Shell UI content. -- No direct Shell close to Game. -- No multiple foreground Shells. -- No background Game ticking. - -## Non-Goals - -- Do not move foreground lifecycle authority into `PrometeuHub`. -- Do not make native Shells VM tasks. -- Do not introduce a general scheduler abstraction. - -## Execution Method - -1. Add a native Shell profile update method on `PrometeuHub`. - - What changes: create `update_native_shell_profile` or an equivalent - clearly named method that returns `SystemProfileUpdate`. - - How it changes: the method calls `gui_update`, maps Start input to - `SystemProfileAction::CloseShell` when a shell window is focused, calls - `render`, and always returns `crash: None`. - - File(s): - `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. - -2. Keep the VM-backed Shell profile method explicitly VM-backed. - - What changes: either keep `update_shell_profile` and document/name its VM - tick behavior, or rename/split it if the codebase convention supports that - without churn. - - How it changes: ensure the VM-backed path is the only Hub profile update - method that calls `os.vm().tick(...)`. - - File(s): - `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`. - -3. Simplify `ShellRunningStep`. - - What changes: remove manual native Shell `gui_update`, Start handling, and - render calls from firmware. - - How it changes: branch on `ProcessKind` and call the appropriate Hub - profile method; keep crash handling only for the VM-backed method. - - File(s): - `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs`. - -4. Add Hub-level native profile tests. - - What changes: prove native Shell profile update emits close on the close - button and on Start, renders Shell UI, and does not tick the VM. - - How it changes: use existing Hub/system test helpers where possible; assert - VM tick index remains unchanged for native profile update. - - File(s): - `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` - and/or `crates/console/prometeu-firmware/src/firmware/firmware.rs`. - -5. Preserve end-to-end regression coverage. - - What changes: keep the Game -> Home -> native Shell test asserting that - repeated Shell frames do not advance the resident Game VM tick index. - - How it changes: rerun the firmware regression added for native Shell - foreground. - - File(s): `crates/console/prometeu-firmware/src/firmware/firmware.rs`. - -## Acceptance Criteria - -- [ ] Native Shell update behavior is owned by `PrometeuHub`, not hand-coded in - `ShellRunningStep`. -- [ ] The native Shell profile update never calls `os.vm().tick(...)`. -- [ ] VM-backed Shell update remains the only Shell profile path that ticks the - VM runtime. -- [ ] Start and close-button Shell close behavior remains unchanged for native - Shells. -- [ ] A suspended resident Game receives no normal gameplay ticks while native - Shell is foreground. - -## Tests - -- Hub-level tests for native Shell profile close behavior. -- Hub-level or firmware-level tests proving native Shell profile update does not - tick the VM. -- Existing firmware regression for Game -> Home -> native Shell repeated frames. -- Run `cargo test -p prometeu-system native_shell shell_profile`. -- Run `cargo test -p prometeu-firmware game_home_shell_hub_same_game_flow`. -- Run `cargo test -p prometeu-firmware`. -- Run `cargo test -p prometeu-system`. -- Run `discussion validate`. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` - -## Risks - -- Renaming the existing VM-backed shell profile method may create unnecessary - churn. Prefer adding the native method and keeping call sites explicit unless - the rename is small and clearly improves readability. -- Hub APIs must remain UI/profile orchestration only; lifecycle close and task - state changes still belong to firmware/SystemOS lifecycle. diff --git a/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md b/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md deleted file mode 100644 index fc1717db..00000000 --- a/discussion/workflow/plans/PLN-0148-introduce-vm-session-registry-and-identity.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -id: PLN-0148 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Introduce VM Session Registry and Identity -status: done -created: 2026-07-04 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 requires every VM-backed process to own its own mutable VM context instead of sharing `Firmware.vm`. This plan introduces the identity and registry layer required before moving runtime state or changing cartridge loading. - -The implementation must make VM session ownership explicit inside `SystemOS`. A VM-backed Game task and a VM-backed Shell task must be able to coexist as distinct sessions, even before all execution paths are migrated to session-owned VMs. - -## Objective - -Add a `VmSession` model and registry owned by `SystemOS`, keyed by a stable session identity tied to the owning task/process. The registry must support creating, retrieving, and validating VM sessions for VM-backed Game and Shell tasks. - -## Dependencies - -- Source decision: DEC-0038. -- Existing foreground/task/process lifecycle from DEC-0037 remains authoritative. -- Must be implemented before PLN-0149, PLN-0150, PLN-0151, and PLN-0152. - -## Scope - -- Add a session identity type, preferably `VmSessionId`, with a deliberate mapping from the owning `TaskId`. -- Add a `VmSession` data structure containing at least: - - owning `TaskId`; - - owning process id or process lookup path; - - `app_id`, title, version, and `AppMode`; - - initial placeholder for the session-owned `VirtualMachine`; - - initial placeholder for session-owned runtime state to be populated by PLN-0149. -- Add a registry under `crates/console/prometeu-system/src/services/` or an equivalent existing services module. -- Expose creation and lookup through `crates/console/prometeu-system/src/os/facades/sessions.rs`. -- Update `SystemOS` construction in `crates/console/prometeu-system/src/os/system_os.rs` to own the registry. -- Enforce V1 uniqueness: - - at most one resident Game session; - - one foreground Shell task at a time; - - same `app_id` resident Game relaunch maps to the existing resident session rather than creating a second Game session. - -## Non-Goals - -- Do not move all VM runtime fields yet; that is PLN-0149. -- Do not route cartridge loading through sessions yet; that is PLN-0150. -- Do not remove `Firmware.vm` yet. -- Do not implement general background execution. -- Do not allow Game-to-Game switching. - -## Execution Method - -1. Inspect `TaskId`, process ids, and `ProcessKind` definitions in `crates/console/prometeu-system/src/services/task/` and `crates/console/prometeu-system/src/services/process/`. -2. Add the VM session module with a small API: - - create session for an existing VM-backed task/process; - - get immutable/mutable session by task id; - - get resident Game session by `app_id`; - - reject incompatible duplicate Game sessions. -3. Wire the registry into `SystemOS`. -4. Extend `SessionsFacade::create_vm_game_task` and `create_vm_shell_task` so task creation also creates a VM session record. -5. Keep native Shell task creation out of the VM session registry. -6. Add focused unit tests under `prometeu-system` for registry identity and duplicate handling. - -## Acceptance Criteria - -- `SystemOS` owns a VM session registry. -- Creating a VM Game task creates exactly one VM session associated with that task. -- Creating a VM Shell task creates exactly one VM session associated with that task. -- Creating a native Shell task creates no VM session. -- Attempting to create a second resident Game session for a different `app_id` is rejected or routed through the existing lifecycle rejection path. -- Relaunching the same resident Game can resolve the existing VM session by `app_id`. -- No firmware state transition depends on the new registry yet except through task/session creation. - -## Tests - -- `cargo test -p prometeu-system` -- Add unit tests for: - - VM Game task creates a VM session; - - VM Shell task creates a VM session; - - native Shell task does not create a VM session; - - lookup by `TaskId`; - - same resident Game `app_id` resolves to the existing session; - - different resident Game `app_id` remains blocked under V1. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/os/system_os.rs` -- `crates/console/prometeu-system/src/os/facades/sessions.rs` -- `crates/console/prometeu-system/src/services/mod.rs` -- New or extended module under `crates/console/prometeu-system/src/services/` -- `crates/console/prometeu-system/src/lib.rs` if exports are required diff --git a/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md b/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md deleted file mode 100644 index c7396a18..00000000 --- a/discussion/workflow/plans/PLN-0149-move-vm-runtime-state-into-vm-sessions.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: PLN-0149 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Move VM Runtime State Into VM Sessions -status: done -created: 2026-07-04 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 states that mutable VM context must belong to the VM-backed process session, not to firmware or a single global runtime slot. The current split has `Firmware.vm: VirtualMachine` and `SystemOS.vm_runtime: VirtualMachineRuntime`, which makes a suspended Game vulnerable to VM-backed Shell execution. - -This plan migrates mutable per-application VM runtime state into the session model introduced by PLN-0148 while keeping global OS services global. - -## Objective - -Make each VM session own the state needed to suspend and later resume that VM-backed process without being overwritten by another VM-backed process. - -## Dependencies - -- Source decision: DEC-0038. -- Requires PLN-0148. -- Must precede full cartridge-loading and scheduler migration in PLN-0150 and PLN-0151. - -## Scope - -- Classify `VirtualMachineRuntime` fields into session-owned versus global OS-owned state. -- Move session-owned fields into the `VmSession` context. -- Keep global services in `SystemOS`, including: - - `LogService`; - - process/task/window managers; - - foreground stack; - - durable `MemcardService`; - - global render publication authority or worker coordination that must remain host-facing. -- Treat these as session-owned unless proven otherwise during implementation: - - `VirtualMachine`; - - current cartridge identity; - - frame and tick counters; - - lifecycle delivery bookkeeping; - - crash report for that session; - - pause/debug flags that apply to that session; - - per-session open files, handles, and staging state used by VM syscalls. -- Keep durable memcard contents keyed by `app_id`; do not turn save data into session-private durable storage. -- Update `VmFacade` and VM runtime helpers so callers can operate on a selected session. - -## Non-Goals - -- Do not redesign render workers beyond separating session-local render command state from global presentation authority. -- Do not implement background execution. -- Do not change syscall semantics except where required to route state through the active session. -- Do not remove host debugger features; preserve or adapt their access path. - -## Execution Method - -1. Add an internal field-by-field classification comment or test-only assertion near `VirtualMachineRuntime` before moving fields. -2. Introduce a session runtime/context type in the VM session module or by splitting `VirtualMachineRuntime` into: - - session-local runtime; - - global render/log/runtime services. -3. Move `VirtualMachine` ownership from firmware placeholder/session stub into the session context. -4. Move per-cartridge and per-frame state from `SystemOS.vm_runtime` into the session context. -5. Move `open_files`, `next_handle`, and VM syscall staging state out of global `SystemOS` if they are VM-session specific. -6. Update `VmRuntimeHost` in `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs` to receive session-local state plus references to required global services. -7. Update `VmFacade` in `crates/console/prometeu-system/src/os/facades/vm.rs` with explicit session-targeted methods. -8. Preserve temporary compatibility methods only where needed for existing host/debugger code, and mark them as active-session views rather than canonical global ownership. -9. Add tests proving two VM sessions can hold different app identity and counters at the same time. - -## Acceptance Criteria - -- No canonical VM state lives in `Firmware`. -- `SystemOS` can hold at least two VM sessions with independent `VirtualMachine` and per-session runtime state. -- VM session A and VM session B can have different `app_id`, title, frame counters, lifecycle delivery history, crash report, open file handles, and debug pause state. -- Durable memcard contents remain shared by `app_id` according to the existing memcard contract. -- Global render ownership remains controlled by SystemOS/host-facing runtime services, not by individual sessions. -- Existing `prometeu-system` VM runtime tests still pass after adapting their setup. - -## Tests - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-firmware` -- Add tests for: - - independent session app identity; - - independent tick/logical-frame counters; - - independent lifecycle delivery bookkeeping; - - VM Shell session initialization does not mutate resident Game session identity; - - session-scoped file handles do not collide across sessions. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/services/vm_runtime/` -- VM session module introduced by PLN-0148 -- `crates/console/prometeu-system/src/os/facades/vm.rs` -- `crates/console/prometeu-system/src/os/system_os.rs` -- `crates/console/prometeu-system/src/os/facades/fs.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` -- `crates/host/prometeu-host-desktop-winit/src/stats.rs` diff --git a/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md b/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md deleted file mode 100644 index bdf0712e..00000000 --- a/discussion/workflow/plans/PLN-0150-route-cartridge-loading-through-vm-sessions.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -id: PLN-0150 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Route Cartridge Loading Through VM Sessions -status: done -created: 2026-07-04 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 requires cartridge loading to create or activate a VM-backed process session instead of initializing a single global VM. The current `LoadCartridgeStep` initializes `ctx.os.vm().initialize(ctx.vm, &cartridge)` before task/session ownership is selected. - -This plan moves cartridge initialization behind session services so Game and VM-backed Shell loading cannot overwrite each other. - -## Objective - -Route Game and VM Shell cartridge loading through the VM session registry and initialize the cartridge inside the target session-owned VM context. - -## Dependencies - -- Source decision: DEC-0038. -- Requires PLN-0148. -- Requires enough of PLN-0149 to expose session-owned VM/runtime initialization. -- Must precede the end-to-end coexistence validation in PLN-0152. - -## Scope - -- Replace direct global VM initialization in `LoadCartridgeStep`. -- Add a session/facade operation that: - - selects an existing resident Game session when the same `app_id` is relaunched; - - rejects or reports different resident Game launches under V1; - - creates a new VM Shell session for a Shell cartridge; - - initializes the cartridge inside the selected session VM. -- Keep asset manager initialization aligned with the foreground cartridge being launched. -- Ensure `AppCrashesStep` receives initialization failures from the target session. -- Update Hub cartridge selection logic so resume versus load is based on lifecycle/session identity, not global VM identity. - -## Non-Goals - -- Do not implement multiple resident Games. -- Do not implement background execution. -- Do not redesign cartridge format or asset packaging. -- Do not broaden Shell windowing behavior beyond the existing one-foreground-Shell model. - -## Execution Method - -1. Add a `load_vm_cartridge` or equivalent method to `SessionsFacade` or `VmFacade`. -2. Make the method return an execution target: - - existing Game task for same resident `app_id`; - - new Game task for first Game launch; - - new VM Shell task for Shell launch; - - structured error/crash report for invalid VM initialization; - - explicit lifecycle error for blocked second Game. -3. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` to call the facade method instead of initializing `ctx.vm`. -4. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` so same-Game relaunch checks session/lifecycle state rather than `ctx.os.vm().current_app_id()`. -5. Preserve AppMode-based transition: - - Game target transitions to `GameRunning`; - - Shell target creates/focuses a window and transitions to `ShellRunning`. -6. Adapt crash tests and resident-game relaunch tests in firmware. - -## Acceptance Criteria - -- `LoadCartridgeStep` no longer initializes a global firmware VM. -- Loading a Shell cartridge creates/initializes a Shell VM session. -- Loading the first Game cartridge creates/initializes the resident Game VM session. -- Clicking the same resident Game from Hub resumes the existing Game session without reinitializing it. -- Attempting to load a different Game while one resident Game exists remains blocked under V1 with a controlled outcome. -- VM initialization failure still transitions to `AppCrashesStep`. - -## Tests - -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-system` -- Add or update firmware tests for: - - first Game load creates one session and enters `GameRunning`; - - same Game relaunch resumes existing task/session; - - VM Shell load creates a distinct session; - - Shell load does not change resident Game session app identity; - - invalid cartridge reports `CrashReport::VmInit`. - -## 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/facades/vm.rs` -- VM session module introduced by PLN-0148 diff --git a/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md b/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md deleted file mode 100644 index 873f2796..00000000 --- a/discussion/workflow/plans/PLN-0151-schedule-foreground-vm-session-ticks.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -id: PLN-0151 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Schedule Foreground VM Session Ticks -status: done -created: 2026-07-04 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 separates session ownership from foreground ownership and states that V1 does not provide general background execution. The runtime therefore needs an explicit scheduler path that ticks the VM session eligible for foreground execution, while leaving suspended resident Game sessions intact. - -## Objective - -Make firmware and SystemOS tick the VM session associated with the current foreground VM-backed task, instead of ticking a global VM/runtime pair. - -## Dependencies - -- Source decision: DEC-0038. -- Requires PLN-0148. -- Requires PLN-0149. -- Requires PLN-0150 for loaded cartridges to map to sessions. - -## Scope - -- Add scheduler/facade operations that tick a specific VM session by `TaskId`. -- Update `GameRunningStep` to tick the Game session identified by its `task_id`. -- Update `ShellRunningStep` to tick only VM-backed Shell sessions. -- Ensure native Shell foreground state performs native profile updates without ticking any VM session. -- Keep suspended Game sessions non-executing while Hub or Shell is foreground. -- Preserve lifecycle event delivery: - - pause/resume events go to the resident Game session; - - Shell lifecycle events, if any, remain scoped to the Shell session. -- Preserve host debugger pause/step behavior against the active VM session view. - -## Non-Goals - -- Do not add background execution classes yet. -- Do not allow multiple simultaneous VM ticks in one host frame. -- Do not implement general service/player processes. -- Do not change the 60 Hz firmware tick contract. - -## Execution Method - -1. Add `SystemOS` facade methods for: - - ticking a VM session by task id; - - querying whether a task is VM-backed; - - exposing the active VM session to host/debugger tooling. -2. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` to tick through the task/session facade. -3. Update `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` to branch on process kind: - - VM Shell ticks its VM session; - - native Shell runs native profile logic only. -4. Remove direct `ctx.vm` use from Game/Shell execution steps. -5. Ensure resident Game pause budget can deliver one final pause tick and then stop ticking once suspended. -6. Update host/debugger active VM access paths to resolve through the foreground session. -7. Add regression tests for tick counters across Game, Hub, native Shell, and VM Shell transitions. - -## Acceptance Criteria - -- A suspended Game session does not advance while Hub is foreground. -- A suspended Game session does not advance while a Shell is foreground. -- A VM Shell session advances while that VM Shell is foreground. -- A native Shell does not tick any VM session. -- Returning from Hub to resident Game resumes ticking the same Game session. -- Host debugger and stats read the active VM session, not a stale global VM. - -## Tests - -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-host-desktop-winit` -- Add tests for: - - Game tick count freezes after Home suspension; - - VM Shell tick count advances independently; - - native Shell does not advance Game tick count; - - resume returns to same Game session tick counter; - - active-session debugger snapshot uses foreground VM session. - -## Affected Artifacts - -- `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs` -- `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/console/prometeu-system/src/os/facades/vm.rs` -- `crates/console/prometeu-system/src/os/facades/lifecycle.rs` -- VM session module introduced by PLN-0148 -- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` -- `crates/host/prometeu-host-desktop-winit/src/stats.rs` diff --git a/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md b/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md deleted file mode 100644 index 8ae74cbc..00000000 --- a/discussion/workflow/plans/PLN-0152-validate-game-and-vm-shell-session-coexistence.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: PLN-0152 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Validate Game and VM Shell Session Coexistence -status: done -created: 2026-07-04 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 was motivated by the concrete failure mode where a resident Game is suspended, a Shell is opened, and Game logs or state unexpectedly continue under the wrong foreground owner. After VM sessions and scheduling are implemented, the system needs explicit regression coverage for this workflow. - -## Objective - -Add end-to-end and integration validation proving a resident Game VM session and a VM-backed Shell session can coexist without overwriting or ticking each other incorrectly. - -## Dependencies - -- Source decision: DEC-0038. -- Requires PLN-0148, PLN-0149, PLN-0150, and PLN-0151. -- Should be executed before closing the broader DEC-0038 implementation thread. - -## Scope - -- Add firmware-level regression tests for: - - Game launch; - - Home/SystemOS request; - - Game pause/suspend; - - VM Shell launch; - - Shell foreground ticking; - - Shell exit; - - Hub return; - - resident Game resume. -- Add system-level tests for independent session state. -- Add host-level tests only where active VM/debugger/stat routing cannot be covered at firmware/system level. -- Verify logs from the stress cartridge or equivalent test cartridge do not appear while Game is suspended and Shell is foreground. -- Verify Shell logs, frames, or tick counters belong to the Shell session. - -## Non-Goals - -- Do not introduce new architecture in this plan. -- Do not broaden background execution. -- Do not rewrite Hub UI behavior except where required to exercise the flow. -- Do not depend on manual desktop testing as the only evidence. - -## Execution Method - -1. Identify or add deterministic test cartridges/helpers that can emit per-session tick/log evidence. -2. Add a firmware integration test for `Game -> Home -> VM Shell -> Hub -> same Game`. -3. Capture Game session counters before Shell launch and after Shell foreground ticks. -4. Assert the Game session counters and lifecycle delivery state remain stable while Shell is foreground. -5. Assert the Shell session counters advance while Shell is foreground. -6. Assert returning to the same Game resumes the same task/session and then advances the Game counter. -7. Add a regression specifically covering the prior symptom: Game `stress` logs must not appear during Shell foreground execution. -8. Run the relevant crate tests and record evidence in the plan completion notes when implemented. - -## Acceptance Criteria - -- The failing manual scenario is covered by automated tests. -- The Game session is suspended and non-ticking while VM Shell is foreground. -- The VM Shell session ticks independently. -- Returning to Hub and selecting the same Game resumes the resident Game session instead of creating a duplicate process/session. -- No test relies on a single global `current_app_id` to infer correctness. - -## Tests - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-host-desktop-winit` if host active-session plumbing changed. -- Optional manual smoke: - - run `cargo run --bin prometeu-host-desktop-winit --profile release --manifest-path crates/host/prometeu-host-desktop-winit/Cargo.toml -- --games-root test-cartridges`; - - open Stress Console; - - press ESC/Home; - - open VM-backed Shell; - - verify no Stress logs appear while Shell is foreground; - - return and resume Stress Console. - -## Affected Artifacts - -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` tests -- `crates/console/prometeu-system/src/os/system_os.rs` tests -- `crates/console/prometeu-system/src/services/vm_runtime/tests.rs` -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` tests if needed -- `test-cartridges/` only if a deterministic VM Shell fixture is missing diff --git a/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md b/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md deleted file mode 100644 index a5e6cd85..00000000 --- a/discussion/workflow/plans/PLN-0153-specify-vm-session-ownership-and-background-ready-scheduling.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: PLN-0153 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Specify VM Session Ownership and Background-Ready Scheduling -status: done -created: 2026-07-04 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 locks the runtime direction: VM context is session-owned, firmware orchestrates macro-state, and background execution is out of V1 but must remain a near-term compatible extension. The canonical specs must describe that contract in English so future implementation and debugging do not drift back to a single global VM model. - -## Objective - -Update canonical runtime specifications to document VM session ownership, foreground execution eligibility, render publication authority, and the background-ready shape of the scheduler without committing to background execution in this implementation batch. - -## Dependencies - -- Source decision: DEC-0038. -- Should be informed by PLN-0148 through PLN-0151 once implementation names settle. -- May be done before PLN-0152 if the implementation contract is already stable, but final wording must match implemented APIs. - -## Scope - -- Update the canonical specs under `docs/specs/runtime/` in English. -- Document: - - each VM-backed process owns one VM session; - - VM sessions own mutable VM context; - - foreground ownership and execution eligibility are separate concepts; - - V1 executes only the foreground VM-backed task, except the existing pause handoff budget; - - suspended resident Game sessions preserve VM context but do not tick; - - VM-backed Shell sessions have their own VM context; - - native Shell does not own a VM session; - - durable app data remains keyed by `app_id`; - - session-scoped handles and staging state are not durable app data; - - global render publication remains SystemOS/host authority. -- Add background-ready wording: - - scheduler model can later mark sessions or service processes as eligible without changing ownership; - - no current guarantee of background progress is introduced. - -## Non-Goals - -- Do not write implementation plans into specs. -- Do not document background execution as available. -- Do not move normative text into `discussion/lessons/`. -- Do not create new legacy docs paths. - -## Execution Method - -1. Locate the most relevant runtime specs, likely: - - `docs/specs/runtime/09-events-and-concurrency.md`; - - `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md`; - - `docs/specs/runtime/14-boot-profiles.md`; - - `docs/specs/runtime/16-host-abi-and-syscalls.md` if host/debugger active VM views are affected. -2. Add concise normative sections using `MUST`, `MUST NOT`, and `SHALL` where appropriate. -3. Keep discussion history out of the spec; cite behavior, not agenda chronology. -4. Align terminology with implemented type names from PLN-0148 through PLN-0151. -5. Run discussion validation after edits and any repository tests required by touched docs tooling. - -## Acceptance Criteria - -- Specs explicitly state that VM context is session-owned for VM-backed processes. -- Specs explicitly state that native Shell does not own a VM session. -- Specs distinguish foreground ownership from future execution eligibility. -- Specs state that V1 does not support general background execution. -- Specs preserve a future path for background-capable sessions/services without changing VM ownership. -- No new framework artifacts are written outside `discussion/`. - -## Tests - -- `discussion validate` -- Manual review of touched specs for English-only normative text. -- Run repository doc checks if such a command exists; otherwise no code test is required for this plan. - -## Affected Artifacts - -- `docs/specs/runtime/09-events-and-concurrency.md` -- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` -- `docs/specs/runtime/14-boot-profiles.md` -- `docs/specs/runtime/16-host-abi-and-syscalls.md` if active VM host/debugger wording is needed diff --git a/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md b/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md deleted file mode 100644 index 45bfaac8..00000000 --- a/discussion/workflow/plans/PLN-0154-route-debugger-operations-through-vm-sessions.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -id: PLN-0154 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Route Debugger Operations Through VM Sessions -status: done -created: 2026-07-05 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 requires VM-backed mutable execution state to be owned by VM sessions. Normal execution now ticks session-owned VMs, but desktop debugger commands still inspect and mutate `firmware.vm`, the transitional legacy VM. This leaves debugger state, breakpoints, single-step execution, PC snapshots, and stack snapshots inconsistent with the active VM session. - -## Objective - -Route host debugger operations through the active VM session rather than the legacy firmware VM. Debugger pause/resume/step/state/breakpoint operations must target the same VM context that foreground execution ticks. - -## Dependencies - -- Source decision: DEC-0038. -- Builds on PLN-0148 through PLN-0152. -- Should be implemented before PLN-0155 removes the legacy firmware VM bridge. - -## Scope - -- Add session-aware debugger accessors to `SystemOS`/`VmFacade`. -- Expose active VM session inspection and mutation operations needed by the debugger: - - PC; - - operand stack preview; - - insert breakpoint; - - remove breakpoint; - - list breakpoints; - - single-instruction debug step; - - active cartridge identity for handshake and state responses. -- Update `crates/host/prometeu-host-desktop-winit/src/debugger.rs` so it no longer reads or mutates `firmware.vm` for active VM state. -- Ensure debugger setup for direct debug boot initializes through normal cartridge/session loading or an explicit session-aware metadata path, not `os.vm().initialize(&mut firmware.vm, ...)`. -- Keep pause/resume/step-frame control mapped to active VM session runtime state. -- Add tests that would fail if debugger state came from the legacy VM while the session VM is active. - -## Non-Goals - -- Do not remove `Firmware.vm`; that is PLN-0155. -- Do not redesign the debugger protocol. -- Do not add multi-client debugging. -- Do not add background-session debugging beyond active/resident session semantics already supported by DEC-0038. - -## Execution Method - -1. Add focused `VmFacade` methods for active-session VM inspection: - - `active_pc`; - - `active_operand_stack_top`; - - `insert_active_breakpoint`; - - `remove_active_breakpoint`; - - `active_breakpoints_list`; - - `debug_step_active_session`. -2. Define active-session resolution explicitly: - - foreground VM session first; - - resident Game session when Hub has foreground but a resident Game is the active debug target; - - no legacy fallback for debugger mutation once a session exists. -3. Update `HostDebugger::setup_boot_target` so debug cartridge metadata is loaded without initializing `firmware.vm` as canonical state. -4. Update `HostDebugger::handle_command` for `Step`, `GetState`, `SetBreakpoint`, `ListBreakpoints`, and `ClearBreakpoint`. -5. Update `HostDebugger::stream_events` breakpoint-hit PC reporting to read from the active session. -6. Add tests under `crates/host/prometeu-host-desktop-winit/src/debugger.rs` proving debugger commands observe and mutate the session VM. -7. Run the host, firmware, and system test suites. - -## Acceptance Criteria - -- Debugger handshake reports cartridge identity from session/runtime state or preloaded metadata, not from a global VM initialized only for debugger setup. -- `GetState` returns PC and stack data from the active VM session. -- `Step` advances the active VM session, not `firmware.vm`. -- Breakpoint commands mutate the active VM session. -- Breakpoint-hit events report the active session PC. -- No debugger command path directly reads or mutates `firmware.vm` for active VM execution state. -- Existing debugger protocol tests pass. - -## Tests - -- `cargo test -p prometeu-host-desktop-winit` -- `cargo test -p prometeu-firmware` -- `cargo test -p prometeu-system` -- `cargo clippy -p prometeu-host-desktop-winit -p prometeu-firmware -p prometeu-system --all-targets -- -D warnings` -- Add tests for: - - `GetState` reads session VM PC; - - `Step` affects session VM; - - breakpoint insert/list/remove uses session VM; - - debugger setup does not initialize `firmware.vm` as the canonical app context. - -## Affected Artifacts - -- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` -- `crates/console/prometeu-system/src/os/facades/vm.rs` -- `crates/console/prometeu-system/src/services/vm_session.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- Debugger-related tests in host and firmware crates diff --git a/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md b/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md deleted file mode 100644 index df572e05..00000000 --- a/discussion/workflow/plans/PLN-0155-retire-legacy-firmware-vm-runtime-bridge.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -id: PLN-0155 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Retire Legacy Firmware VM Runtime Bridge -status: done -created: 2026-07-05 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 states that `Firmware.vm` must not remain the canonical owner of VM-backed app context. PLN-0148 through PLN-0152 moved normal cartridge loading and ticking toward VM sessions, but a transitional legacy bridge still initializes and exposes the old global VM/runtime path for compatibility. - -## Objective - -Remove the legacy firmware VM/runtime bridge from normal runtime architecture. After this plan, VM-backed app execution, initialization, active runtime queries, and host-facing inspection must resolve through VM sessions or explicitly global render/runtime services. - -## Dependencies - -- Source decision: DEC-0038. -- Requires PLN-0154 so debugger operations no longer depend on `firmware.vm`. -- Builds on PLN-0148 through PLN-0153. - -## Scope - -- Remove `Firmware.vm` from `crates/console/prometeu-firmware/src/firmware/firmware.rs` if all callsites have migrated. -- Remove `PrometeuContext.vm` from `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs`. -- Remove `legacy_vm` from `SessionsFacade::load_vm_cartridge`. -- Remove `initialize_legacy_active_vm`. -- Remove or narrow global `VmFacade::initialize`, `tick`, `reset`, and `debug_step_instruction` methods that operate on caller-supplied `VirtualMachine`. -- Keep global render ownership, render worker lifecycle, telemetry publication, and log service where they are global SystemOS responsibilities. -- Preserve Hub/Home rendering behavior while avoiding any guest VM state stored in firmware. -- Update direct boot, debug boot, reset, and load-cartridge tests to prove session ownership is the only app VM path. - -## Non-Goals - -- Do not remove `VirtualMachineRuntime` itself. -- Do not redesign render worker ownership. -- Do not add background execution. -- Do not change debugger protocol shape beyond the PLN-0154 migration. - -## Execution Method - -1. Use `rg` to list every remaining `firmware.vm`, `ctx.vm`, `legacy_vm`, and `os.vm().initialize/tick/reset/debug_step_instruction` callsite. -2. Replace firmware context construction so state steps receive `SystemOS`, Hub, signals, platform, and boot target only. -3. Replace reset behavior with session registry/runtime reset semantics: - - clear VM sessions on firmware reset when booting fresh; - - preserve only explicitly resident sessions during lifecycle flows, not reset. -4. Update cartridge loading to initialize only the selected VM session. -5. Split `VmFacade` into: - - session-targeted VM execution/inspection methods; - - global render/runtime authority methods. -6. Delete compatibility methods that accept `&mut VirtualMachine` when no remaining production callsite exists. -7. Add tests or assertions proving no production code path initializes or ticks a global firmware VM. -8. Run full affected test suites and clippy. - -## Acceptance Criteria - -- `Firmware` no longer owns `pub vm: VirtualMachine`. -- `PrometeuContext` no longer carries a VM reference. -- `LoadCartridgeStep` initializes only session-owned VM context. -- `GameRunningStep` and VM-backed `ShellRunningStep` tick only sessions. -- Debugger, stats, and host runner access active session state or global render services explicitly. -- `rg "firmware\\.vm|ctx\\.vm|legacy_vm|os\\.vm\\(\\)\\.initialize\\(|os\\.vm\\(\\)\\.tick\\("` finds no production runtime dependency on the legacy global VM path. -- Tests continue to prove Game resident + VM Shell coexistence. - -## 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` -- Add or update tests for: - - reset clears session-owned VM contexts appropriately; - - direct boot creates session-owned VM context; - - debug boot uses session-owned VM context; - - no global VM tick path remains. - -## Affected Artifacts - -- `crates/console/prometeu-firmware/src/firmware/firmware.rs` -- `crates/console/prometeu-firmware/src/firmware/prometeu_context.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_*.rs` -- `crates/console/prometeu-system/src/os/facades/sessions.rs` -- `crates/console/prometeu-system/src/os/facades/vm.rs` -- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` -- `crates/host/prometeu-host-desktop-winit/src/runner.rs` -- `crates/host/prometeu-host-desktop-winit/src/stats.rs` diff --git a/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md b/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md deleted file mode 100644 index b84a935d..00000000 --- a/discussion/workflow/plans/PLN-0156-harden-vm-session-lifecycle-error-boundaries.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -id: PLN-0156 -ticket: foreground-stack-game-pause-shell-vm-backed -title: Harden VM Session Lifecycle Error Boundaries -status: done -created: 2026-07-05 -ref_decisions: [DEC-0038] -tags: [runtime, os, lifecycle, shell, game, vm, foreground, architecture] ---- - -## Briefing - -DEC-0038 makes VM session services part of the runtime ownership boundary. Some current session creation paths still use `expect` for lifecycle failures and missing task/process/session invariants. That is acceptable for short-term scaffolding, but session services should expose typed errors at operational boundaries so callers can choose controlled crash, rejection, or recovery behavior. - -## Objective - -Replace operational `expect`/panic paths in VM session creation/loading with typed errors and controlled firmware outcomes. Keep internal invariant assertions only where impossible states have already been proven by prior checks. - -## Dependencies - -- Source decision: DEC-0038. -- Builds on PLN-0148 through PLN-0152. -- Can be implemented before or after PLN-0154. -- Should be implemented before broadening VM session scheduling or adding background-capable processes. - -## Scope - -- Add or extend typed errors for session/lifecycle operations: - - lifecycle rejection while creating Game/Shell task; - - missing task for session creation; - - missing process for session creation; - - duplicate session; - - VM-backed session missing during cartridge initialization; - - foreground Shell already exists. -- Convert `SessionsFacade::create_vm_shell_task` to a fallible equivalent and keep an infallible wrapper only for test/setup code if needed. -- Convert `create_vm_session_for_task` to avoid `expect` for task/process lookup. -- Convert `initialize_session_cartridge` to return typed missing-session errors instead of panicking. -- Map session/lifecycle errors into controlled `CrashReport` or launch rejection behavior in firmware. -- Preserve existing V1 policy: - - one resident Game; - - one foreground Shell; - - no Game-to-Game switching; - - native Shell creates no VM session. - -## Non-Goals - -- Do not change DEC-0038 ownership semantics. -- Do not add background execution. -- Do not redesign the foreground stack. -- Do not hide programmer invariants behind overly broad error swallowing. - -## Execution Method - -1. Introduce a `SessionError` or extend `VmSessionError` with operational cases that cross facade boundaries. -2. Update `SessionsFacade` methods: - - `try_create_vm_game_task`; - - new `try_create_vm_shell_task`; - - `load_vm_cartridge`; - - `create_vm_session_for_task`; - - `initialize_session_cartridge`. -3. Update firmware loading paths to map typed session errors into: - - controlled `CrashReport::VmPanic` for unrecoverable internal failures; - - logged rejection and no state transition for unsupported lifecycle transitions where appropriate. -4. Update tests that currently rely on panics or infallible creation. -5. Add focused tests for: - - duplicate foreground Shell rejection during VM Shell load; - - missing task/process session creation errors; - - missing VM session during initialization; - - native Shell still creates no VM session. -6. Run system, firmware, host tests and clippy. - -## Acceptance Criteria - -- VM session facade operational failures are represented by typed errors. -- `create_vm_shell_task` has a fallible path used by production cartridge loading. -- VM Shell load does not panic when lifecycle rejects the Shell; it returns a controlled error/outcome. -- Missing task/process/session conditions in session services return typed errors. -- Existing tests for resident Game, native Shell, VM Shell, and Game/Shell coexistence still pass. -- No new broad `unwrap`/`expect` appears in production session/lifecycle paths. - -## 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` -- Add tests for controlled errors from session creation/loading edge cases. - -## Affected Artifacts - -- `crates/console/prometeu-system/src/os/facades/sessions.rs` -- `crates/console/prometeu-system/src/services/vm_session.rs` -- `crates/console/prometeu-system/src/os/lifecycle.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` -- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs` -- Related system and firmware tests