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]