Compare commits

...

10 Commits

Author SHA1 Message Date
e01b463e1f
implements PLN-0153
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
2026-07-04 19:42:59 +01:00
b8fc5a0534
implements PLN-0152 2026-07-04 19:41:35 +01:00
f26731413f
implements PLN-0151 2026-07-04 19:40:05 +01:00
fa2e329d4b
implements PLN-0150 2026-07-04 19:36:24 +01:00
c3d6b70b0c
implements PLN-0149 2026-07-04 19:34:04 +01:00
49a676ff86
implements PLN-0148 2026-07-04 19:30:00 +01:00
23a181de46
VM Context Ownership for Resident Game and VM-Backed Shell 2026-07-04 19:25:55 +01:00
bd27f5eb63
implements PLN-0147 2026-07-04 18:55:24 +01:00
6646ccfbb5
implements PLN-0146 2026-07-04 18:52:49 +01:00
0ecab2910b
implements PLN-0145 2026-07-04 18:52:04 +01:00
30 changed files with 1921 additions and 122 deletions

View File

@ -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,32 @@ 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<FirmwareState> {
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();
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.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 {
@ -367,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,
@ -380,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();
@ -1015,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());

View File

@ -14,6 +14,7 @@ pub enum FirmwareState {
LaunchHub(LaunchHubStep),
HubHome(HubHomeStep),
LoadCartridge(LoadCartridgeStep),
ResumeResidentGame,
GameRunning(GameRunningStep),
ShellRunning(ShellRunningStep),
AppCrashes(AppCrashesStep),

View File

@ -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));

View File

@ -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,42 +40,19 @@ 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()
&& let Some(resident_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
.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,

View File

@ -14,11 +14,13 @@ use prometeu_system::windows::WindowOwner;
pub struct LoadCartridgeStep {
pub cartridge: Cartridge,
init_error: Option<CrashReport>,
loaded_task_id: Option<prometeu_system::task::TaskId>,
loaded_app_mode: Option<AppMode>,
}
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<FirmwareState> {
@ -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)))
}

View File

@ -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_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 => {
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));
}

View File

@ -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;

View File

@ -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<ProcessState, LifecycleError> {
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<ProcessKind, LifecycleError> {
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<ProcessId, Life
Ok(task.process_id)
}
fn process_for_task(os: &SystemOS, task_id: TaskId) -> 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 } => {

View File

@ -1,5 +1,19 @@
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,
@ -7,15 +21,27 @@ pub struct SessionsFacade<'a> {
impl<'a> SessionsFacade<'a> {
pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> 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<String>,
) -> Result<TaskId, LifecycleError> {
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<String>) -> TaskId {
@ -26,6 +52,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 +69,101 @@ impl<'a> SessionsFacade<'a> {
task_id
}
pub fn load_vm_cartridge(
&mut self,
legacy_vm: &mut VirtualMachine,
cartridge: &Cartridge,
) -> Result<LoadedVmCartridge, CrashReport> {
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)
}
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)
}
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();
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(|_| ())
}
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)
}
}

View File

@ -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<CrashReport> {
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<GameLifecycleEvent> {
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<B, S>(&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)
}
}

View File

@ -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<GameLifecycleEvent>,
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,99 @@ 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 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);

View File

@ -1,3 +1,4 @@
use crate::task::TaskId;
use crate::{CrashReport, GameLibrary, SystemOS};
use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect;
@ -193,6 +194,45 @@ 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,
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 +438,34 @@ fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, 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 +586,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);
}
}

View File

@ -6,4 +6,5 @@ pub mod memcard;
pub mod process;
pub mod task;
pub mod vm_runtime;
pub mod vm_session;
pub mod windows;

View File

@ -0,0 +1,277 @@
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);
#[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,
pub runtime: VirtualMachineRuntime,
pub fs_state: FsState,
pub open_files: HashMap<u32, String>,
pub next_handle: u32,
}
impl VmSession {
fn new(
task: &Task,
process: &Process,
app_mode: AppMode,
app_version: impl Into<String>,
cap_config: Option<CertificationConfig>,
logs_count: Arc<AtomicU32>,
) -> 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(),
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)]
pub struct VmSessionRegistry {
sessions: HashMap<VmSessionId, VmSession>,
}
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<String>,
cap_config: Option<CertificationConfig>,
logs_count: Arc<AtomicU32>,
) -> Result<VmSessionId, VmSessionError> {
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, cap_config, logs_count),
);
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<AppMode, VmSessionError> {
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, "", 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));
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);
assert_eq!(session.runtime.current_app_id, 0);
assert_eq!(session.fs_state, FsState::Unmounted);
assert_eq!(session.next_handle, 1);
}
#[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", 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);
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, "", None, Arc::new(AtomicU32::new(0))),
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, "", None, Arc::new(AtomicU32::new(0)))
.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);
}
#[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()
);
}
}

View File

@ -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":"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"},{"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"}]}

View File

@ -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.

View File

@ -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`.

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -0,0 +1,92 @@
---
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

View File

@ -0,0 +1,99 @@
---
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`

View File

@ -0,0 +1,90 @@
---
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

View File

@ -0,0 +1,92 @@
---
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`

View File

@ -0,0 +1,86 @@
---
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

View File

@ -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: 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

View File

@ -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.

View File

@ -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.

View File

@ -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:

View File

@ -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)`