diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 93787873..2c88ee1f 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -339,8 +339,8 @@ mod tests { firmware.tick(&signals, &mut hardware); assert!(matches!(firmware.state, FirmwareState::SystemRunning(_))); - assert!(firmware.hub.window_manager.focused.is_some()); - assert_eq!(firmware.hub.window_manager.windows.len(), 1); + assert!(firmware.os.window_manager.focused.is_some()); + assert_eq!(firmware.os.window_manager.windows.len(), 1); } #[test] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs index 050ffe0c..8133a3cc 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs @@ -15,12 +15,7 @@ impl AppCrashesStep { } pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log( - LogLevel::Error, - LogSource::Pos, - self.report.log_tag(), - self.log_message(), - ); + ctx.os.log(LogLevel::Error, LogSource::Pos, self.report.log_tag(), self.log_message()); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 508cfd16..63fdfda1 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -15,12 +15,7 @@ impl GameRunningStep { } pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log( - LogLevel::Info, - LogSource::Pos, - 0, - "Entering GameRunning".to_string(), - ); + ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Entering GameRunning".to_string()); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { @@ -40,7 +35,7 @@ impl GameRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - let result = ctx.os.vm_runtime.tick(ctx.vm, ctx.signals, ctx.hw); + let result = ctx.os.tick_vm(ctx.vm, ctx.signals, ctx.hw); if !ctx.os.vm_runtime.logical_frame_active { ctx.hw.gfx_mut().present(); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index bd558cae..8ad987b5 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -7,7 +7,7 @@ pub struct HubHomeStep; impl HubHomeStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log(LogLevel::Info, LogSource::Hub, 0, "Entering HubHome".to_string()); + ctx.os.log(LogLevel::Info, LogSource::Hub, 0, "Entering HubHome".to_string()); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs index c3f73043..eb20a8c4 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs @@ -9,7 +9,7 @@ pub struct LaunchHubStep; impl LaunchHubStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log(LogLevel::Info, LogSource::Pos, 0, "Launching Hub".to_string()); + ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Launching Hub".to_string()); ctx.hub.init(); } @@ -22,7 +22,7 @@ impl LaunchHubStep { return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge))); } Err(e) => { - ctx.os.vm_runtime.log( + ctx.os.log( LogLevel::Error, LogSource::Pos, 0, diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs index 4b2951c5..fdc4ae7a 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs @@ -20,7 +20,7 @@ impl LoadCartridgeStep { } pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log( + ctx.os.log( LogLevel::Info, LogSource::Pos, 0, @@ -34,7 +34,7 @@ impl LoadCartridgeStep { self.cartridge.assets.clone(), ); - self.init_error = ctx.os.vm_runtime.initialize_vm(ctx.vm, &self.cartridge).err(); + self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err(); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { @@ -43,12 +43,12 @@ impl LoadCartridgeStep { } if self.cartridge.app_mode == AppMode::Shell { - let id = ctx.hub.window_manager.add_window( + let id = ctx.os.window_manager.add_window( self.cartridge.title.clone(), Rect { x: 40, y: 20, w: 240, h: 140 }, Color::WHITE, ); - ctx.hub.window_manager.set_focus(id); + ctx.os.window_manager.set_focus(id); let task_id = ctx.os.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone()); diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs index 8718369e..20cde51d 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs @@ -8,7 +8,7 @@ pub struct ResetStep; impl ResetStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string()); + ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string()); ctx.os.vm_runtime.reset(ctx.vm); } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs index da1e14d1..6695a7dd 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs @@ -10,12 +10,7 @@ pub struct SplashScreenStep { impl SplashScreenStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log( - LogLevel::Info, - LogSource::Pos, - 0, - "Showing SplashScreen".to_string(), - ); + ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Showing SplashScreen".to_string()); // Play sound on enter // ctx.hw.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off); } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs index 71c33289..d7dd7ab8 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs @@ -14,12 +14,7 @@ impl SystemRunningStep { } pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.vm_runtime.log( - LogLevel::Info, - LogSource::Hub, - 0, - "Entering SystemRunning".to_string(), - ); + ctx.os.log(LogLevel::Info, LogSource::Hub, 0, "Entering SystemRunning".to_string()); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 4cb3affd..78a9ef7f 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -2,7 +2,6 @@ mod crash_report; mod os; mod programs; mod services; -mod virtual_machine_runtime; pub use crash_report::CrashReport; pub use os::SystemOS; @@ -10,4 +9,4 @@ pub use programs::PrometeuHub; pub use services::fs; pub use services::process; pub use services::task; -pub use virtual_machine_runtime::VirtualMachineRuntime; +pub use services::vm_runtime::VirtualMachineRuntime; diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 78500121..8f9fecee 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -1,22 +1,101 @@ +use crate::CrashReport; use crate::VirtualMachineRuntime; +use crate::fs::{FsBackend, FsState, VirtualFS}; use crate::process::ProcessManager; +use crate::services::memcard::MemcardService; +use crate::services::window_manager::WindowManager; use crate::task::TaskId; use crate::task::TaskManager; +use prometeu_hal::cartridge::Cartridge; +use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::telemetry::CertificationConfig; +use prometeu_hal::{HardwareBridge, InputSignals}; +use prometeu_vm::VirtualMachine; +use std::collections::HashMap; +use std::sync::Arc; pub struct SystemOS { pub vm_runtime: VirtualMachineRuntime, pub process_manager: ProcessManager, pub task_manager: TaskManager, + pub window_manager: WindowManager, + pub log_service: LogService, + pub fs: VirtualFS, + pub fs_state: FsState, + pub memcard: MemcardService, + pub open_files: HashMap, + pub next_handle: u32, } impl SystemOS { pub fn new(cap_config: Option) -> Self { - Self { - vm_runtime: VirtualMachineRuntime::new(cap_config), + let log_service = LogService::new(4096); + let mut os = Self { + vm_runtime: VirtualMachineRuntime::new_with_log_counter( + cap_config, + Arc::clone(&log_service.logs_count), + ), process_manager: ProcessManager::new(), task_manager: TaskManager::new(), - } + window_manager: WindowManager::new(), + log_service, + fs: VirtualFS::new(), + fs_state: FsState::Unmounted, + memcard: MemcardService::new(), + open_files: HashMap::new(), + next_handle: 1, + }; + + os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string()); + + os + } + + pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) { + self.vm_runtime.log(&mut self.log_service, level, source, tag, msg); + } + + pub fn initialize_vm( + &mut self, + vm: &mut VirtualMachine, + cartridge: &Cartridge, + ) -> Result<(), CrashReport> { + self.clear_cartridge_service_state(); + self.vm_runtime.initialize_vm(&mut self.log_service, vm, cartridge) + } + + pub fn tick_vm( + &mut self, + vm: &mut VirtualMachine, + signals: &InputSignals, + hw: &mut dyn HardwareBridge, + ) -> Option { + self.vm_runtime.tick( + &mut self.log_service, + &mut self.fs, + &mut self.fs_state, + &mut self.memcard, + &mut self.open_files, + &mut self.next_handle, + vm, + signals, + hw, + ) + } + + pub fn mount_fs(&mut self, backend: Box) { + self.vm_runtime.mount_fs(&mut self.log_service, &mut self.fs, &mut self.fs_state, backend); + } + + pub fn unmount_fs(&mut self) { + self.fs.unmount(); + self.fs_state = FsState::Unmounted; + } + + fn clear_cartridge_service_state(&mut self) { + self.open_files.clear(); + self.next_handle = 1; + self.memcard.clear_all_staging(); } pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into) -> TaskId { diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs index 6549fd0f..02d05735 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs @@ -1,5 +1,4 @@ #[allow(clippy::module_inception)] mod prometeu_hub; -mod window_manager; pub use prometeu_hub::PrometeuHub; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 6d013dd9..333ff302 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -1,4 +1,3 @@ -use crate::programs::prometeu_hub::window_manager::WindowManager; use crate::{CrashReport, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; @@ -7,9 +6,7 @@ use prometeu_hal::{HardwareBridge, InputSignals}; use prometeu_vm::VirtualMachine; /// PrometeuHub: Launcher and system UI environment. -pub struct PrometeuHub { - pub window_manager: WindowManager, -} +pub struct PrometeuHub; pub struct SystemProfileUpdate { pub crash: Option, @@ -24,7 +21,7 @@ impl Default for PrometeuHub { impl PrometeuHub { pub fn new() -> Self { - Self { window_manager: WindowManager::new() } + Self } pub fn init(&mut self) { @@ -37,41 +34,41 @@ impl PrometeuHub { let mut next_window = None; if hw.pad().a().pressed { - os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string()); + os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string()); next_window = Some(( "Green Window".to_string(), Rect { x: 0, y: 0, w: 160, h: 90 }, Color::GREEN, )); } else if hw.pad().b().pressed { - os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string()); + os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string()); next_window = Some(( "Indigo Window".to_string(), Rect { x: 160, y: 0, w: 160, h: 90 }, Color::INDIGO, )); } else if hw.pad().x().pressed { - os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string()); + os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string()); next_window = Some(( "Yellow Window".to_string(), Rect { x: 0, y: 90, w: 160, h: 90 }, Color::YELLOW, )); } else if hw.pad().y().pressed { - os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string()); + os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string()); next_window = Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED)); } if let Some((title, rect, color)) = next_window { - self.window_manager.remove_all_windows(); - let id = self.window_manager.add_window(title, rect, color); - self.window_manager.set_focus(id); + os.window_manager.remove_all_windows(); + let id = os.window_manager.add_window(title, rect, color); + os.window_manager.set_focus(id); } } - pub fn render(&mut self, _os: &mut SystemOS, hw: &mut dyn HardwareBridge) { - for window in &self.window_manager.windows { + pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) { + for window in &os.window_manager.windows { hw.gfx_mut().fill_rect( window.viewport.x, window.viewport.y, @@ -93,17 +90,17 @@ impl PrometeuHub { self.gui_update(os, hw); - if let Some(focused_id) = self.window_manager.focused { + if let Some(focused_id) = os.window_manager.focused { if hw.pad().start().down { - self.window_manager.remove_window(focused_id); + os.window_manager.remove_window(focused_id); } else { - crash = os.vm_runtime.tick(vm, signals, hw); + crash = os.tick_vm(vm, signals, hw); } } self.render(os, hw); hw.gfx_mut().present(); - SystemProfileUpdate { crash, focused_window_active: self.window_manager.focused.is_some() } + SystemProfileUpdate { crash, focused_window_active: os.window_manager.focused.is_some() } } } diff --git a/crates/console/prometeu-system/src/services/mod.rs b/crates/console/prometeu-system/src/services/mod.rs index afdf1642..53a750cd 100644 --- a/crates/console/prometeu-system/src/services/mod.rs +++ b/crates/console/prometeu-system/src/services/mod.rs @@ -2,3 +2,5 @@ pub mod fs; pub mod memcard; pub mod process; pub mod task; +pub mod vm_runtime; +pub mod window_manager; diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs similarity index 92% rename from crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs rename to crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs index 7c7f945b..e73c2d03 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs @@ -1,5 +1,6 @@ use super::*; -use crate::services::memcard::{MemcardSlotState, MemcardStatus}; +use crate::fs::{FsState, VirtualFS}; +use crate::services::memcard::{MemcardService, MemcardSlotState, MemcardStatus}; use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef}; use prometeu_hal::cartridge::AppMode; @@ -13,10 +14,40 @@ use prometeu_hal::{ AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int, }; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::Ordering; -impl VirtualMachineRuntime { +pub(crate) struct VmRuntimeHost<'a> { + pub runtime: &'a mut VirtualMachineRuntime, + pub log_service: &'a mut LogService, + pub fs: &'a mut VirtualFS, + pub fs_state: &'a mut FsState, + pub memcard: &'a mut MemcardService, + pub open_files: &'a mut HashMap, + pub next_handle: &'a mut u32, +} + +impl Deref for VmRuntimeHost<'_> { + type Target = VirtualMachineRuntime; + + fn deref(&self) -> &Self::Target { + self.runtime + } +} + +impl DerefMut for VmRuntimeHost<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.runtime + } +} + +impl VmRuntimeHost<'_> { + fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) { + self.runtime.log(self.log_service, level, source, tag, msg); + } + fn panic_payload_to_string(payload: Box) -> String { if let Some(message) = payload.downcast_ref::() { return message.clone(); @@ -40,8 +71,8 @@ impl VirtualMachineRuntime { let app_id = self.current_app_id; let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0); - if count >= Self::MAX_LOGS_PER_FRAME { - if count == Self::MAX_LOGS_PER_FRAME { + if count >= VirtualMachineRuntime::MAX_LOGS_PER_FRAME { + if count == VirtualMachineRuntime::MAX_LOGS_PER_FRAME { self.logs_written_this_frame.insert(app_id, count + 1); self.log( LogLevel::Warn, @@ -56,8 +87,8 @@ impl VirtualMachineRuntime { self.logs_written_this_frame.insert(app_id, count + 1); let mut final_msg = msg; - if final_msg.len() > Self::MAX_LOG_LEN { - final_msg.truncate(Self::MAX_LOG_LEN); + if final_msg.len() > VirtualMachineRuntime::MAX_LOG_LEN { + final_msg.truncate(VirtualMachineRuntime::MAX_LOG_LEN); } self.log(level, LogSource::App { app_id }, tag, final_msg); @@ -122,7 +153,7 @@ impl VirtualMachineRuntime { } } -impl NativeInterface for VirtualMachineRuntime { +impl NativeInterface for VmRuntimeHost<'_> { fn syscall( &mut self, id: SyscallId, @@ -395,13 +426,13 @@ impl NativeInterface for VirtualMachineRuntime { } Syscall::FsOpen => { let path = expect_string(args, 0, "path")?; - if self.fs_state != FsState::Mounted { + if *self.fs_state != FsState::Mounted { ret.push_int(-1); return Ok(()); } - let handle = self.next_handle; + let handle = *self.next_handle; self.open_files.insert(handle, path); - self.next_handle += 1; + *self.next_handle += 1; ret.push_int(handle as i64); Ok(()) } @@ -410,8 +441,9 @@ impl NativeInterface for VirtualMachineRuntime { let path = self .open_files .get(&handle) - .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; - match self.fs.read_file(path) { + .ok_or_else(|| VmFault::Panic("Invalid handle".into()))? + .clone(); + match self.fs.read_file(&path) { Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()), Err(_) => ret.push_null(), } @@ -423,8 +455,9 @@ impl NativeInterface for VirtualMachineRuntime { let path = self .open_files .get(&handle) - .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; - match self.fs.write_file(path, &content) { + .ok_or_else(|| VmFault::Panic("Invalid handle".into()))? + .clone(); + match self.fs.write_file(&path, &content) { Ok(_) => ret.push_bool(true), Err(_) => ret.push_bool(false), } @@ -476,7 +509,7 @@ impl NativeInterface for VirtualMachineRuntime { ret.push_int(0); return Ok(()); } - let stat = self.memcard.slot_stat(&self.fs, self.current_app_id, slot); + let stat = self.memcard.slot_stat(self.fs, self.current_app_id, slot); let status = if stat.state == MemcardSlotState::Corrupt { MemcardStatus::Corrupt } else { @@ -500,7 +533,7 @@ impl NativeInterface for VirtualMachineRuntime { return Ok(()); } let read = - self.memcard.slot_read(&self.fs, self.current_app_id, slot, offset, max_bytes); + self.memcard.slot_read(self.fs, self.current_app_id, slot, offset, max_bytes); ret.push_int(read.status as i64); ret.push_string(hex_encode(&read.bytes)); ret.push_int(read.bytes_read as i64); @@ -516,8 +549,8 @@ impl NativeInterface for VirtualMachineRuntime { return Ok(()); } let payload = hex_decode(&payload_hex)?; - let write = - self.memcard.slot_write(&self.fs, self.current_app_id, slot, offset, &payload); + let app_id = self.current_app_id; + let write = self.memcard.slot_write(self.fs, app_id, slot, offset, &payload); ret.push_int(write.status as i64); ret.push_int(write.bytes_written as i64); Ok(()) @@ -529,9 +562,10 @@ impl NativeInterface for VirtualMachineRuntime { return Ok(()); } let status = { + let app_id = self.current_app_id; let memcard = &mut self.memcard; let fs = &mut self.fs; - memcard.slot_commit(fs, self.current_app_id, slot) + memcard.slot_commit(fs, app_id, slot) }; ret.push_int(status as i64); Ok(()) @@ -543,9 +577,10 @@ impl NativeInterface for VirtualMachineRuntime { return Ok(()); } let status = { + let app_id = self.current_app_id; let memcard = &mut self.memcard; let fs = &mut self.fs; - memcard.slot_clear(fs, self.current_app_id, slot) + memcard.slot_clear(fs, app_id, slot) }; ret.push_int(status as i64); Ok(()) diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs similarity index 69% rename from crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs rename to crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs index 232c35d6..f74810f8 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/lifecycle.rs @@ -1,26 +1,23 @@ use super::*; use crate::CrashReport; -use crate::fs::FsBackend; +use crate::fs::{FsBackend, FsState, VirtualFS}; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::{LogLevel, LogSource}; +use std::collections::HashMap; impl VirtualMachineRuntime { - pub fn new(cap_config: Option) -> Self { + pub fn new_with_log_counter( + cap_config: Option, + logs_count: Arc, + ) -> Self { let boot_time = Instant::now(); - let log_service = LogService::new(4096); - let atomic_telemetry = Arc::new(AtomicTelemetry::new(Arc::clone(&log_service.logs_count))); - let mut os = Self { + let atomic_telemetry = Arc::new(AtomicTelemetry::new(logs_count)); + Self { tick_index: 0, logical_frame_index: 0, logical_frame_active: false, logical_frame_remaining_cycles: 0, last_frame_cpu_time_us: 0, - fs: VirtualFS::new(), - fs_state: FsState::Unmounted, - memcard: MemcardService::new(), - open_files: HashMap::new(), - next_handle: 1, - log_service, current_app_id: 0, current_cartridge_title: String::new(), current_cartridge_app_version: String::new(), @@ -36,25 +33,41 @@ impl VirtualMachineRuntime { frame_start_string_materializations: 0, needs_prepare_entry_call: false, boot_time, - }; - - os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string()); - - os + } } - pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) { + pub fn log( + &self, + log_service: &mut LogService, + level: LogLevel, + source: LogSource, + tag: u16, + msg: String, + ) { let ts_ms = self.boot_time.elapsed().as_millis() as u64; let frame = self.logical_frame_index; - self.log_service.log(ts_ms, frame, level, source, tag, msg); + log_service.log(ts_ms, frame, level, source, tag, msg); } - pub fn mount_fs(&mut self, backend: Box) { - self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string()); - match self.fs.mount(backend) { + pub fn mount_fs( + &mut self, + log_service: &mut LogService, + fs: &mut VirtualFS, + fs_state: &mut FsState, + backend: Box, + ) { + self.log( + log_service, + LogLevel::Info, + LogSource::Fs, + 0, + "Attempting to mount filesystem".to_string(), + ); + match fs.mount(backend) { Ok(_) => { - self.fs_state = FsState::Mounted; + *fs_state = FsState::Mounted; self.log( + log_service, LogLevel::Info, LogSource::Fs, 0, @@ -63,26 +76,32 @@ impl VirtualMachineRuntime { } Err(e) => { let err_msg = format!("Failed to mount filesystem: {:?}", e); - self.log(LogLevel::Error, LogSource::Fs, 0, err_msg); - self.fs_state = FsState::Error(e); + self.log(log_service, LogLevel::Error, LogSource::Fs, 0, err_msg); + *fs_state = FsState::Error(e); } } } - pub fn unmount_fs(&mut self) { - self.fs.unmount(); - self.fs_state = FsState::Unmounted; + pub fn unmount_fs(&mut self, fs: &mut VirtualFS, fs_state: &mut FsState) { + fs.unmount(); + *fs_state = FsState::Unmounted; } - pub(crate) fn update_fs(&mut self) { - if self.fs_state == FsState::Mounted && !self.fs.is_healthy() { + pub(crate) fn update_fs( + &mut self, + log_service: &mut LogService, + fs: &mut VirtualFS, + fs_state: &mut FsState, + ) { + if *fs_state == FsState::Mounted && !fs.is_healthy() { self.log( + log_service, LogLevel::Error, LogSource::Fs, 0, "Filesystem became unhealthy, unmounting".to_string(), ); - self.unmount_fs(); + self.unmount_fs(fs, fs_state); } } @@ -93,10 +112,6 @@ impl VirtualMachineRuntime { self.last_frame_cpu_time_us = 0; self.atomic_telemetry.reset(); - self.open_files.clear(); - self.next_handle = 1; - self.memcard.clear_all_staging(); - self.current_app_id = 0; self.current_cartridge_title.clear(); self.current_cartridge_app_version.clear(); @@ -120,6 +135,7 @@ impl VirtualMachineRuntime { pub fn initialize_vm( &mut self, + log_service: &mut LogService, vm: &mut VirtualMachine, cartridge: &Cartridge, ) -> Result<(), CrashReport> { @@ -138,6 +154,7 @@ impl VirtualMachineRuntime { let report = CrashReport::VmInit { error: e }; self.last_crash_report = Some(report.clone()); self.log( + log_service, LogLevel::Error, LogSource::Vm, report.log_tag(), diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime.rs b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs similarity index 84% rename from crates/console/prometeu-system/src/virtual_machine_runtime.rs rename to crates/console/prometeu-system/src/services/vm_runtime/mod.rs index 682ce694..8ecf6a5b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/mod.rs @@ -5,8 +5,6 @@ mod tests; mod tick; use crate::CrashReport; -use crate::fs::{FsState, VirtualFS}; -use crate::services::memcard::MemcardService; use prometeu_bytecode::string_materialization_count; use prometeu_hal::cartridge::AppMode; use prometeu_hal::log::LogService; @@ -14,6 +12,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier}; use prometeu_vm::VirtualMachine; use std::collections::HashMap; use std::sync::Arc; +use std::sync::atomic::AtomicU32; use std::time::Instant; pub struct VirtualMachineRuntime { @@ -22,12 +21,6 @@ pub struct VirtualMachineRuntime { pub logical_frame_active: bool, pub logical_frame_remaining_cycles: u64, pub last_frame_cpu_time_us: u64, - pub fs: VirtualFS, - pub fs_state: FsState, - pub memcard: MemcardService, - pub open_files: HashMap, - pub next_handle: u32, - pub log_service: LogService, pub current_app_id: u32, pub current_cartridge_title: String, pub current_cartridge_app_version: String, @@ -50,4 +43,8 @@ impl VirtualMachineRuntime { pub const SLICE_PER_TICK: u64 = 1_500_000; pub const MAX_LOG_LEN: usize = 256; pub const MAX_LOGS_PER_FRAME: u32 = 10; + + pub fn new(cap_config: Option) -> Self { + Self::new_with_log_counter(cap_config, Arc::new(AtomicU32::new(0))) + } } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs similarity index 70% rename from crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs rename to crates/console/prometeu-system/src/services/vm_runtime/tests.rs index 1867bcb9..8a840997 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -1,5 +1,6 @@ use super::*; -use crate::fs::{FsBackend, FsEntry, FsError}; +use crate::fs::{FsBackend, FsEntry, FsError, FsState, VirtualFS}; +use crate::services::memcard::MemcardService; use prometeu_bytecode::TRAP_TYPE; use prometeu_bytecode::Value; use prometeu_bytecode::assembler::assemble; @@ -188,6 +189,7 @@ mod fs_memcard; #[test] fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); let mut vm = VirtualMachine::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -202,7 +204,7 @@ fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { ); let cartridge = cartridge_with_program(program, caps::NONE); - let res = runtime.initialize_vm(&mut vm, &cartridge); + let res = runtime.initialize_vm(&mut log_service, &mut vm, &cartridge); assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); assert_eq!(runtime.current_app_id, 0); @@ -213,6 +215,7 @@ fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { #[test] fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); let mut vm = VirtualMachine::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -227,7 +230,7 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { ); let cartridge = cartridge_with_program(program, caps::GFX); - let res = runtime.initialize_vm(&mut vm, &cartridge); + let res = runtime.initialize_vm(&mut log_service, &mut vm, &cartridge); assert!(res.is_ok()); assert_eq!(runtime.current_app_id, 42); @@ -237,6 +240,12 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { #[test] fn tick_returns_error_when_vm_ends_slice_with_trap() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -253,10 +262,21 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() { ); let cartridge = cartridge_with_program(program, caps::GFX); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); - let report = - runtime.tick(&mut vm, &signals, &mut hardware).expect("trap must surface as runtime error"); + let report = runtime + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) + .expect("trap must surface as runtime error"); match report { CrashReport::VmTrap { trap } => { @@ -271,6 +291,12 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() { #[test] fn tick_system_profile_rejects_gfx_game_surface() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -287,8 +313,20 @@ fn tick_system_profile_rejects_gfx_game_surface() { ); let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::Shell); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware).expect("system gfx call must trap"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) + .expect("system gfx call must trap"); match report { CrashReport::VmTrap { trap } => { @@ -302,6 +340,12 @@ fn tick_system_profile_rejects_gfx_game_surface() { #[test] fn tick_system_profile_rejects_composer_game_surface() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -318,9 +362,20 @@ fn tick_system_profile_rejects_composer_game_surface() { ); let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::Shell); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = - runtime.tick(&mut vm, &signals, &mut hardware).expect("system composer call must trap"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) + .expect("system composer call must trap"); match report { CrashReport::VmTrap { trap } => { @@ -336,6 +391,12 @@ fn tick_system_profile_rejects_composer_game_surface() { #[test] fn tick_system_profile_rejects_bank_game_surface() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -352,9 +413,20 @@ fn tick_system_profile_rejects_bank_game_surface() { ); let cartridge = cartridge_with_program_and_mode(program, caps::BANK, AppMode::Shell); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = - runtime.tick(&mut vm, &signals, &mut hardware).expect("system bank call must trap"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) + .expect("system bank call must trap"); match report { CrashReport::VmTrap { trap } => { @@ -368,6 +440,12 @@ fn tick_system_profile_rejects_bank_game_surface() { #[test] fn tick_system_profile_can_use_shared_log_transport() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -385,8 +463,18 @@ fn tick_system_profile_can_use_shared_log_transport() { ); let cartridge = cartridge_with_program_and_mode(program, caps::LOG, AppMode::Shell); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "system log transport must remain internally shared"); assert!(vm.is_halted()); @@ -395,12 +483,28 @@ fn tick_system_profile_can_use_shared_log_transport() { #[test] fn tick_returns_panic_report_distinct_from_trap() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let report = runtime - .tick(&mut vm, &signals, &mut hardware) + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) .expect("panic must surface as runtime error"); match report { @@ -416,6 +520,12 @@ fn tick_returns_panic_report_distinct_from_trap() { #[test] fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let signals = InputSignals::default(); let program = @@ -432,8 +542,18 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { glyph_slot_index.rebuild_from_slots(&slots); assert!(hardware.frame_composer.bind_scene(0)); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "frame render path must not crash"); hardware.gfx.present(); @@ -443,6 +563,12 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { #[test] fn tick_renders_scene_through_public_composer_syscalls() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let signals = InputSignals::default(); let code = assemble( @@ -491,8 +617,18 @@ fn tick_renders_scene_through_public_composer_syscalls() { hardware.gfx.scene_fade_level = 31; hardware.gfx.hud_fade_level = 31; - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "public composer path must not crash"); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]); @@ -503,6 +639,12 @@ fn tick_renders_scene_through_public_composer_syscalls() { #[test] fn tick_draw_text_survives_scene_backed_frame_composition() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let signals = InputSignals::default(); let code = assemble( @@ -552,8 +694,18 @@ fn tick_draw_text_survives_scene_backed_frame_composition() { hardware.gfx.scene_fade_level = 31; hardware.gfx.hud_fade_level = 31; - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "scene-backed overlay text must not crash"); hardware.gfx.present(); @@ -563,6 +715,12 @@ fn tick_draw_text_survives_scene_backed_frame_composition() { #[test] fn tick_draw_text_survives_no_scene_frame_path() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let signals = InputSignals::default(); let code = assemble( @@ -585,8 +743,18 @@ fn tick_draw_text_survives_no_scene_frame_path() { hardware.gfx.scene_fade_level = 31; hardware.gfx.hud_fade_level = 31; - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "no-scene overlay text must not crash"); hardware.gfx.present(); @@ -596,6 +764,7 @@ fn tick_draw_text_survives_no_scene_frame_path() { #[test] fn initialize_vm_success_clears_previous_crash_report() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) }); let mut vm = VirtualMachine::default(); @@ -612,7 +781,7 @@ fn initialize_vm_success_clears_previous_crash_report() { ); let cartridge = cartridge_with_program(program, caps::GFX); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); assert!(runtime.last_crash_report.is_none()); } @@ -620,15 +789,16 @@ fn initialize_vm_success_clears_previous_crash_report() { fn reset_clears_cartridge_scoped_runtime_state() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); + let fs_state = FsState::Mounted; + let mut open_files: HashMap = HashMap::new(); + let next_handle = 9; runtime.tick_index = 77; runtime.logical_frame_index = 12; runtime.logical_frame_active = true; runtime.logical_frame_remaining_cycles = 345; runtime.last_frame_cpu_time_us = 678; - runtime.fs_state = FsState::Mounted; - runtime.open_files.insert(7, "/save.dat".into()); - runtime.next_handle = 9; + open_files.insert(7, "/save.dat".into()); runtime.current_app_id = 42; runtime.current_cartridge_title = "Cart".into(); runtime.current_cartridge_app_version = "1.2.3".into(); @@ -650,13 +820,13 @@ fn reset_clears_cartridge_scoped_runtime_state() { runtime.reset(&mut vm); assert_eq!(runtime.tick_index, 77); - assert_eq!(runtime.fs_state, FsState::Mounted); + assert_eq!(fs_state, FsState::Mounted); assert_eq!(runtime.logical_frame_index, 0); assert!(!runtime.logical_frame_active); assert_eq!(runtime.logical_frame_remaining_cycles, 0); assert_eq!(runtime.last_frame_cpu_time_us, 0); - assert!(runtime.open_files.is_empty()); - assert_eq!(runtime.next_handle, 1); + assert_eq!(open_files.len(), 1); + assert_eq!(next_handle, 9); assert_eq!(runtime.current_app_id, 0); assert!(runtime.current_cartridge_title.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty()); @@ -679,6 +849,12 @@ fn reset_clears_cartridge_scoped_runtime_state() { #[test] fn tick_numeric_happy_path_records_zero_internal_allocations() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -687,8 +863,18 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() { let program = serialized_single_function_module(code, vec![]); let cartridge = cartridge_with_program(program, caps::NONE); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none()); let snapshot = runtime.atomic_telemetry.snapshot(); @@ -699,6 +885,12 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() { #[test] fn tick_already_materialized_string_path_records_zero_internal_allocations() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -711,8 +903,18 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() { ); let cartridge = cartridge_with_program(program, caps::NONE); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none()); let snapshot = runtime.atomic_telemetry.snapshot(); @@ -723,6 +925,8 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() { #[test] fn initialize_vm_failure_clears_previous_identity_and_handles() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut open_files: HashMap = HashMap::new(); let mut vm = VirtualMachine::default(); let good_program = serialized_single_function_module( @@ -736,10 +940,13 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() { }], ); let good_cartridge = cartridge_with_program(good_program, caps::GFX); - runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize"); + runtime + .initialize_vm(&mut log_service, &mut vm, &good_cartridge) + .expect("runtime must initialize"); - runtime.open_files.insert(5, "/save.dat".into()); - runtime.next_handle = 6; + open_files.insert(5, "/save.dat".into()); + + let next_handle = 6; runtime.paused = true; runtime.debug_step_request = true; runtime.atomic_telemetry.cycles_used.store(123, Ordering::Relaxed); @@ -756,15 +963,15 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() { ); let bad_cartridge = cartridge_with_program(bad_program, caps::NONE); - let res = runtime.initialize_vm(&mut vm, &bad_cartridge); + let res = runtime.initialize_vm(&mut log_service, &mut vm, &bad_cartridge); assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); assert_eq!(runtime.current_app_id, 0); assert!(runtime.current_cartridge_title.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty()); assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); - assert!(runtime.open_files.is_empty()); - assert_eq!(runtime.next_handle, 1); + assert_eq!(open_files.len(), 1); + assert_eq!(next_handle, 6); assert!(!runtime.paused); assert!(!runtime.debug_step_request); assert_eq!(runtime.atomic_telemetry.cycles_used.load(Ordering::Relaxed), 0); @@ -774,6 +981,12 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() { #[test] fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -790,8 +1003,18 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::GFX); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!( @@ -803,6 +1026,12 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { #[test] fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -822,8 +1051,18 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::GFX); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]); @@ -832,6 +1071,12 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { #[test] fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -851,8 +1096,18 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::GFX); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "invalid layer must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]); @@ -861,6 +1116,12 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { #[test] fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -887,8 +1148,18 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { AssetsPayloadSource::from_bytes(asset_data), ); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "invalid composer parameter range must not crash"); assert!(vm.is_halted()); assert_eq!( @@ -900,6 +1171,12 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { #[test] fn tick_audio_play_sample_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -919,8 +1196,18 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::AUDIO); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]); @@ -929,6 +1216,12 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() { #[test] fn tick_audio_play_voice_invalid_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -948,8 +1241,18 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::AUDIO); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "invalid voice must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]); @@ -958,6 +1261,12 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() { #[test] fn tick_audio_play_missing_asset_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -977,8 +1286,18 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::AUDIO); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "missing audio asset must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::BankInvalid as i64)]); @@ -987,6 +1306,12 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() { #[test] fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -1006,9 +1331,20 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { ); let cartridge = cartridge_with_program(program, caps::AUDIO); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = - runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) + .expect("type mismatch must surface as trap"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE); @@ -1022,6 +1358,12 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { #[test] fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -1041,9 +1383,20 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() { ); let cartridge = cartridge_with_program(program, caps::GFX); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = - runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime + .tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ) + .expect("type mismatch must surface as trap"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE); diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs similarity index 67% rename from crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs rename to crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs index 39b99e4e..28584be8 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs @@ -3,6 +3,12 @@ use super::*; #[test] fn tick_asset_commit_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -19,8 +25,18 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::ASSET); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); @@ -29,6 +45,12 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() { #[test] fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -45,8 +67,18 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { ); let cartridge = cartridge_with_program(program, caps::ASSET); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "missing asset must not crash"); assert!(vm.is_halted()); assert_eq!( @@ -58,6 +90,12 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { #[test] fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -80,8 +118,18 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { ); let cartridge = cartridge_with_program(program, caps::ASSET); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "invalid slot must not crash"); assert!(vm.is_halted()); assert_eq!( @@ -93,6 +141,12 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { #[test] fn tick_asset_status_unknown_handle_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -109,8 +163,18 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::ASSET); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "unknown asset handle must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]); @@ -119,6 +183,12 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() { #[test] fn tick_bank_info_returns_slot_summary_not_json() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -141,8 +211,18 @@ fn tick_bank_info_returns_slot_summary_not_json() { ); let cartridge = cartridge_with_program(program, caps::BANK); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "bank summary must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(16), Value::Int64(1)]); @@ -151,6 +231,7 @@ fn tick_bank_info_returns_slot_summary_not_json() { #[test] fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); let mut vm = VirtualMachine::default(); let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( @@ -165,7 +246,7 @@ fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() { ); let cartridge = cartridge_with_program(program, caps::BANK); - let res = runtime.initialize_vm(&mut vm, &cartridge); + let res = runtime.initialize_vm(&mut log_service, &mut vm, &cartridge); assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); } @@ -173,6 +254,12 @@ fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() { #[test] fn tick_asset_commit_invalid_transition_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -207,8 +294,18 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { ); let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated"); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "invalid transition must not crash"); assert!(vm.is_halted()); assert_eq!(handle, 1); @@ -218,6 +315,12 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { #[test] fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -234,8 +337,18 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { ); let cartridge = cartridge_with_program(program, caps::ASSET); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "unknown handle cancel must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); @@ -244,6 +357,12 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { #[test] fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -268,7 +387,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { ); let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated"); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); loop { match hardware.assets.status(handle) { @@ -284,7 +403,17 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { hardware.assets.apply_commits(); assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "cancel after commit must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]); @@ -293,6 +422,12 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { #[test] fn tick_status_first_surface_smoke_across_composer_audio_and_asset() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -331,8 +466,18 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() { ); let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET); - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "mixed status-first surface must not crash"); assert!(vm.is_halted()); assert_eq!( diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs similarity index 69% rename from crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs rename to crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs index bfd30f5a..3d1c80d1 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_fs_memcard.rs @@ -3,7 +3,13 @@ use super::*; #[test] fn tick_memcard_slot_roundtrip_for_game_profile() { let mut runtime = VirtualMachineRuntime::new(None); - runtime.mount_fs(Box::new(MemFsBackend::default())); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; + runtime.mount_fs(&mut log_service, &mut fs, &mut fs_state, Box::new(MemFsBackend::default())); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -50,8 +56,18 @@ fn tick_memcard_slot_roundtrip_for_game_profile() { preload: vec![], }; - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "memcard roundtrip must not crash"); assert!(vm.is_halted()); assert_eq!( @@ -63,6 +79,12 @@ fn tick_memcard_slot_roundtrip_for_game_profile() { #[test] fn tick_memcard_access_is_denied_for_non_game_profile() { let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); @@ -89,8 +111,18 @@ fn tick_memcard_access_is_denied_for_non_game_profile() { preload: vec![], }; - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime.tick(&mut vm, &signals, &mut hardware); + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut hardware, + ); assert!(report.is_none(), "non-game memcard call must return status"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]); diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs similarity index 85% rename from crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs rename to crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 38c237f3..3456bf7a 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -1,9 +1,13 @@ +use super::dispatch::VmRuntimeHost; use super::*; use crate::CrashReport; +use crate::fs::{FsState, VirtualFS}; +use crate::services::memcard::MemcardService; use prometeu_hal::asset::{BankTelemetry, BankType}; -use prometeu_hal::log::{LogLevel, LogSource}; +use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::{HardwareBridge, HostContext, InputSignals}; use prometeu_vm::LogicalFrameEndingReason; +use std::collections::HashMap; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::Ordering; @@ -52,11 +56,30 @@ impl VirtualMachineRuntime { pub fn debug_step_instruction( &mut self, + log_service: &mut LogService, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge, ) -> Option { - let mut ctx = HostContext::new(Some(hw)); - match vm.step(self, &mut ctx) { + let step_result = { + let mut ctx = HostContext::new(Some(hw)); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files = HashMap::new(); + let mut next_handle = 1; + let mut host = VmRuntimeHost { + runtime: self, + log_service, + fs: &mut fs, + fs_state: &mut fs_state, + memcard: &mut memcard, + open_files: &mut open_files, + next_handle: &mut next_handle, + }; + vm.step(&mut host, &mut ctx) + }; + + match step_result { Ok(_) => None, Err(e) => { let report = match e { @@ -70,6 +93,7 @@ impl VirtualMachineRuntime { }, }; self.log( + log_service, LogLevel::Error, LogSource::Vm, report.log_tag(), @@ -83,6 +107,12 @@ impl VirtualMachineRuntime { pub fn tick( &mut self, + log_service: &mut LogService, + fs: &mut VirtualFS, + fs_state: &mut FsState, + mem_card: &mut MemcardService, + open_files: &mut HashMap, + next_handle: &mut u32, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge, @@ -94,7 +124,7 @@ impl VirtualMachineRuntime { return None; } - self.update_fs(); + self.update_fs(log_service, fs, fs_state); if !self.logical_frame_active { self.logical_frame_active = true; @@ -128,7 +158,16 @@ impl VirtualMachineRuntime { if budget > 0 { let run_result = { let mut ctx = HostContext::new(Some(hw)); - vm.run_budget(budget, self, &mut ctx) + let mut host = VmRuntimeHost { + runtime: self, + log_service, + fs, + fs_state, + memcard: mem_card, + open_files, + next_handle, + }; + vm.run_budget(budget, &mut host, &mut ctx) }; match run_result { @@ -143,6 +182,7 @@ impl VirtualMachineRuntime { self.paused = true; self.debug_step_request = false; self.log( + log_service, LogLevel::Info, LogSource::Vm, 0xDEB1, @@ -154,6 +194,7 @@ impl VirtualMachineRuntime { let report = CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) }; self.log( + log_service, LogLevel::Error, LogSource::Vm, report.log_tag(), @@ -166,6 +207,7 @@ impl VirtualMachineRuntime { if let LogicalFrameEndingReason::Trap(trap) = &run.reason { let report = CrashReport::VmTrap { trap: trap.clone() }; self.log( + log_service, LogLevel::Error, LogSource::Vm, report.log_tag(), @@ -182,6 +224,7 @@ impl VirtualMachineRuntime { let message = Self::host_panic_payload_to_string(payload); let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; self.log( + log_service, LogLevel::Error, LogSource::Vm, report.log_tag(), @@ -229,18 +272,15 @@ impl VirtualMachineRuntime { let ts_ms = self.boot_time.elapsed().as_millis() as u64; let telemetry_snapshot = self.atomic_telemetry.snapshot(); - let violations = self.certifier.evaluate( - &telemetry_snapshot, - &mut self.log_service, - ts_ms, - ) as u32; + let violations = + self.certifier.evaluate(&telemetry_snapshot, log_service, ts_ms) as u32; self.atomic_telemetry.violations.store(violations, Ordering::Relaxed); self.atomic_telemetry .completed_logical_frames .fetch_add(1, Ordering::Relaxed); - self.log_service.reset_count(); + log_service.reset_count(); self.logical_frame_index += 1; self.logical_frame_active = false; @@ -258,7 +298,13 @@ impl VirtualMachineRuntime { } Err(e) => { let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) }; - self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary()); + self.log( + log_service, + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + report.summary(), + ); self.last_crash_report = Some(report.clone()); return Some(report); } diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/window_manager.rs b/crates/console/prometeu-system/src/services/window_manager.rs similarity index 100% rename from crates/console/prometeu-system/src/programs/prometeu_hub/window_manager.rs rename to crates/console/prometeu-system/src/services/window_manager.rs diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index 5430cd15..a1867789 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -58,7 +58,11 @@ impl HostDebugger { // Pre-load cartridge metadata so the Handshake message can contain // valid information about the App being debugged. if let Ok(cartridge) = CartridgeLoader::load(path) { - let _ = firmware.os.vm_runtime.initialize_vm(&mut firmware.vm, &cartridge); + let _ = firmware.os.vm_runtime.initialize_vm( + &mut firmware.os.log_service, + &mut firmware.vm, + &cartridge, + ); } match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { @@ -201,7 +205,11 @@ impl HostDebugger { DebugCommand::Step => { // Execute exactly one instruction and keep paused. firmware.os.vm_runtime.paused = true; - let _ = firmware.os.vm_runtime.debug_step_instruction(&mut firmware.vm, hardware); + let _ = firmware.os.vm_runtime.debug_step_instruction( + &mut firmware.os.log_service, + &mut firmware.vm, + hardware, + ); } DebugCommand::StepFrame => { // Execute until the end of the current logical frame. @@ -308,7 +316,7 @@ impl HostDebugger { } // 1. Process and send new log entries. - let new_events = firmware.os.vm_runtime.log_service.get_after(self.last_log_seq); + let new_events = firmware.os.log_service.get_after(self.last_log_seq); for event in new_events { self.last_log_seq = event.seq; diff --git a/crates/host/prometeu-host-desktop-winit/src/overlay.rs b/crates/host/prometeu-host-desktop-winit/src/overlay.rs index b54707bb..ec1ae313 100644 --- a/crates/host/prometeu-host-desktop-winit/src/overlay.rs +++ b/crates/host/prometeu-host-desktop-winit/src/overlay.rs @@ -59,7 +59,7 @@ struct FrameCanvas<'a> { pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot { let tel = firmware.os.vm_runtime.atomic_telemetry.snapshot(); - let recent_logs = firmware.os.vm_runtime.log_service.get_recent(10); + let recent_logs = firmware.os.log_service.get_recent(10); let violations_count = recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count(); diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index af4dd44e..e7f61988 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -153,7 +153,7 @@ impl HostRunner { let mut firmware = Firmware::new(cap_config); if let Some(root) = &fs_root { let backend = HostDirBackend::new(root); - firmware.os.vm_runtime.mount_fs(Box::new(backend)); + firmware.os.mount_fs(Box::new(backend)); } Self { @@ -333,13 +333,11 @@ impl ApplicationHandler for HostRunner { // 2. Maintain filesystem connection if it was lost (e.g., directory removed). if let Some(root) = &self.fs_root { use prometeu_system::fs::FsState; - if matches!( - self.firmware.os.vm_runtime.fs_state, - FsState::Unmounted | FsState::Error(_) - ) && std::path::Path::new(root).exists() + if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) + && std::path::Path::new(root).exists() { let backend = HostDirBackend::new(root); - self.firmware.os.vm_runtime.mount_fs(Box::new(backend)); + self.firmware.os.mount_fs(Box::new(backend)); } } @@ -403,9 +401,9 @@ impl ApplicationHandler for HostRunner { // Synchronize system logs to the host console. let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX); let new_events = if last_seq == u64::MAX { - self.firmware.os.vm_runtime.log_service.get_recent(4096) + self.firmware.os.log_service.get_recent(4096) } else { - self.firmware.os.vm_runtime.log_service.get_after(last_seq) + self.firmware.os.log_service.get_after(last_seq) }; self.log_sink.process_events(new_events); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index bd845d39..112b04fe 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":32,"AGD":32,"DEC":24,"PLN":51,"LSN":41,"CLSN":1}} +{"type":"meta","next_id":{"DSC":34,"AGD":34,"DEC":25,"PLN":55,"LSN":41,"CLSN":1}} {"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} @@ -30,3 +30,5 @@ {"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"discussion/lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-28","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0030-internal-viewport-270p/LSN-0039-resolution-baseline-changes-are-runtime-wide-contracts.md","status":"done","created_at":"2026-04-28","updated_at":"2026-04-28"}]} {"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]} +{"type":"discussion","id":"DSC-0032","status":"open","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-14","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[{"id":"AGD-0032","file":"AGD-0032-system-os-lifecycle-process-task-contract.md","status":"open","created_at":"2026-05-14","updated_at":"2026-05-14"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0033","status":"in_progress","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-14","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[{"id":"AGD-0033","file":"AGD-0033-system-os-service-ownership-and-module-layout.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14"}],"decisions":[{"id":"DEC-0024","file":"DEC-0024-systemos-service-ownership-and-module-layout.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14","ref_agenda":"AGD-0033"}],"plans":[{"id":"PLN-0051","file":"PLN-0051-move-vm-runtime-into-services.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0052","file":"PLN-0052-promote-window-manager-to-systemos-service.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0053","file":"PLN-0053-move-logging-ownership-to-systemos.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0054","file":"PLN-0054-move-fs-and-memcard-ownership-to-systemos.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]}],"lessons":[]} diff --git a/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md b/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md new file mode 100644 index 00000000..425b59cb --- /dev/null +++ b/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md @@ -0,0 +1,302 @@ +--- +id: AGD-0032 +ticket: system-os-lifecycle-process-task-contract +title: Agenda - SystemOS Lifecycle, Process and Task Contract +status: open +created: 2026-05-14 +resolved: +decision: +tags: [runtime, os, lifecycle, process, task, shell, firmware] +--- + +# Agenda - SystemOS Lifecycle, Process and Task Contract + +## Contexto + +Prometeu deu o primeiro passo concreto para deixar de ser apenas um firmware que +roda cartuchos e passar a ter um modelo de sistema operacional de console. + +Antes, o cartucho era representado quase exclusivamente pelo fluxo do firmware: + +```text +LoadCartridgeStep + -> initialize_vm + -> GameRunningStep / SystemRunningStep +``` + +Esse modelo permitia executar jogos e apps de sistema, mas não dava ao OS uma +entidade própria para representar processo, task, foreground, background, +suspensão, fechamento ou crash. O firmware sabia que algo estava rodando; o +Prometeu OS ainda não possuía um contrato próprio de execução e presença de +usuário. + +Agora existe um `SystemOS` em `prometeu-system`, agregando: + +```text +SystemOS + VirtualMachineRuntime + ProcessManager + TaskManager +``` + +Isso estabelece uma fronteira importante: a VM continua sendo a VM, e não a dona +do sistema. O `SystemOS` passa a ser o lugar natural para concentrar serviços de +OS, lifecycle, permissões e coordenação entre execução técnica e experiência de +usuário. + +Os conceitos iniciais são: + +```text +Process + execução técnica: + VmGame, VmShell, NativeShell + +Task + presença navegável do usuário: + Game ou Shell em Foreground, Background, Suspended, Closed ou Crashed +``` + +O carregamento de cartucho começou a registrar entidades de sistema: + +```text +AppMode::Game + -> cria processo VmGame + -> cria task Game + -> marca task como Foreground + -> entra em GameRunningStep com TaskId + +AppMode::System + -> cria processo VmShell + -> cria task Shell + -> marca task como Foreground + -> entra em SystemRunningStep com TaskId +``` + +`GameRunningStep` agora executa uma task específica e valida se ela ainda está em +`Foreground` antes de avançar o frame. Essa é a primeira ponte real entre o +firmware atual e um OS com lifecycle. + +## Problema + +O código já tem os primeiros tipos e fluxos, mas o contrato arquitetural ainda +não está fechado: + +- quem é a autoridade para mover uma task entre `Foreground`, `Background`, + `Suspended`, `Closed` e `Crashed`; +- como `TaskState` e `ProcessState` devem evoluir juntos; +- quais transições pertencem ao `SystemOS` e quais continuam nos managers + internos; +- como o firmware deve reagir quando uma task deixa de estar executável; +- quando o `WindowManager` deixa de ser parte do `PrometeuHub` e vira serviço + real do OS; +- se o nome público do perfil de sistema deve continuar `System` ou convergir + para `Shell` no contrato de cartucho/runtime. + +Sem esse contrato, o risco é o sistema crescer em duas direções ruins: + +1. o firmware continuar acumulando decisões de lifecycle que pertencem ao OS; +2. `TaskManager`, `ProcessManager`, `PrometeuHub` e `VirtualMachineRuntime` + criarem regras paralelas e inconsistentes. + +## Pontos Criticos + +- `SystemOS` deve ser a API de coordenação de lifecycle, não apenas um struct + agregador de managers. +- `ProcessManager` e `TaskManager` devem continuar guardando estado local, mas + não devem, sozinhos, definir o contrato semântico completo entre task e + processo. +- `Task` representa presença navegável e foco do usuário; `Process` representa + execução técnica. +- `Foreground` não é apenas uma flag visual: no modelo atual, ele autoriza o + `GameRunningStep` a executar frame. +- `Suspended`, `Closed` e `Crashed` precisam mover `TaskState` e `ProcessState` + de forma coordenada. +- `Background` precisa ser distinguido de `Suspended`: background pode existir + como presença navegável não focada, mas ainda não implica que o processo possa + executar. +- `PrometeuHub` deve evoluir para Shell/Home visual, não permanecer como + sinônimo de OS. +- `WindowManager` provavelmente deve virar serviço de `SystemOS`, mas talvez + isso precise esperar a primeira API de lifecycle estabilizar. +- A inspiração de UX é console/Switch; a inspiração interna é mais Android-like: + processos, tasks, lifecycle, surfaces/janelas, serviços e permissões. + +## Dependencia / Agenda Paralela + +`AGD-0033` trata a limpeza de ownership e layout de serviços do `SystemOS` antes +ou em paralelo a esta agenda. + +Essa separação é intencional: `AGD-0032` deve decidir lifecycle de +`Task`/`Process`, enquanto `AGD-0033` deve decidir se `VirtualMachineRuntime`, +`LogService`, `WindowManager` e outros serviços pertencem ao root, ao Hub ou à +camada `services` do `SystemOS`. + +Se `AGD-0033` concluir que `LogService` e outros serviços precisam sair da VM +antes do lifecycle, a decisão de `AGD-0032` deve tratar essa mudança como +pré-requisito de implementação, não como parte do contrato de lifecycle em si. + +## Opcoes + +### Opcao A - Manter lifecycle distribuido nos managers e no firmware + +**Abordagem:** +`TaskManager` muda tasks, `ProcessManager` muda processos, e o firmware chama +ambos diretamente quando precisar. + +**Vantagens:** +- menor mudança imediata; +- preserva managers simples; +- fácil de encaixar incrementalmente no código atual. + +**Custos / Riscos:** +- espalha regra de lifecycle por vários chamadores; +- torna mais provável esquecer de mover `ProcessState` junto com `TaskState`; +- mantém firmware como conhecedor de detalhes internos do OS; +- dificulta introduzir Shell/Home, suspensão e retomada sem duplicação. + +**Manutenibilidade:** +Aceitável no curto prazo, fraca como modelo de OS. + +### Opcao B - Centralizar lifecycle no SystemOS + +**Abordagem:** +`SystemOS` vira a API semântica para operações como: + +```text +suspend_task +resume_task +close_task +crash_task +foreground_task +background_task +``` + +Essas operações coordenam `TaskManager` e `ProcessManager` juntas. + +Exemplos de contrato: + +```text +jogo rodando: + TaskState::Foreground + ProcessState::Running + +jogo suspenso: + TaskState::Suspended + ProcessState::Suspended + +jogo fechado: + TaskState::Closed + ProcessState::Stopped + +jogo crashado: + TaskState::Crashed + ProcessState::Crashed +``` + +**Vantagens:** +- estabelece `SystemOS` como autoridade de lifecycle; +- mantém firmware mais fino; +- reduz inconsistência entre task e processo; +- cria base natural para Home/Shell, suspend/resume, close/crash e permissões. + +**Custos / Riscos:** +- exige definir transições inválidas e retorno/erro de cada operação; +- pode expor API cedo demais se tentarmos modelar todas as regras finais agora; +- precisa decidir se `Background` implica processo running, suspended ou apenas + presença não focada. + +**Manutenibilidade:** +Forte. É o caminho mais alinhado com transformar Prometeu em console OS. + +### Opcao C - Promover WindowManager a servico do OS antes do lifecycle + +**Abordagem:** +Mover primeiro o `WindowManager` para dentro de `SystemOS` e depois organizar +tasks/processos em torno de janelas/surfaces. + +**Vantagens:** +- aproxima rapidamente o modelo de Shell/Home e apps gerenciados; +- evita `PrometeuHub` continuar parecendo o sistema inteiro; +- abre caminho para surfaces e composição de apps de sistema. + +**Custos / Riscos:** +- pode misturar lifecycle de execução com lifecycle visual; +- força decisões de janela/surface antes de fechar Process/Task; +- pode congelar um WindowManager ainda experimental. + +**Manutenibilidade:** +Boa como passo futuro, mas arriscada como próximo passo se o lifecycle base +ainda não estiver normativo. + +## Sugestao / Recomendacao + +A recomendação inicial é seguir a **Opção B**. + +O próximo contrato deve declarar `SystemOS` como autoridade para lifecycle de +tasks e processos. `TaskManager` e `ProcessManager` continuam existindo, mas +devem ser mecanismos internos de armazenamento/transição, não a API semântica +principal do sistema. + +O primeiro corte deve ser pequeno: + +- adicionar métodos de lifecycle em `SystemOS`; +- coordenar `TaskState` e `ProcessState` nesses métodos; +- definir comportamento para task inexistente e process inexistente; +- preservar `GameRunningStep` como consumidor de `TaskId`, validando + foreground; +- evitar mover `WindowManager` neste mesmo corte. + +O `WindowManager` deve ser a agenda/decisão seguinte: promovê-lo a serviço real +do sistema quando a base `Task/Process/Lifecycle` estiver fechada. + +Também precisamos resolver a nomenclatura `System` versus `Shell`. O código e o +contrato anterior usam `AppMode::System`; a narrativa nova fala em +`AppMode::Shell`. A agenda deve decidir se: + +- mantemos `AppMode::System` como perfil formal e usamos `Shell` como tipo de + task/processo/UI; ou +- renomeamos o perfil público para `Shell`, com migração explícita. + +Minha inclinação inicial é manter `AppMode::System` por compatibilidade de +contrato e usar `Shell` para a camada de UI/app gerenciado dentro do OS. + +## Perguntas em Aberto + +- [ ] `SystemOS` deve expor `suspend_task`, `resume_task`, `close_task` e + `crash_task` já no próximo corte? +- [ ] `foreground_task` e `background_task` devem ser operações explícitas do + `SystemOS`, ou apenas detalhe do `TaskManager`? +- [ ] Qual é a semântica exata de `Background`: processo ainda `Running`, + processo `Suspended`, ou estado de presença sem garantia de execução? +- [ ] `resume_task` deve sempre mover a task para `Foreground`, ou pode retomar + para `Background`? +- [ ] O que acontece quando a task existe mas o processo associado não existe? +- [ ] `close_task` remove imediatamente entidades fechadas ou apenas marca + `Closed`/`Stopped` para coleta posterior? +- [ ] `crash_task` deve receber/armazenar `CrashReport`, ou isso fica em outro + serviço de diagnóstico? +- [ ] `GameRunningStep` deve transicionar para Hub/Home quando sua task deixa de + estar `Foreground`, ou apenas não executar frame? +- [ ] O perfil de manifesto continua `AppMode::System`, ou abrimos uma migração + formal para `AppMode::Shell`? +- [ ] A decisão de ownership/layout em `AGD-0033` deve ser pré-requisito para o + primeiro plan de lifecycle, ou pode ser executada em paralelo? + +## Criterio para Encerrar + +Esta agenda pode virar decisão quando houver resposta para: + +- qual entidade é autoridade normativa de lifecycle; +- quais métodos mínimos entram no primeiro contrato de `SystemOS`; +- como cada método move `TaskState` e `ProcessState`; +- qual semântica inicial de `Background` e `Suspended`; +- como o firmware deve reagir a task não-foreground/não-executável; +- qual nomenclatura será usada para `System` versus `Shell`; +- como a dependência com `AGD-0033` será tratada nos plans. + +## Proximo Passo + +Revisar esta agenda e fechar as perguntas principais. Se a direção for aceita, +o próximo estágio deve ser uma decisão normativa para `SystemOS` como autoridade +de lifecycle de `Task`/`Process`, seguida de uma família curta de plans para +implementação. diff --git a/discussion/workflow/agendas/AGD-0033-system-os-service-ownership-and-module-layout.md b/discussion/workflow/agendas/AGD-0033-system-os-service-ownership-and-module-layout.md new file mode 100644 index 00000000..5dad3dc5 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0033-system-os-service-ownership-and-module-layout.md @@ -0,0 +1,199 @@ +--- +id: AGD-0033 +ticket: system-os-service-ownership-and-module-layout +title: Agenda - SystemOS Service Ownership and Module Layout +status: accepted +created: 2026-05-14 +resolved: +decision: +tags: [runtime, os, services, module-layout, vm, window-manager, logging] +--- + +# Agenda - SystemOS Service Ownership and Module Layout + +## Contexto + +Antes de avançar o contrato de lifecycle do `SystemOS`, há uma limpeza de +fronteira que precisa ser discutida separadamente. + +O `SystemOS` começou como agregador de: + +```text +SystemOS + VirtualMachineRuntime + ProcessManager + TaskManager +``` + +Mas o código ainda carrega sinais do modelo anterior, em que a VM concentrava +coisas que hoje parecem serviços de sistema. O exemplo mais claro é +`LogService` dentro de `VirtualMachineRuntime`. A mesma dúvida aparece em torno +de outros serviços e estados associados a filesystem, memcard, diagnóstico, +telemetria e identidade do cartucho. + +Há também uma questão de layout: + +- `VirtualMachineRuntime` está no root de `prometeu-system`, embora conceitualmente + agora seja um serviço do OS; +- `WindowManager` está dentro de `programs/prometeu_hub`, embora a direção de + arquitetura seja transformar o Hub em Shell/Home visual, não no sistema; +- `ProcessManager` e `TaskManager` já vivem em `services`, o que torna a posição + da VM e do WindowManager inconsistente com o desenho novo. + +## Problema + +Misturar ownership de serviços com execução da VM dificulta a evolução do OS. +Se `VirtualMachineRuntime` continuar parecendo dona de logging, filesystem, +memcard, diagnóstico e janela, o `SystemOS` vira apenas um wrapper, e não a +autoridade natural para coordenar serviços do console. + +Ao mesmo tempo, nem tudo aqui é puramente mecânico: + +- mover arquivos para `services` pode ser uma alteração sem mudança funcional; +- mover `LogService` para `SystemOS` altera ownership e pode exigir uma API de + serviço ou passagem explícita de dependência para a VM; +- promover `WindowManager` a serviço do OS antecipa uma fronteira arquitetural, + mesmo que a implementação continue mínima. + +Por isso, este assunto não deve ser escondido dentro da agenda de lifecycle +(`AGD-0032`). Ele é uma agenda de pré-requisito ou paralela: primeiro precisamos +limpar a fronteira entre "VM como executor" e "OS como dono de serviços". + +## Pontos Criticos + +- `VirtualMachineRuntime` deve representar execução VM, não ser o container + geral de serviços do sistema. +- `SystemOS` deve ser o lugar natural para serviços compartilhados por jogos, + Shell/Home, firmware e processos nativos futuros. +- `LogService` provavelmente pertence ao `SystemOS`, mas a forma de acesso pela + VM precisa respeitar as restrições de borrow e o desenho de `NativeInterface`. +- `VirtualFS` e `MemcardService` também parecem serviços de OS, mas movê-los no + mesmo corte pode aumentar demais o risco. +- `WindowManager` deve sair de `PrometeuHub` porque o Hub deve virar consumidor + visual do OS, não proprietário de serviço de janela. +- A mudança de layout de módulo deve preservar comportamento, mas não precisa + preservar caminhos antigos de import; callsites devem ser atualizados + diretamente. +- A decisão precisa separar explicitamente "relocação de módulo" de "mudança de + ownership". + +## Opcoes + +### Opcao A - Agregar tudo na agenda de lifecycle + +**Abordagem:** +Usar `AGD-0032` para decidir lifecycle, ownership de serviços, layout de módulos +e promoção do `WindowManager`. + +**Vantagens:** +- uma única discussão cobre toda a direção do `SystemOS`; +- menos artefatos de workflow. + +**Custos / Riscos:** +- mistura semântica de lifecycle com limpeza estrutural; +- aumenta a chance de uma decisão grande demais; +- dificulta transformar a discussão em plans pequenos e verificáveis; +- pode bloquear lifecycle por causa de detalhes de módulo e borrow. + +### Opcao B - Separar uma agenda de service ownership e layout + +**Abordagem:** +Manter `AGD-0032` focada em lifecycle de `Task`/`Process` e usar esta agenda +para decidir onde vivem os serviços do OS, quais módulos devem ir para +`services`, e quais extrações são mecânicas ou semânticas. + +**Vantagens:** +- mantém a agenda principal limpa; +- permite um primeiro plan mecânico de relocação sem reabrir lifecycle; +- torna explícito que `LogService` sair da VM é mudança de ownership; +- cria base melhor para promover `WindowManager` sem confundir Hub e OS. + +**Custos / Riscos:** +- cria uma dependência a mais antes da implementação do lifecycle; +- pode revelar que alguns serviços exigem desenho de API antes do refactor; +- exige cuidado para atualizar callsites diretamente sem deixar caminhos antigos + exportados. + +### Opcao C - Fazer apenas os moves mecanicos direto no codigo + +**Abordagem:** +Mover `VirtualMachineRuntime` e `WindowManager` para `services` sem decisão +formal, deixando `LogService` para depois. + +**Vantagens:** +- rápido; +- baixo risco se for realmente só path/reexport; +- reduz ruído visual do layout atual. + +**Custos / Riscos:** +- não resolve a dúvida central de ownership; +- pode mascarar uma decisão arquitetural como refactor mecânico; +- viola a política do repositório se a mudança alterar fronteiras sem agenda e + decisão. + +## Sugestao / Recomendacao + +A recomendação é seguir a **Opção B**. + +Esta agenda deve ser separada da `AGD-0032`, mas tratada como pré-requisito ou +agenda paralela para ela. O primeiro contrato deve dividir o trabalho em dois +tipos: + +1. **Relocação mecânica sem mudança funcional** + - mover `VirtualMachineRuntime` para `services`; + - mover `WindowManager` para `services`; + - não manter reexports ou aliases de compatibilidade para os caminhos antigos; + - atualizar callsites diretamente para os caminhos novos; + - provar que o comportamento não mudou. + +2. **Correção de ownership de serviços** + - mover `LogService` para `SystemOS` se aceitarmos que logging é serviço do + OS; + - decidir se `VirtualFS`, `MemcardService`, diagnóstico, telemetria e + certificação ficam na VM, no OS, ou em serviços próprios; + - definir como a VM acessa serviços de OS sem voltar a ser dona deles. + +Para o primeiro corte, a inclinação é: + +- `VirtualMachineRuntime` deve ficar em `services`, como serviço de execução VM; +- `WindowManager` deve ficar em `services`, mesmo que ainda seja mínimo; +- `LogService` deve ser tratado como serviço de `SystemOS`, não detalhe interno + da VM; +- `VirtualFS` e `MemcardService` devem ser avaliados em seguida, sem mover tudo + junto automaticamente. + +## Perguntas em Aberto + +- [x] O path deve ser `services/vm_runtime` ou + `services/virtual_machine_runtime`? vm_runtime +- [x] `VirtualMachineRuntime` continua sendo o nome correto depois de virar + serviço, ou devemos introduzir um nome menor apenas no módulo? Podemos manter o mesmo nome +- [x] `LogService` deve sair da VM no mesmo plan do move mecânico, ou em um + plan separado de ownership? pode ser em um plan separado +- [x] A VM deve receber referência explícita ao logging do `SystemOS`, ou deve + chamar uma API de serviço mais estreita? A VM pode ter uma referência explícita ao logging do `SystemOS` para seu uso +- [x] `VirtualFS` e `MemcardService` pertencem ao `SystemOS` já, ou continuam na + VM até existir uma camada de permissões e mount por processo? podemos move-los para systemos jah +- [x] `WindowManager` deve virar campo do `SystemOS` imediatamente, ou primeiro + apenas sair de `PrometeuHub` em termos de módulo? eu diria que ele jah deve nascer em system os a partir de jah, mesmo que sem implementar nada +- [x] O `PrometeuHub` deve continuar expondo `window_manager` enquanto a Shell + visual ainda depende dele, ou isso deve ser mediado pelo `SystemOS` desde já? o prometeu hub eh um shell especial, ou seja, o window manager deve mediado pelo system os desde jah + +## Criterio para Encerrar + +Esta agenda pode virar decisão quando houver resposta para: + +- quais serviços pertencem ao `SystemOS` no primeiro corte; +- qual layout canônico de módulo será usado em `services`; +- quais mudanças são relocação mecânica e quais são ownership semântico; +- como `VirtualMachineRuntime` acessa `LogService` depois da extração; +- se `WindowManager` será apenas movido de módulo ou também promovido a campo do + `SystemOS`; +- como os callsites serão atualizados sem reexports temporários. + +## Proximo Passo + +Revisar esta agenda antes de fechar `AGD-0032`. Se a direção for aceita, o +próximo estágio deve ser uma decisão curta para fronteira de serviços do +`SystemOS`, seguida de plans pequenos: primeiro relocação mecânica, depois +extração de ownership onde houver contrato suficiente. diff --git a/discussion/workflow/decisions/DEC-0024-systemos-service-ownership-and-module-layout.md b/discussion/workflow/decisions/DEC-0024-systemos-service-ownership-and-module-layout.md new file mode 100644 index 00000000..50679e50 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0024-systemos-service-ownership-and-module-layout.md @@ -0,0 +1,133 @@ +--- +id: DEC-0024 +ticket: system-os-service-ownership-and-module-layout +title: SystemOS Service Ownership and Module Layout +status: accepted +created: 2026-05-14 +ref_agenda: AGD-0033 +tags: [runtime, os, services, module-layout, vm, window-manager, logging] +--- + +## Status + +Accepted. Esta decisão foi aceita em 2026-05-14 e passa a ser referência +normativa para os plans de ownership e layout de serviços do `SystemOS`. + +## Contexto + +`SystemOS` está se tornando o dono dos serviços de console em +`prometeu-system`. O formato atual ainda deixa responsabilidades de OS dentro ou +perto de `VirtualMachineRuntime` e `PrometeuHub`: + +- `VirtualMachineRuntime` vive no root do crate, embora agora seja um serviço de + execução VM do OS; +- `LogService` é owned por `VirtualMachineRuntime`, embora logging seja serviço + de observabilidade do OS; +- `VirtualFS` e `MemcardService` também ficam dentro de + `VirtualMachineRuntime`, embora representem serviços do sistema; +- `WindowManager` vive em `programs/prometeu_hub`, embora `PrometeuHub` deva ser + Shell/Home visual, não dono de serviços do OS. + +Esta decisão é separada da `AGD-0032`, que trata lifecycle de `Task`/`Process`. +Aqui o contrato é ownership de serviços e layout de módulos. + +## Decisao + +`SystemOS` SHALL ser o owner dos serviços do Prometeu OS. + +`VirtualMachineRuntime` SHALL representar somente execução VM. Ele MUST NOT ser +usado como container geral de serviços do sistema. + +O layout canônico da VM runtime SHALL ser: + +```text +crates/console/prometeu-system/src/services/vm_runtime/ +``` + +O nome público do tipo SHALL continuar `VirtualMachineRuntime`. + +`WindowManager` SHALL sair de `programs/prometeu_hub` e ir para a árvore +`services`. Ele SHALL ser owned ou mediado por `SystemOS` desde este estágio, +mesmo que a implementação continue mínima. + +`PrometeuHub` SHALL ser tratado como uma Shell/Home especial. Ele MUST NOT ser +owner do window manager como serviço de OS. O acesso do Hub a window management +MUST passar por `SystemOS` ou por uma superfície mediada pelo OS. + +`LogService` SHALL sair de `VirtualMachineRuntime` e virar serviço owned por +`SystemOS`. `VirtualMachineRuntime` MAY receber referência ou handle explícito +para o logging do OS quando precisar registrar eventos de execução VM, mas esse +acesso MUST NOT transformar a VM em owner de logging. + +`VirtualFS` e `MemcardService` SHALL mover para `SystemOS` neste cleanup de +ownership. A VM pode continuar usando capacidades de filesystem e memcard, mas +o ownership pertence ao boundary do OS. + +Esta decisão MUST NOT introduzir reexports ou aliases de compatibilidade para +os paths antigos. Os plans de implementação MUST atualizar callsites +diretamente. Novos imports MUST usar os paths canônicos de `services` e o +boundary de ownership do `SystemOS`. + +## Rationale + +O runtime está saindo de um runner de firmware para um console OS. Nesse modelo, +execução VM é um serviço do sistema, não o centro proprietário do sistema. Se +logging, filesystem, memcard, window management e futuramente lifecycle +continuarem dentro ou ao redor de `VirtualMachineRuntime`, a VM permanece como +owner de fato do OS. + +Mover `VirtualMachineRuntime` para `services/vm_runtime` alinha layout com +arquitetura: execução VM é serviço, não root. + +Mover `WindowManager` para `services` evita que `PrometeuHub` continue parecendo +o sistema inteiro. O Hub é Shell/Home e deve consumir serviços do OS. + +Mover `LogService`, `VirtualFS` e `MemcardService` para `SystemOS` cria uma base +mais limpa para permissões, processo/task ownership, mount policy, diagnóstico e +mediação da Shell. + +Não manter compatibilidade por reexport é aceitável agora porque esses paths são +internos e não há necessidade de janela longa de migração. Atualizar callsites +diretamente mantém o boundary claro. + +## Invariantes / Contrato + +- `SystemOS` MUST ser o ponto de ownership para serviços compartilhados do OS. +- `VirtualMachineRuntime` MUST NOT own `LogService`, `VirtualFS`, + `MemcardService` ou `WindowManager` depois dos plans correspondentes. +- `VirtualMachineRuntime` MAY usar serviços do OS por referência, handle ou API + estreita, mas MUST permanecer semanticamente executor VM. +- `PrometeuHub` MUST ser consumidor de OS services, não owner desses serviços. +- Paths antigos MUST NOT permanecer disponíveis por reexport ou alias. +- Mudanças de path MUST preservar comportamento observável. +- Esta decisão MUST NOT definir lifecycle de `Task`/`Process`; isso pertence à + `AGD-0032` e à decisão futura correspondente. + +## Impactos + +- `prometeu-system` deve ganhar layout de serviço mais coerente em `services`. +- `SystemOS` deve concentrar ownership de `LogService`, `VirtualFS`, + `MemcardService` e `WindowManager`. +- `VirtualMachineRuntime` deve reduzir escopo para execução VM. +- `PrometeuHub` deve começar a depender de mediação do OS para window + management. +- Firmware e testes podem precisar atualizar imports e construtores. +- Não há mudança exigida de ABI pública de cartucho. + +## Referencias + +- Agenda: `AGD-0033` +- Agenda relacionada: `AGD-0032` + +## Propagacao Necessaria + +- Plans: criar plans pequenos para relocação da VM runtime, promoção do + WindowManager e extração de ownership de serviços. +- Code: atualizar `prometeu-system`, `SystemOS`, `VirtualMachineRuntime`, + `PrometeuHub`, callsites de firmware e testes afetados. +- Tests: rodar a suíte Rust afetada e adicionar cobertura se ownership mudar + construtores ou caminhos de acesso. +- Specs: sem update obrigatório neste momento, salvo se plans futuros expuserem + o boundary em contrato público. +- Discussion: manter `AGD-0032` dependente desta decisão antes de planejar + lifecycle. diff --git a/discussion/workflow/plans/PLN-0051-move-vm-runtime-into-services.md b/discussion/workflow/plans/PLN-0051-move-vm-runtime-into-services.md new file mode 100644 index 00000000..a2f5a77e --- /dev/null +++ b/discussion/workflow/plans/PLN-0051-move-vm-runtime-into-services.md @@ -0,0 +1,103 @@ +--- +id: PLN-0051 +ticket: system-os-service-ownership-and-module-layout +title: Move VM Runtime Into Services +status: done +created: 2026-05-14 +ref_decisions: [DEC-0024] +tags: [runtime, os, services, module-layout, vm, window-manager, logging] +--- + +## Briefing + +Implement the mechanical module relocation required by `DEC-0024`: move +`VirtualMachineRuntime` from the `prometeu-system` crate root into the services +tree at `services/vm_runtime`. + +This plan is intentionally behavior-preserving. It prepares the service layout +for later ownership plans without moving logging, filesystem, or memcard state. + +## Source Decisions + +- `DEC-0024`: `VirtualMachineRuntime` SHALL move to + `services/vm_runtime`; old root paths MUST NOT remain available through + compatibility reexports. + +## Target + +Make `VirtualMachineRuntime` a service module while keeping the public type name +unchanged and preserving current VM behavior. + +The canonical implementation path after this plan is: + +```text +crates/console/prometeu-system/src/services/vm_runtime/ +``` + +## Dependencies + +- Source decision: `DEC-0024`. +- No prior plan is required. +- This plan should run before `PLN-0052`, `PLN-0053`, and `PLN-0054`. + +## Scope + +- Move `crates/console/prometeu-system/src/virtual_machine_runtime.rs` to + `crates/console/prometeu-system/src/services/vm_runtime.rs` or equivalent + module root under `services/vm_runtime`. +- Move the existing `virtual_machine_runtime/` child modules under + `services/vm_runtime/`. +- Update `crates/console/prometeu-system/src/services/mod.rs` to expose + `vm_runtime`. +- Update `crates/console/prometeu-system/src/lib.rs` to export + `VirtualMachineRuntime` from `services::vm_runtime`. +- Update internal module paths and tests directly. + +## Out of Scope + +- Do not move `LogService` ownership. +- Do not move `VirtualFS` or `MemcardService` ownership. +- Do not move `WindowManager`. +- Do not introduce reexports or aliases for the old root module path. +- Do not change VM runtime behavior, syscalls, telemetry, logging semantics, or + cartridge execution behavior. + +## Execution Plan + +1. Move the VM runtime module files into `src/services/vm_runtime/`. +2. Register `pub mod vm_runtime;` from `src/services/mod.rs`. +3. Remove the root `mod virtual_machine_runtime;` declaration from `src/lib.rs`. +4. Export `VirtualMachineRuntime` through the services path. +5. Update `SystemOS`, tests, and any internal imports to use the new module + location. +6. Run formatting and the affected Rust tests. + +## Acceptance Criteria + +- `VirtualMachineRuntime` implementation lives under `services/vm_runtime`. +- No root `virtual_machine_runtime` module remains in `prometeu-system`. +- No compatibility reexport preserves the old root module path. +- Public type name `VirtualMachineRuntime` remains available where the crate API + intentionally exposes it. +- Runtime behavior remains unchanged. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- If workspace constraints make that too broad, run the narrowest available + `prometeu-system` test target and record the exact command in implementation + evidence. + +## Risks + +- Module move churn can hide accidental import compatibility. Review must check + that no old root module remains. +- Test modules may need path updates because they currently live under the VM + runtime module tree. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/lib.rs` +- `crates/console/prometeu-system/src/services/mod.rs` +- `crates/console/prometeu-system/src/services/vm_runtime/**` +- Existing VM runtime tests moved with the module diff --git a/discussion/workflow/plans/PLN-0052-promote-window-manager-to-systemos-service.md b/discussion/workflow/plans/PLN-0052-promote-window-manager-to-systemos-service.md new file mode 100644 index 00000000..1202fb39 --- /dev/null +++ b/discussion/workflow/plans/PLN-0052-promote-window-manager-to-systemos-service.md @@ -0,0 +1,85 @@ +--- +id: PLN-0052 +ticket: system-os-service-ownership-and-module-layout +title: Promote Window Manager To SystemOS Service +status: done +created: 2026-05-14 +ref_decisions: [DEC-0024] +tags: [runtime, os, services, module-layout, vm, window-manager, logging] +--- + +## Briefing + +Promote `WindowManager` from a `PrometeuHub` private module into an OS service +owned or mediated by `SystemOS`, as required by `DEC-0024`. + +## Source Decisions + +- `DEC-0024`: `WindowManager` SHALL move to the services tree and SHALL be + owned or mediated by `SystemOS`; `PrometeuHub` MUST NOT own it as an OS + service. + +## Target + +Make `WindowManager` part of the `services` tree and stop treating +`PrometeuHub` as the owner of window management. + +## Dependencies + +- Source decision: `DEC-0024`. +- Depends on `PLN-0051` only for the general service layout convention. + +## Scope + +- Move `crates/console/prometeu-system/src/programs/prometeu_hub/window_manager.rs` + into the `services` tree. +- Expose the service from `crates/console/prometeu-system/src/services/mod.rs`. +- Add a `WindowManager` field or OS-mediated access path on `SystemOS`. +- Remove `window_manager` ownership from `PrometeuHub`. +- Update `PrometeuHub` to access window management through `SystemOS`. +- Update tests and imports directly. + +## Out of Scope + +- Do not implement new windowing behavior. +- Do not define surface/window lifecycle beyond the existing minimal behavior. +- Do not introduce old-path compatibility reexports. +- Do not change task/process lifecycle semantics. + +## Execution Plan + +1. Move the `WindowManager` module into `src/services/`. +2. Register the new service module in `services/mod.rs`. +3. Add `WindowManager` initialization to `SystemOS::new`. +4. Remove `WindowManager` storage from `PrometeuHub`. +5. Update Hub methods so their window manager access is mediated by `SystemOS`. +6. Update tests for the new module path and ownership location. +7. Run formatting and the affected tests. + +## Acceptance Criteria + +- `WindowManager` no longer lives under `programs/prometeu_hub`. +- `PrometeuHub` does not own a `WindowManager` field. +- `SystemOS` owns or mediates access to `WindowManager`. +- No old `programs::prometeu_hub::window_manager` path remains available. +- Existing Hub behavior remains unchanged. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- Include any focused tests for `WindowManager` after the move. + +## Risks + +- `PrometeuHub` currently owns its `WindowManager`; moving ownership may require + method signatures to pass through `SystemOS` cleanly. +- The implementation must not accidentally introduce new window/surface + semantics while moving ownership. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/programs/prometeu_hub/mod.rs` +- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` +- `crates/console/prometeu-system/src/services/mod.rs` +- `crates/console/prometeu-system/src/services/window_manager.rs` or equivalent +- `crates/console/prometeu-system/src/os/system_os.rs` diff --git a/discussion/workflow/plans/PLN-0053-move-logging-ownership-to-systemos.md b/discussion/workflow/plans/PLN-0053-move-logging-ownership-to-systemos.md new file mode 100644 index 00000000..b5bea105 --- /dev/null +++ b/discussion/workflow/plans/PLN-0053-move-logging-ownership-to-systemos.md @@ -0,0 +1,88 @@ +--- +id: PLN-0053 +ticket: system-os-service-ownership-and-module-layout +title: Move Logging Ownership To SystemOS +status: done +created: 2026-05-14 +ref_decisions: [DEC-0024] +tags: [runtime, os, services, module-layout, vm, window-manager, logging] +--- + +## Briefing + +Move `LogService` ownership from `VirtualMachineRuntime` to `SystemOS`, while +allowing VM execution code to use OS logging through an explicit reference or +handle. + +## Source Decisions + +- `DEC-0024`: `LogService` SHALL move from `VirtualMachineRuntime` to + `SystemOS`; VM execution MAY use an explicit OS logging reference or handle + but MUST NOT own logging. + +## Target + +Make logging an OS-owned service and reduce `VirtualMachineRuntime` to a user of +logging rather than its owner. + +## Dependencies + +- Source decision: `DEC-0024`. +- Depends on `PLN-0051`. +- May run after or alongside `PLN-0052`, but code integration is simpler after + `SystemOS` service ownership is established. + +## Scope + +- Add `LogService` ownership to `SystemOS`. +- Remove `log_service: LogService` from `VirtualMachineRuntime`. +- Update VM runtime methods that read/write logs to receive logging access from + `SystemOS` or a narrow explicit handle. +- Update construction paths so `LogService::new(4096)` is owned by `SystemOS`. +- Update tests that currently inspect runtime-owned logs. + +## Out of Scope + +- Do not change log capacity or log record semantics. +- Do not change VM syscall behavior except for the ownership path. +- Do not introduce compatibility accessors that imply VM ownership. +- Do not move filesystem or memcard ownership in this plan. + +## Execution Plan + +1. Add a `LogService` field to `SystemOS` and initialize it in `SystemOS::new`. +2. Remove `LogService` initialization and storage from + `VirtualMachineRuntime::new`. +3. Update VM logging call paths in `dispatch`, `tick`, lifecycle, and tests to + use OS-owned logging. +4. If borrow constraints require it, introduce a narrow logging handle/reference + parameter at the call boundary instead of giving the VM ownership. +5. Update all tests that assert logging behavior. +6. Run formatting and tests. + +## Acceptance Criteria + +- `VirtualMachineRuntime` no longer stores `LogService`. +- `SystemOS` owns `LogService`. +- VM execution can still emit and expose logs through the existing behavior. +- No compatibility API suggests that logging remains VM-owned. +- Existing logging tests pass or are updated to assert OS ownership. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- Include focused tests covering VM log writes and reads after ownership moves + to `SystemOS`. + +## Risks + +- Borrowing constraints around `NativeInterface` may require a narrow access + pattern rather than simply borrowing all of `SystemOS`. +- Tests that currently construct only `VirtualMachineRuntime` may need to move + to `SystemOS` construction to reflect the new owner. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/system_os.rs` +- `crates/console/prometeu-system/src/services/vm_runtime/**` +- Tests under the VM runtime module diff --git a/discussion/workflow/plans/PLN-0054-move-fs-and-memcard-ownership-to-systemos.md b/discussion/workflow/plans/PLN-0054-move-fs-and-memcard-ownership-to-systemos.md new file mode 100644 index 00000000..b76d8581 --- /dev/null +++ b/discussion/workflow/plans/PLN-0054-move-fs-and-memcard-ownership-to-systemos.md @@ -0,0 +1,95 @@ +--- +id: PLN-0054 +ticket: system-os-service-ownership-and-module-layout +title: Move FS And Memcard Ownership To SystemOS +status: done +created: 2026-05-14 +ref_decisions: [DEC-0024] +tags: [runtime, os, services, module-layout, vm, window-manager, logging] +--- + +## Briefing + +Move `VirtualFS` and `MemcardService` ownership from `VirtualMachineRuntime` to +`SystemOS`, preserving current VM filesystem and memcard behavior. + +## Source Decisions + +- `DEC-0024`: `VirtualFS` and `MemcardService` SHALL move from + `VirtualMachineRuntime` to `SystemOS`; VM execution may use them, but OS owns + the service state. + +## Target + +Make filesystem and memcard OS-owned services while keeping VM syscalls and +tests behaviorally equivalent. + +## Dependencies + +- Source decision: `DEC-0024`. +- Depends on `PLN-0051`. +- Should run after `PLN-0053` so the pattern for VM access to OS-owned services + is already established. + +## Scope + +- Add `VirtualFS`, `FsState`, `MemcardService`, open file handle state, and + related handle allocation state to `SystemOS` if they are currently VM-owned + service state. +- Remove direct ownership of `VirtualFS` and `MemcardService` from + `VirtualMachineRuntime`. +- Update FS and memcard syscall paths so VM execution uses OS-owned services. +- Update tests that currently create a bare `VirtualMachineRuntime` when they + need filesystem or memcard services. +- Keep service modules under `services/fs` and `services/memcard`. + +## Out of Scope + +- Do not redesign filesystem semantics, path normalization, memcard slot + semantics, or persistence policy. +- Do not introduce process permissions or mount namespaces. +- Do not change cartridge ABI. +- Do not move unrelated telemetry, certification, asset, or debug state unless + required by compilation and explicitly documented in implementation evidence. + +## Execution Plan + +1. Identify the complete set of FS/memcard-owned fields currently stored in + `VirtualMachineRuntime`. +2. Add the OS-owned fields to `SystemOS` and initialize them in `SystemOS::new`. +3. Remove those service fields from `VirtualMachineRuntime`. +4. Update VM dispatch/syscall code to operate on OS-owned FS and memcard state + through explicit references or narrow access methods. +5. Update tests to construct `SystemOS` when testing FS/memcard behavior through + VM execution. +6. Run formatting and the affected Rust tests. + +## Acceptance Criteria + +- `VirtualMachineRuntime` no longer owns `VirtualFS` or `MemcardService`. +- `SystemOS` owns filesystem and memcard service state needed by VM execution. +- Existing FS and memcard behavior is preserved. +- No compatibility path implies VM ownership of FS or memcard services. +- Any remaining VM-owned fields are execution-specific and justified by the + implementation evidence. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- Include focused FS and memcard tests after updating constructors/access paths. + +## Risks + +- FS and memcard syscalls are stateful and may be coupled to open handle fields + in `VirtualMachineRuntime`; the implementation must move the whole service + state coherently. +- Bare VM tests may need broader OS fixtures, which can reveal unrelated setup + assumptions. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/system_os.rs` +- `crates/console/prometeu-system/src/services/fs/**` +- `crates/console/prometeu-system/src/services/memcard.rs` +- `crates/console/prometeu-system/src/services/vm_runtime/**` +- FS/memcard runtime tests diff --git a/files/config/runtime-edge-coverage-domains.json b/files/config/runtime-edge-coverage-domains.json index 26d502fe..c9ed097d 100644 --- a/files/config/runtime-edge-coverage-domains.json +++ b/files/config/runtime-edge-coverage-domains.json @@ -1,20 +1,33 @@ { "version": 1, "domains": { - "system/runtime": { - "baseline_percent": 68, + "system/os": { + "baseline_percent": 78, "paths": [ - "crates/console/prometeu-system/src/virtual_machine_runtime.rs", - "crates/console/prometeu-system/src/virtual_machine_runtime/" + "crates/console/prometeu-system/src/os/", + "crates/console/prometeu-system/src/services/process", + "crates/console/prometeu-system/src/services/task" ] }, - "fs": { + "system/runtime": { + "baseline_percent": 70, + "paths": [ + "crates/console/prometeu-system/src/services/vm_runtime" + ] + }, + "system/fs": { "baseline_percent": 77, "paths": [ "crates/console/prometeu-system/src/services/fs/", "crates/console/prometeu-system/src/services/memcard.rs" ] }, + "system/programs": { + "baseline_percent": 55, + "paths": [ + "crates/console/prometeu-system/src/programs/" + ] + }, "asset/bank": { "baseline_percent": 86, "paths": [