diff --git a/.gitignore b/.gitignore index b8a7613d..26622bca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # ===== Rust / Cargo ===== +.cargo/config.toml /target/ **/target/ @@ -10,6 +11,7 @@ # RustRover / IntelliJ .idea/ *.iml +.junie # VS Code .vscode/ diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 3d6bf562..cd9233dc 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -308,12 +308,11 @@ mod tests { other => panic!("expected AppCrashes state, got {:?}", other), } - let task = firmware.os.task_manager.get(task_id).expect("task should exist"); - let process = - firmware.os.process_manager.get(task.process_id).expect("process should exist"); - - assert_eq!(task.state, TaskState::Crashed); - assert_eq!(process.state, ProcessState::Crashed); + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Crashed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(task_id), + Ok(ProcessState::Crashed) + ); } #[test] @@ -351,8 +350,8 @@ mod tests { firmware.tick(&signals, &mut hardware); assert!(matches!(firmware.state, FirmwareState::SystemRunning(_))); - assert!(firmware.os.window_manager.focused.is_some()); - assert_eq!(firmware.os.window_manager.windows.len(), 1); + assert!(firmware.os.window().focused_window().is_some()); + assert_eq!(firmware.os.window().window_count(), 1); } #[test] @@ -364,11 +363,11 @@ mod tests { firmware.load_cartridge(valid_cartridge(AppMode::Shell)); firmware.tick(&signals, &mut hardware); - let tick_index_before_system_update = firmware.os.vm_runtime.tick_index; + let tick_index_before_system_update = firmware.os.vm().tick_index(); firmware.tick(&signals, &mut hardware); assert!(matches!(firmware.state, FirmwareState::SystemRunning(_))); - assert_eq!(firmware.os.vm_runtime.tick_index, tick_index_before_system_update + 1); + assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1); } #[test] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 43d7ae63..5bd9579f 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 @@ -19,7 +19,7 @@ impl GameRunningStep { } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { - let Some(task_state) = ctx.os.task_manager.get(self.task_id).map(|task| task.state) else { + let Some(task_state) = ctx.os.lifecycle().task_state(self.task_id) else { // TODO: is it panic? let report = CrashReport::VmPanic { message: "task is gone, panic!".to_string(), pc: None }; @@ -32,18 +32,18 @@ impl GameRunningStep { message: "should be running as foreground, panic!".to_string(), pc: None, }; - let _ = ctx.os.crash_task(self.task_id, Some(&report)); + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - let result = ctx.os.tick_vm(ctx.vm, ctx.signals, ctx.hw); + let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.hw); - if !ctx.os.vm_runtime.logical_frame_active { + if !ctx.os.vm().logical_frame_active() { ctx.hw.gfx_mut().present(); } if let Some(report) = result { - let _ = ctx.os.crash_task(self.task_id, Some(&report)); + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs index fdc4ae7a..28b21a63 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 @@ -34,7 +34,7 @@ impl LoadCartridgeStep { self.cartridge.assets.clone(), ); - self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err(); + self.init_error = ctx.os.vm().initialize(ctx.vm, &self.cartridge).err(); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { @@ -43,20 +43,24 @@ impl LoadCartridgeStep { } if self.cartridge.app_mode == AppMode::Shell { - let id = ctx.os.window_manager.add_window( + let id = ctx.os.window().add_window( self.cartridge.title.clone(), Rect { x: 40, y: 20, w: 240, h: 140 }, Color::WHITE, ); - ctx.os.window_manager.set_focus(id); + ctx.os.window().set_focus(id); - let task_id = - ctx.os.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone()); + let task_id = ctx + .os + .sessions() + .create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone()); return Some(FirmwareState::SystemRunning(SystemRunningStep::new(task_id))); } - let task_id = - ctx.os.create_vm_game_task(self.cartridge.app_id, self.cartridge.title.clone()); + let task_id = ctx + .os + .sessions() + .create_vm_game_task(self.cartridge.app_id, self.cartridge.title.clone()); Some(FirmwareState::GameRunning(GameRunningStep::new(task_id))) } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs index 20cde51d..586e3026 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs @@ -9,7 +9,7 @@ pub struct ResetStep; impl ResetStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string()); - ctx.os.vm_runtime.reset(ctx.vm); + ctx.os.vm().reset(ctx.vm); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { 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 bcbccdf8..25bfc402 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 @@ -20,7 +20,7 @@ impl SystemRunningStep { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { let outcome = ctx.hub.update_system_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw); if let Some(report) = outcome.crash { - let _ = ctx.os.crash_task(self.task_id, Some(&report)); + let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report)); return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } diff --git a/crates/console/prometeu-system/src/os/facades/fs.rs b/crates/console/prometeu-system/src/os/facades/fs.rs new file mode 100644 index 00000000..8d40665b --- /dev/null +++ b/crates/console/prometeu-system/src/os/facades/fs.rs @@ -0,0 +1,26 @@ +use crate::fs::{FsBackend, FsState}; +use crate::os::SystemOS; + +pub struct FsFacade<'a> { + pub(in crate::os) os: &'a mut SystemOS, +} + +impl<'a> FsFacade<'a> { + pub fn needs_mount(&self) -> bool { + matches!(self.os.fs_state, FsState::Unmounted | FsState::Error(_)) + } + + pub fn mount(&mut self, backend: Box) { + self.os.vm_runtime.mount_fs( + &mut self.os.log_service, + &mut self.os.fs, + &mut self.os.fs_state, + backend, + ); + } + + pub fn unmount(&mut self) { + self.os.fs.unmount(); + self.os.fs_state = FsState::Unmounted; + } +} diff --git a/crates/console/prometeu-system/src/os/facades/lifecycle.rs b/crates/console/prometeu-system/src/os/facades/lifecycle.rs new file mode 100644 index 00000000..7fe687b8 --- /dev/null +++ b/crates/console/prometeu-system/src/os/facades/lifecycle.rs @@ -0,0 +1,94 @@ +use crate::CrashReport; +use crate::os::SystemOS; +use crate::os::{LifecycleError, LifecycleOperation}; +use crate::process::{ProcessId, ProcessState}; +use crate::task::{TaskId, TaskState}; + +pub struct LifecycleFacade<'a> { + pub(in crate::os) os: &'a mut SystemOS, +} + +impl<'a> LifecycleFacade<'a> { + pub fn task_state(&self, task_id: TaskId) -> Option { + self.os.task_manager.get(task_id).map(|task| task.state) + } + + pub fn process_state_for_task(&self, task_id: TaskId) -> Result { + let process_id = process_id_for_task(self.os, task_id)?; + self.os + .process_manager + .get(process_id) + .map(|process| process.state) + .ok_or(LifecycleError::ProcessNotFound(process_id)) + } + + pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.task_manager.set_foreground(task_id); + self.os.process_manager.mark_running(process_id); + + Ok(()) + } + + pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.task_manager.mark_suspended(task_id); + self.os.process_manager.mark_suspended(process_id); + + Ok(()) + } + + pub fn resume_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let task = + self.os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; + + if task.state != TaskState::Suspended { + return Err(LifecycleError::InvalidTransition { + task_id, + from: task.state, + operation: LifecycleOperation::Resume, + }); + } + + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.task_manager.set_foreground(task_id); + self.os.process_manager.mark_running(process_id); + + Ok(()) + } + + pub fn close_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.task_manager.close_task(task_id); + self.os.process_manager.mark_stopped(process_id); + + Ok(()) + } + + pub fn crash_task( + &mut self, + task_id: TaskId, + _report: Option<&CrashReport>, + ) -> Result<(), LifecycleError> { + let process_id = process_id_for_task(self.os, task_id)?; + + self.os.task_manager.mark_crashed(task_id); + self.os.process_manager.mark_crashed(process_id); + + Ok(()) + } +} + +fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result { + let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; + + if !os.process_manager.contains(task.process_id) { + return Err(LifecycleError::ProcessNotFound(task.process_id)); + } + + Ok(task.process_id) +} diff --git a/crates/console/prometeu-system/src/os/facades/mod.rs b/crates/console/prometeu-system/src/os/facades/mod.rs new file mode 100644 index 00000000..be54cf66 --- /dev/null +++ b/crates/console/prometeu-system/src/os/facades/mod.rs @@ -0,0 +1,11 @@ +mod fs; +mod lifecycle; +mod sessions; +mod vm; +mod window; + +pub use fs::FsFacade; +pub use lifecycle::LifecycleFacade; +pub use sessions::SessionsFacade; +pub use vm::VmFacade; +pub use window::WindowFacade; diff --git a/crates/console/prometeu-system/src/os/facades/sessions.rs b/crates/console/prometeu-system/src/os/facades/sessions.rs new file mode 100644 index 00000000..42f8f3fe --- /dev/null +++ b/crates/console/prometeu-system/src/os/facades/sessions.rs @@ -0,0 +1,44 @@ +use crate::os::SystemOS; +use crate::task::TaskId; + +pub struct SessionsFacade<'a> { + pub(in crate::os) os: &'a mut SystemOS, +} + +impl<'a> SessionsFacade<'a> { + pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into) -> TaskId { + let title = title.into(); + let process_id = self.os.process_manager.spawn_vm_game(app_id, title.clone()); + let task_id = self.os.task_manager.create_game_task(process_id, app_id, title); + self.os + .lifecycle() + .set_foreground_task(task_id) + .expect("newly created game task should have an associated process"); + + task_id + } + + pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { + let title = title.into(); + let process_id = self.os.process_manager.spawn_vm_shell(app_id, title.clone()); + let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title); + self.os + .lifecycle() + .set_foreground_task(task_id) + .expect("newly created shell task should have an associated process"); + + task_id + } + + pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { + let title = title.into(); + let process_id = self.os.process_manager.spawn_native_shell(app_id, title.clone()); + let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title); + self.os + .lifecycle() + .set_foreground_task(task_id) + .expect("newly created native shell task should have an associated process"); + + task_id + } +} diff --git a/crates/console/prometeu-system/src/os/facades/vm.rs b/crates/console/prometeu-system/src/os/facades/vm.rs new file mode 100644 index 00000000..4f38d314 --- /dev/null +++ b/crates/console/prometeu-system/src/os/facades/vm.rs @@ -0,0 +1,121 @@ +use crate::CrashReport; +use crate::os::SystemOS; +use prometeu_hal::cartridge::{AppMode, Cartridge}; +use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; +use prometeu_hal::{HardwareBridge, InputSignals}; +use prometeu_vm::VirtualMachine; +use std::sync::atomic::Ordering; + +pub struct VmFacade<'a> { + pub(in crate::os) os: &'a mut SystemOS, +} + +impl<'a> VmFacade<'a> { + pub fn initialize( + &mut self, + vm: &mut VirtualMachine, + cartridge: &Cartridge, + ) -> Result<(), CrashReport> { + self.os.clear_cartridge_service_state(); + self.os.vm_runtime.initialize_vm(&mut self.os.log_service, vm, cartridge) + } + + pub fn tick( + &mut self, + vm: &mut VirtualMachine, + signals: &InputSignals, + hw: &mut dyn HardwareBridge, + ) -> Option { + self.os.vm_runtime.tick( + &mut self.os.log_service, + &mut self.os.fs, + &mut self.os.fs_state, + &mut self.os.memcard, + &mut self.os.open_files, + &mut self.os.next_handle, + vm, + signals, + hw, + ) + } + + pub fn reset(&mut self, vm: &mut VirtualMachine) { + self.os.vm_runtime.reset(vm); + } + + pub fn debug_step_instruction( + &mut self, + vm: &mut VirtualMachine, + hw: &mut dyn HardwareBridge, + ) -> Option { + self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, hw) + } + + pub fn paused(&self) -> bool { + self.os.vm_runtime.paused + } + + pub fn set_paused(&mut self, paused: bool) { + self.os.vm_runtime.paused = paused; + } + + pub fn set_inspection_active(&mut self, active: bool) { + self.os.vm_runtime.inspection_active = active; + } + + pub fn request_debug_step(&mut self) { + self.os.vm_runtime.debug_step_request = true; + } + + pub fn debug_step_requested(&self) -> bool { + self.os.vm_runtime.debug_step_request + } + + pub fn logical_frame_active(&self) -> bool { + self.os.vm_runtime.logical_frame_active + } + + pub fn tick_index(&self) -> u64 { + self.os.vm_runtime.tick_index + } + + pub fn logical_frame_index(&self) -> u64 { + self.os.vm_runtime.logical_frame_index + } + + pub fn last_frame_cpu_time_us(&self) -> u64 { + self.os.vm_runtime.last_frame_cpu_time_us + } + + pub fn completed_logical_frames(&self) -> u64 { + self.os.vm_runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed).into() + } + + pub fn telemetry_snapshot(&self) -> TelemetryFrame { + self.os.vm_runtime.atomic_telemetry.snapshot() + } + + pub fn cert_config(&self) -> &CertificationConfig { + &self.os.vm_runtime.certifier.config + } + + pub fn last_crash_report(&self) -> Option<&CrashReport> { + self.os.vm_runtime.last_crash_report.as_ref() + } + + pub fn current_app_id(&self) -> u32 { + self.os.vm_runtime.current_app_id + } + + pub fn current_cartridge_title(&self) -> String { + self.os.vm_runtime.current_cartridge_title.clone() + } + + pub fn current_cartridge_app_version(&self) -> String { + self.os.vm_runtime.current_cartridge_app_version.clone() + } + + pub fn current_cartridge_app_mode(&self) -> AppMode { + self.os.vm_runtime.current_cartridge_app_mode + } +} diff --git a/crates/console/prometeu-system/src/os/facades/window.rs b/crates/console/prometeu-system/src/os/facades/window.rs new file mode 100644 index 00000000..c22e76e0 --- /dev/null +++ b/crates/console/prometeu-system/src/os/facades/window.rs @@ -0,0 +1,37 @@ +use crate::os::SystemOS; +use prometeu_hal::color::Color; +use prometeu_hal::window::{Rect, Window, WindowId}; + +pub struct WindowFacade<'a> { + pub(in crate::os) os: &'a mut SystemOS, +} + +impl<'a> WindowFacade<'a> { + pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId { + self.os.window_manager.add_window(title, viewport, color) + } + + pub fn set_focus(&mut self, id: WindowId) { + self.os.window_manager.set_focus(id); + } + + pub fn focused_window(&self) -> Option { + self.os.window_manager.focused + } + + pub fn close_window(&mut self, id: WindowId) { + self.os.window_manager.remove_window(id); + } + + pub fn remove_all_windows(&mut self) { + self.os.window_manager.remove_all_windows(); + } + + pub fn windows(&self) -> &[Window] { + &self.os.window_manager.windows + } + + pub fn window_count(&self) -> usize { + self.os.window_manager.windows.len() + } +} diff --git a/crates/console/prometeu-system/src/os/mod.rs b/crates/console/prometeu-system/src/os/mod.rs index 5a2e6d76..a438d5cc 100644 --- a/crates/console/prometeu-system/src/os/mod.rs +++ b/crates/console/prometeu-system/src/os/mod.rs @@ -1,5 +1,7 @@ +mod facades; mod lifecycle; mod system_os; +pub use facades::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade}; pub use lifecycle::{LifecycleError, LifecycleOperation}; pub use system_os::SystemOS; diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index ae536400..d804409b 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -1,31 +1,26 @@ -use crate::CrashReport; use crate::VirtualMachineRuntime; -use crate::fs::{FsBackend, FsState, VirtualFS}; -use crate::os::{LifecycleError, LifecycleOperation}; +use crate::fs::{FsState, VirtualFS}; +use crate::os::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade}; use crate::process::ProcessManager; use crate::services::memcard::MemcardService; use crate::services::window_manager::WindowManager; use crate::task::TaskManager; -use crate::task::{TaskId, TaskState}; -use prometeu_hal::cartridge::Cartridge; -use prometeu_hal::log::{LogLevel, LogService, LogSource}; +use prometeu_hal::log::{LogEvent, 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, + pub(super) vm_runtime: VirtualMachineRuntime, + pub(super) process_manager: ProcessManager, + pub(super) task_manager: TaskManager, + pub(super) window_manager: WindowManager, + pub(super) log_service: LogService, + pub(super) fs: VirtualFS, + pub(super) fs_state: FsState, + pub(super) memcard: MemcardService, + pub(super) open_files: HashMap, + pub(super) next_handle: u32, } impl SystemOS { @@ -56,156 +51,47 @@ impl SystemOS { 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 recent_logs(&self, n: usize) -> Vec { + self.log_service.get_recent(n) } - 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 logs_after(&self, seq: u64) -> Vec { + self.log_service.get_after(seq) } - 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 lifecycle(&mut self) -> LifecycleFacade<'_> { + LifecycleFacade { os: self } } - pub fn unmount_fs(&mut self) { - self.fs.unmount(); - self.fs_state = FsState::Unmounted; + pub fn sessions(&mut self) -> SessionsFacade<'_> { + SessionsFacade { os: self } } - fn clear_cartridge_service_state(&mut self) { + pub fn vm(&mut self) -> VmFacade<'_> { + VmFacade { os: self } + } + + pub fn fs(&mut self) -> FsFacade<'_> { + FsFacade { os: self } + } + + pub fn window(&mut self) -> WindowFacade<'_> { + WindowFacade { os: self } + } + + pub(super) 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 { - let title = title.into(); - let process_id = self.process_manager.spawn_vm_game(app_id, title.clone()); - let task_id = self.task_manager.create_game_task(process_id, app_id, title); - self.set_foreground_task(task_id) - .expect("newly created game task should have an associated process"); - - task_id - } - - pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { - let title = title.into(); - let process_id = self.process_manager.spawn_vm_shell(app_id, title.clone()); - let task_id = self.task_manager.create_shell_task(process_id, app_id, title); - self.set_foreground_task(task_id) - .expect("newly created shell task should have an associated process"); - - task_id - } - - pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into) -> TaskId { - let title = title.into(); - let process_id = self.process_manager.spawn_native_shell(app_id, title.clone()); - let task_id = self.task_manager.create_shell_task(process_id, app_id, title); - self.set_foreground_task(task_id) - .expect("newly created native shell task should have an associated process"); - - task_id - } - - pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { - let process_id = self.process_id_for_task(task_id)?; - - self.task_manager.set_foreground(task_id); - self.process_manager.mark_running(process_id); - - Ok(()) - } - - pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { - let process_id = self.process_id_for_task(task_id)?; - - self.task_manager.mark_suspended(task_id); - self.process_manager.mark_suspended(process_id); - - Ok(()) - } - - pub fn resume_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { - let task = self.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; - - if task.state != TaskState::Suspended { - return Err(LifecycleError::InvalidTransition { - task_id, - from: task.state, - operation: LifecycleOperation::Resume, - }); - } - - let process_id = self.process_id_for_task(task_id)?; - - self.task_manager.set_foreground(task_id); - self.process_manager.mark_running(process_id); - - Ok(()) - } - - pub fn close_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { - let process_id = self.process_id_for_task(task_id)?; - - self.task_manager.close_task(task_id); - self.process_manager.mark_stopped(process_id); - - Ok(()) - } - - pub fn crash_task( - &mut self, - task_id: TaskId, - _report: Option<&CrashReport>, - ) -> Result<(), LifecycleError> { - let process_id = self.process_id_for_task(task_id)?; - - self.task_manager.mark_crashed(task_id); - self.process_manager.mark_crashed(process_id); - - Ok(()) - } - - fn process_id_for_task( - &self, - task_id: TaskId, - ) -> Result { - let task = self.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; - - if !self.process_manager.contains(task.process_id) { - return Err(LifecycleError::ProcessNotFound(task.process_id)); - } - - Ok(task.process_id) - } } #[cfg(test)] mod tests { use super::*; + use crate::os::{LifecycleError, LifecycleOperation}; use crate::process::ProcessState; + use crate::task::{TaskId, TaskState}; fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState { let task = os.task_manager.get(task_id).expect("task should exist"); @@ -215,10 +101,10 @@ mod tests { #[test] fn set_foreground_task_marks_task_and_process_running() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); - os.suspend_task(task_id).expect("suspend should succeed"); - os.set_foreground_task(task_id).expect("foreground should succeed"); + os.lifecycle().suspend_task(task_id).expect("suspend should succeed"); + os.lifecycle().set_foreground_task(task_id).expect("foreground should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, @@ -230,9 +116,9 @@ mod tests { #[test] fn suspend_task_marks_task_and_process_suspended() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); - os.suspend_task(task_id).expect("suspend should succeed"); + os.lifecycle().suspend_task(task_id).expect("suspend should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, @@ -244,10 +130,10 @@ mod tests { #[test] fn resume_task_marks_suspended_task_foreground_and_process_running() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); - os.suspend_task(task_id).expect("suspend should succeed"); - os.resume_task(task_id).expect("resume should succeed"); + os.lifecycle().suspend_task(task_id).expect("suspend should succeed"); + os.lifecycle().resume_task(task_id).expect("resume should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, @@ -259,10 +145,10 @@ mod tests { #[test] fn close_task_marks_task_closed_and_process_stopped_without_removing_entities() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); let process_id = os.task_manager.get(task_id).expect("task should exist").process_id; - os.close_task(task_id).expect("close should succeed"); + os.lifecycle().close_task(task_id).expect("close should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should remain").state, @@ -277,9 +163,9 @@ mod tests { #[test] fn crash_task_marks_task_and_process_crashed() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); - os.crash_task(task_id, None).expect("crash should succeed"); + os.lifecycle().crash_task(task_id, None).expect("crash should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, @@ -292,28 +178,34 @@ mod tests { fn lifecycle_operation_missing_task_returns_typed_error() { let mut os = SystemOS::new(None); - assert_eq!(os.suspend_task(TaskId(999)), Err(LifecycleError::TaskNotFound(TaskId(999)))); + assert_eq!( + os.lifecycle().suspend_task(TaskId(999)), + Err(LifecycleError::TaskNotFound(TaskId(999))) + ); } #[test] fn lifecycle_operation_missing_process_returns_typed_error() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); let process_id = os.task_manager.get(task_id).expect("task should exist").process_id; os.process_manager.mark_stopped(process_id); os.process_manager.remove_stopped(); - assert_eq!(os.suspend_task(task_id), Err(LifecycleError::ProcessNotFound(process_id))); + assert_eq!( + os.lifecycle().suspend_task(task_id), + Err(LifecycleError::ProcessNotFound(process_id)) + ); } #[test] fn resume_task_from_non_suspended_state_returns_invalid_transition() { let mut os = SystemOS::new(None); - let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); assert_eq!( - os.resume_task(task_id), + os.lifecycle().resume_task(task_id), Err(LifecycleError::InvalidTransition { task_id, from: TaskState::Foreground, 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 333ff302..3619a4bd 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 @@ -61,14 +61,15 @@ impl PrometeuHub { } if let Some((title, rect, color)) = next_window { - os.window_manager.remove_all_windows(); - let id = os.window_manager.add_window(title, rect, color); - os.window_manager.set_focus(id); + let mut window = os.window(); + window.remove_all_windows(); + let id = window.add_window(title, rect, color); + window.set_focus(id); } } pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) { - for window in &os.window_manager.windows { + for window in os.window().windows() { hw.gfx_mut().fill_rect( window.viewport.x, window.viewport.y, @@ -90,17 +91,17 @@ impl PrometeuHub { self.gui_update(os, hw); - if let Some(focused_id) = os.window_manager.focused { + if let Some(focused_id) = os.window().focused_window() { if hw.pad().start().down { - os.window_manager.remove_window(focused_id); + os.window().close_window(focused_id); } else { - crash = os.tick_vm(vm, signals, hw); + crash = os.vm().tick(vm, signals, hw); } } self.render(os, hw); hw.gfx_mut().present(); - SystemProfileUpdate { crash, focused_window_active: os.window_manager.focused.is_some() } + SystemProfileUpdate { crash, focused_window_active: os.window().focused_window().is_some() } } } diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index a1867789..c635a4f6 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -58,11 +58,7 @@ 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.os.log_service, - &mut firmware.vm, - &cartridge, - ); + let _ = firmware.os.vm().initialize(&mut firmware.vm, &cartridge); } match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { @@ -121,10 +117,10 @@ impl HostDebugger { protocol_version: DEVTOOLS_PROTOCOL_VERSION, runtime_version: "0.1".to_string(), cartridge: HandshakeCartridge { - app_id: firmware.os.vm_runtime.current_app_id, - title: firmware.os.vm_runtime.current_cartridge_title.clone(), - app_version: firmware.os.vm_runtime.current_cartridge_app_version.clone(), - app_mode: firmware.os.vm_runtime.current_cartridge_app_mode, + app_id: firmware.os.vm().current_app_id(), + title: firmware.os.vm().current_cartridge_title(), + app_version: firmware.os.vm().current_cartridge_app_version(), + app_mode: firmware.os.vm().current_cartridge_app_mode(), }, }; self.send_response(handshake); @@ -142,7 +138,7 @@ impl HostDebugger { println!("[Debugger] Connection closed by remote."); self.stream = None; // Resume VM execution if it was paused waiting for the debugger. - firmware.os.vm_runtime.paused = false; + firmware.os.vm().set_paused(false); self.waiting_for_start = false; } Ok(n) => { @@ -169,7 +165,7 @@ impl HostDebugger { Err(e) => { eprintln!("[Debugger] Connection error: {}", e); self.stream = None; - firmware.os.vm_runtime.paused = false; + firmware.os.vm().set_paused(false); self.waiting_for_start = false; } } @@ -194,27 +190,23 @@ impl HostDebugger { println!("[Debugger] Starting execution..."); self.waiting_for_start = false; } - firmware.os.vm_runtime.paused = false; + firmware.os.vm().set_paused(false); } DebugCommand::Pause => { - firmware.os.vm_runtime.paused = true; + firmware.os.vm().set_paused(true); } DebugCommand::Resume => { - firmware.os.vm_runtime.paused = false; + firmware.os.vm().set_paused(false); } 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.os.log_service, - &mut firmware.vm, - hardware, - ); + firmware.os.vm().set_paused(true); + let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, hardware); } DebugCommand::StepFrame => { // Execute until the end of the current logical frame. - firmware.os.vm_runtime.paused = false; - firmware.os.vm_runtime.debug_step_request = true; + firmware.os.vm().set_paused(false); + firmware.os.vm().request_debug_step(); } DebugCommand::GetState => { // Return detailed VM register and stack state. @@ -223,8 +215,8 @@ impl HostDebugger { let resp = DebugResponse::GetState { pc: firmware.vm.pc(), stack_top, - frame_index: firmware.os.vm_runtime.logical_frame_index, - app_id: firmware.os.vm_runtime.current_app_id, + frame_index: firmware.os.vm().logical_frame_index(), + app_id: firmware.os.vm().current_app_id(), }; self.send_response(resp); } @@ -309,14 +301,14 @@ impl HostDebugger { /// Scans the system for new information to push to the debugger client. fn stream_events(&mut self, firmware: &mut Firmware) { - if let Some(report) = firmware.os.vm_runtime.last_crash_report.as_ref() { + if let Some(report) = firmware.os.vm().last_crash_report() { self.stream_fault(report); } else { self.last_fault_summary = None; } // 1. Process and send new log entries. - let new_events = firmware.os.log_service.get_after(self.last_log_seq); + let new_events = firmware.os.logs_after(self.last_log_seq); for event in new_events { self.last_log_seq = event.seq; @@ -324,17 +316,17 @@ impl HostDebugger { if event.tag == 0xDEB1 { self.send_event(DebugEvent::BreakpointHit { pc: firmware.vm.pc(), - frame_index: firmware.os.vm_runtime.logical_frame_index, + frame_index: firmware.os.vm().logical_frame_index(), }); } if (0xCA01..=0xCA07).contains(&event.tag) - && let Some(cert_event) = Self::cert_event_from_snapshot( - event.tag, - firmware.os.vm_runtime.atomic_telemetry.snapshot(), - &firmware.os.vm_runtime.certifier.config, - firmware.os.vm_runtime.logical_frame_index, - ) + && let Some(cert_event) = { + let telemetry = firmware.os.vm().telemetry_snapshot(); + let cert_config = *firmware.os.vm().cert_config(); + let frame_index = firmware.os.vm().logical_frame_index(); + Self::cert_event_from_snapshot(event.tag, telemetry, &cert_config, frame_index) + } { self.send_event(cert_event); } @@ -347,9 +339,9 @@ impl HostDebugger { } // 2. Send telemetry snapshots at the completion of every frame. - let current_frame = firmware.os.vm_runtime.logical_frame_index; + let current_frame = firmware.os.vm().logical_frame_index(); if current_frame > self.last_telemetry_frame { - let tel = firmware.os.vm_runtime.atomic_telemetry.snapshot(); + let tel = firmware.os.vm().telemetry_snapshot(); self.send_event(Self::telemetry_event_from_snapshot(tel)); self.last_telemetry_frame = current_frame; } @@ -407,14 +399,14 @@ mod tests { let mut hardware = Hardware::new(); debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut hardware); - assert!(firmware.os.vm_runtime.paused); + assert!(firmware.os.vm().paused()); debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut hardware); - assert!(!firmware.os.vm_runtime.paused); + assert!(!firmware.os.vm().paused()); debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut hardware); - assert!(!firmware.os.vm_runtime.paused); - assert!(firmware.os.vm_runtime.debug_step_request); + assert!(!firmware.os.vm().paused()); + assert!(firmware.os.vm().debug_step_requested()); } #[test] @@ -424,11 +416,11 @@ mod tests { let mut hardware = Hardware::new(); debugger.waiting_for_start = true; - firmware.os.vm_runtime.paused = true; + firmware.os.vm().set_paused(true); debugger.handle_command(DebugCommand::Start, &mut firmware, &mut hardware); assert!(!debugger.waiting_for_start); - assert!(!firmware.os.vm_runtime.paused); + assert!(!firmware.os.vm().paused()); } } diff --git a/crates/host/prometeu-host-desktop-winit/src/overlay.rs b/crates/host/prometeu-host-desktop-winit/src/overlay.rs index ec1ae313..a18795c0 100644 --- a/crates/host/prometeu-host-desktop-winit/src/overlay.rs +++ b/crates/host/prometeu-host-desktop-winit/src/overlay.rs @@ -57,9 +57,9 @@ struct FrameCanvas<'a> { height: usize, } -pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot { - let tel = firmware.os.vm_runtime.atomic_telemetry.snapshot(); - let recent_logs = firmware.os.log_service.get_recent(10); +pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &mut Firmware) -> OverlaySnapshot { + let tel = firmware.os.vm().telemetry_snapshot(); + let recent_logs = firmware.os.recent_logs(10); let violations_count = recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count(); @@ -75,7 +75,7 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla }); } - if let Some(report) = firmware.os.vm_runtime.last_crash_report.as_ref() { + if let Some(report) = firmware.os.vm().last_crash_report() { footer.push(OverlayMetric { label: "CRASH", value: truncate_value(&report.summary(), 28), @@ -86,9 +86,8 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla let cycles_ratio = ratio(tel.cycles_used, tel.cycles_budget); let heap_total_bytes = firmware .os - .vm_runtime - .certifier - .config + .vm() + .cert_config() .max_heap_bytes .or(if tel.heap_max_bytes > 0 { Some(tel.heap_max_bytes) } else { None }) .unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES); diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index e7f61988..4d2a3b65 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.mount_fs(Box::new(backend)); + firmware.os.fs().mount(Box::new(backend)); } Self { @@ -203,8 +203,8 @@ impl HostRunner { } } - fn machine_running(&self) -> bool { - !self.firmware.os.vm_runtime.paused && !self.debugger.waiting_for_start + fn machine_running(&mut self) -> bool { + !self.firmware.os.vm().paused() && !self.debugger.waiting_for_start } } @@ -266,7 +266,7 @@ impl ApplicationHandler for HostRunner { WindowEvent::RedrawRequested => { let overlay_snapshot = - self.overlay_enabled.then(|| capture_snapshot(&self.stats, &self.firmware)); + self.overlay_enabled.then(|| capture_snapshot(&self.stats, &mut self.firmware)); // Get Pixels directly from the field (not via helper that gets the entire &mut self) let pixels = self.pixels.as_mut().expect("pixels not initialized"); @@ -320,25 +320,25 @@ impl ApplicationHandler for HostRunner { fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { let was_debugger_connected = self.debugger.stream.is_some(); let was_waiting_for_start = self.debugger.waiting_for_start; - let was_paused = self.firmware.os.vm_runtime.paused; + let was_paused = self.firmware.os.vm().paused(); // 1. Process pending debug commands from the network. self.debugger.check_commands(&mut self.firmware, &mut self.hardware); // Sync inspection mode state. This is host-owned overlay/debugger control, // not a guest-visible debug ABI switch. - self.firmware.os.vm_runtime.inspection_active = - self.overlay_enabled || self.debugger.stream.is_some(); + self.firmware + .os + .vm() + .set_inspection_active(self.overlay_enabled || self.debugger.stream.is_some()); - // 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.fs_state, FsState::Unmounted | FsState::Error(_)) - && std::path::Path::new(root).exists() - { - let backend = HostDirBackend::new(root); - self.firmware.os.mount_fs(Box::new(backend)); - } + // 2. Maintain a filesystem connection if it was lost (e.g., directory removed). + if let Some(root) = &self.fs_root + && self.firmware.os.fs().needs_mount() + && std::path::Path::new(root).exists() + { + let backend = HostDirBackend::new(root); + self.firmware.os.fs().mount(Box::new(backend)); } // 3. Timing Management (The heart of determinism). @@ -362,13 +362,13 @@ impl ApplicationHandler for HostRunner { // Unless the debugger is waiting for a 'start' command, advance the system. if !self.debugger.waiting_for_start { self.firmware.tick(&self.input.signals, &mut self.hardware); - self.stats.record_host_cpu_time(self.firmware.os.vm_runtime.last_frame_cpu_time_us); + self.stats.record_host_cpu_time(self.firmware.os.vm().last_frame_cpu_time_us()); } // Sync pause state with audio. // We do this AFTER firmware.tick to avoid MasterPause/Resume commands // being cleared by the OS if a new logical frame starts in this tick. - let is_paused = self.firmware.os.vm_runtime.paused || self.debugger.waiting_for_start; + let is_paused = self.firmware.os.vm().paused() || self.debugger.waiting_for_start; if is_paused != self.last_paused_state { self.last_paused_state = is_paused; let cmd = @@ -383,11 +383,11 @@ impl ApplicationHandler for HostRunner { self.stats.record_frame(); } - self.presentation.note_published_frame(self.firmware.os.vm_runtime.logical_frame_index); + self.presentation.note_published_frame(self.firmware.os.vm().logical_frame_index()); if was_debugger_connected != self.debugger.stream.is_some() || was_waiting_for_start != self.debugger.waiting_for_start - || was_paused != self.firmware.os.vm_runtime.paused + || was_paused != self.firmware.os.vm().paused() { self.invalidate_host_surface(); } @@ -396,14 +396,14 @@ impl ApplicationHandler for HostRunner { self.audio.update_stats(&mut self.stats); // Update technical statistics displayed in the window title. - self.stats.update(now, self.window, &self.hardware, &self.firmware); + self.stats.update(now, self.window, &self.hardware, &mut self.firmware); // 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.log_service.get_recent(4096) + self.firmware.os.recent_logs(4096) } else { - self.firmware.os.log_service.get_after(last_seq) + self.firmware.os.logs_after(last_seq) }; self.log_sink.process_events(new_events); @@ -813,7 +813,7 @@ mod tests { stream.write_all(b"{\"type\":\"pause\"}\n").expect("Should write pause"); std::thread::sleep(std::time::Duration::from_millis(50)); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); - assert!(runner.firmware.os.vm_runtime.paused, "VM should be paused"); + assert!(runner.firmware.os.vm().paused(), "VM should be paused"); } // 2. Disconnect (stream goes out of scope) @@ -821,7 +821,7 @@ mod tests { runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // 3. Check if unpaused - assert!(!runner.firmware.os.vm_runtime.paused, "VM should have unpaused after disconnect"); + assert!(!runner.firmware.os.vm().paused(), "VM should have unpaused after disconnect"); assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state"); } } diff --git a/crates/host/prometeu-host-desktop-winit/src/stats.rs b/crates/host/prometeu-host-desktop-winit/src/stats.rs index a14b4994..fb20f9c7 100644 --- a/crates/host/prometeu-host-desktop-winit/src/stats.rs +++ b/crates/host/prometeu-host-desktop-winit/src/stats.rs @@ -65,7 +65,7 @@ impl HostStats { now: Instant, window: Option<&Window>, _hardware: &Hardware, - firmware: &Firmware, + firmware: &mut Firmware, ) { let stats_elapsed = now.duration_since(self.last_stats_update); if stats_elapsed >= Duration::from_secs(1) { @@ -88,14 +88,9 @@ impl HostStats { self.current_fps, cpu_load_core, cpu_load_audio, - firmware.os.vm_runtime.tick_index, - firmware.os.vm_runtime.logical_frame_index, - firmware - .os - .vm_runtime - .atomic_telemetry - .completed_logical_frames - .load(std::sync::atomic::Ordering::Relaxed), + firmware.os.vm().tick_index(), + firmware.os.vm().logical_frame_index(), + firmware.os.vm().completed_logical_frames(), ); window.set_title(&title); } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index f2e2d13d..fbd229b4 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,5 @@ -{"type":"meta","next_id":{"DSC":34,"AGD":34,"DEC":26,"PLN":58,"LSN":41,"CLSN":1}} +{"type":"meta","next_id":{"DSC":35,"AGD":35,"DEC":27,"PLN":59,"LSN":41,"CLSN":1}} +{"type":"discussion","id":"DSC-0034","status":"in_progress","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-agenda-systemos-domain-facades.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-systemos-domain-facades.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0058","file":"PLN-0058-systemos-domain-facades-first-wave.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0026"]}],"lessons":[]} {"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"}]} diff --git a/discussion/workflow/agendas/AGD-0034-agenda-systemos-domain-facades.md b/discussion/workflow/agendas/AGD-0034-agenda-systemos-domain-facades.md new file mode 100644 index 00000000..2e0e240a --- /dev/null +++ b/discussion/workflow/agendas/AGD-0034-agenda-systemos-domain-facades.md @@ -0,0 +1,353 @@ +--- +id: AGD-0034 +ticket: system-os-domain-facades +title: Agenda - SystemOS Domain Facades +status: accepted +created: 2026-05-15 +resolved: 2026-05-15 +decision: DEC-0026 +tags: [runtime, os, services, api-surface, lifecycle, fs] +--- + +# Agenda - SystemOS Domain Facades + +## Contexto + +Depois de `DEC-0024` e `DEC-0025`, `SystemOS` passou a concentrar ownership e +coordenação de vários domínios do Prometeu OS: + +```text +SystemOS + vm_runtime + process_manager + task_manager + window_manager + log_service + fs / fs_state / open_files / next_handle + memcard + lifecycle methods + VM initialize/tick helpers +``` + +Essa concentração é correta do ponto de vista de ownership: o OS deve ser dono +ou mediador dos serviços do console. O problema agora é a superfície pública do +root. Se tudo fica como `os.bla()`, `os.campo.bla()` ou campos públicos soltos, +o `SystemOS` vira um objeto largo demais e deixa de comunicar domínio. + +A intenção proposta é separar a superfície em compartimentos: + +```text +os.lifecycle().suspend_task(...) +os.lifecycle().resume_task(...) +os.lifecycle().crash_task(...) + +os.sessions().create_vm_game_task(...) +os.sessions().create_vm_shell_task(...) +os.sessions().create_native_shell_task(...) + +os.vm().initialize(...) +os.vm().tick(...) + +os.fs().mount(...) +os.fs().open(...) +os.fs().read(...) +os.fs().write(...) +os.fs().close(...) + +os.window().add_window(...) +os.window().set_focus(...) +os.window().focused_window(...) +os.window().close_window(...) +``` + +Logging pode continuar na raiz como operação transversal: + +```text +os.log(...) +``` + +## Problema + +O `SystemOS` está crescendo wild na raiz. Isso cria alguns riscos: + +- novos domínios tendem a adicionar mais métodos diretamente em `SystemOS`; +- lifecycle, filesystem, window, VM runtime e memcard competem pela mesma + superfície; +- testes e firmware continuam alcançando managers internos diretamente; +- fica difícil distinguir API semântica de OS de detalhe interno de serviço; +- a raiz pública pode virar uma coleção de campos públicos em vez de uma + interface de sistema. + +O ponto importante: não queremos desfazer o ownership do `SystemOS`. Queremos +compartimentalizar o acesso. + +## Pontos Criticos + +- `SystemOS` deve continuar sendo o boundary de ownership e coordenação. +- Facades não devem virar owners independentes que duplicam estado. +- A raiz do OS deve ficar pequena e expressiva. +- Lifecycle deve continuar coordenando `TaskState` e `ProcessState` junto. +- Filesystem tem estado composto (`VirtualFS`, `FsState`, `open_files`, + `next_handle`) e por isso se beneficia de uma facade própria. +- Window management já é serviço do OS, mas `PrometeuHub` e firmware ainda + acessam `os.window_manager` diretamente. +- VM runtime talvez também precise de facade ou área própria, mas isso pode ser + uma etapa posterior para não misturar com lifecycle/fs. +- O formato da facade precisa respeitar borrow rules em Rust; campos públicos + simples podem não ser viáveis quando uma operação precisa de múltiplos campos + internos do `SystemOS`. + +## Opcoes + +### Opcao A - Manter tudo na raiz do SystemOS + +**Abordagem:** +Continuar adicionando métodos e campos diretamente em `SystemOS`. + +**Vantagens:** +- menor mudança imediata; +- callsites simples; +- menos tipos auxiliares; +- evita atrito com borrow checker. + +**Custos / Riscos:** +- a raiz continua crescendo; +- domínios ficam misturados; +- incentiva acesso direto a managers internos; +- torna mais difícil explicar a API do OS; +- cada nova capability aumenta a pressão no mesmo arquivo e no mesmo objeto. + +**Manutenibilidade:** +Fraca no médio prazo. + +### Opcao B - Criar facades de domínio em torno do SystemOS + +**Abordagem:** +Criar facades estreitas por domínio, expostas como acesso semântico: + +```text +os.lifecycle().suspend_task(...) +os.fs().mount(...) +os.window().set_focus(...) +``` + +Ou, se o design permitir sem duplicar ownership: + +```text +os.lifecycle.suspend_task(...) +os.fs.mount(...) +os.window.set_focus(...) +``` + +As facades seriam views/handles sobre os campos internos do `SystemOS`, não +owners independentes. + +**Vantagens:** +- deixa a raiz do OS menor; +- agrupa API por domínio; +- torna mais claro o que é lifecycle, fs, window, memcard e VM; +- ajuda a esconder managers internos; +- reduz a chance de firmware coordenar detalhes por fora. + +**Custos / Riscos:** +- exige decidir padrão de borrowing; +- pode adicionar tipos pequenos e boilerplate; +- `os.lifecycle.bla()` como campo direto pode ser difícil se a facade precisar + acessar `task_manager` e `process_manager` que vivem no mesmo `SystemOS`; +- talvez precise começar com métodos `os.lifecycle()` em vez de campos públicos. + +**Manutenibilidade:** +Forte se as facades forem views estreitas e não novos owners. + +### Opcao C - Criar services owners independentes dentro do SystemOS + +**Abordagem:** +Mover estado e lógica para structs owned diretamente: + +```text +SystemOS + lifecycle: LifecycleService + fs: FileSystemService + window: WindowService +``` + +Cada service seria owner do seu próprio estado ou de parte dele. + +**Vantagens:** +- aproxima o shape desejado `os.lifecycle.bla()`; +- reduz métodos no root; +- pode simplificar alguns domínios autocontidos. + +**Custos / Riscos:** +- lifecycle precisa coordenar task/process, então ownership separado pode criar + borrow/coordenação mais difícil; +- filesystem hoje cruza VM syscall state, `VirtualFS`, `FsState`, handles e + logging; +- pode reabrir decisões de ownership que acabaram de estabilizar; +- risco de mover estado cedo demais para caber numa estética de API. + +**Manutenibilidade:** +Boa para domínios autocontidos, arriscada para lifecycle neste momento. + +## Sugestao / Recomendacao + +A direção recomendada é **Opção B: facades de domínio como views estreitas sobre +o `SystemOS`**. + +O corte recomendado é: + +```text +SystemOS + lifecycle() + set_foreground_task + suspend_task + resume_task + close_task + crash_task + + sessions() + create_vm_game_task + create_vm_shell_task + create_native_shell_task + + vm() + initialize + tick + + fs() + mount + open + read + write + close + + window() + add_window + set_focus + focused_window + close_window + + log(...) +``` + +E evitar que a raiz acumule: + +```text +suspend_task +resume_task +close_task +crash_task +mount_fs +initialize_vm +tick_vm +create_vm_game_task +create_vm_shell_task +create_native_shell_task +window_manager direto +task_manager direto +process_manager direto +vm_runtime direto +fs direto +memcard direto +``` + +Para Rust, o primeiro corte provavelmente deve usar métodos de acesso que +retornam views: + +```rust +os.lifecycle().suspend_task(task_id) +os.fs().mount(backend) +os.window().set_focus(window_id) +``` + +Isso é menos bonito que campo direto, mas evita congelar ownership errado. Se +mais tarde algum domínio puder virar service owner real, podemos migrar para +campo direto sem mudar a semântica. + +Logging pode permanecer na raiz: + +```rust +os.log(...) +``` + +Motivo: logging é transversal, curto, usado por várias etapas do firmware, e não +carrega sozinho uma política complexa de estado. + +## Perguntas em Aberto + +- [x] Queremos a sintaxe ideal `os.lifecycle.bla()` mesmo que isso force + services owners reais, ou aceitamos `os.lifecycle().bla()` como primeira forma + borrow-friendly? quando escrevi os.lifecycle.bla() eu queria mesmo era os.lifecycle().bla() +- [x] Quais domínios entram no primeiro corte: só `lifecycle` e `fs`, ou também + `window`? nao precisamos criar nada, mas lifecycle, fs, window e vm sao easy wins no meu ponto de vista +- [x] `create_vm_game_task` e `create_vm_shell_task` pertencem a + `os.lifecycle()`, a `os.processes()/os.tasks()`, ou a um futuro domínio + `os.apps()/os.sessions()`? vamos de os.session() para esses. +- [x] `tick_vm` e `initialize_vm` devem ficar temporariamente na raiz, ou já + entrar em `os.vm()`? isso, os.vm(). +- [x] Campos como `task_manager`, `process_manager`, `window_manager`, + `vm_runtime`, `fs`, `memcard` devem deixar de ser públicos no mesmo corte ou + em planos graduais? eu nao os deixaria publicos desde jah, isso forca a politica de facades dentro de system os. +- [x] Como preservar testes sem criar APIs públicas só para inspeção? Testes devem ser feitos nos subdomínios; SystemOS só testa composição e coordenação que não pertence isoladamente a um domínio. + +## Resolucao Proposta + +A agenda deve fechar pela **Opção B**. + +O `SystemOS` continua sendo o owner e boundary de coordenação, mas sua superfície +pública deve ser compartimentalizada em facades de domínio. A forma normativa +inicial deve ser por métodos que retornam views borrow-friendly: + +```rust +os.lifecycle().suspend_task(task_id) +os.sessions().create_vm_game_task(app_id, title) +os.vm().initialize(vm, cartridge) +os.fs().mount(backend) +os.window().set_focus(window_id) +os.log(level, source, tag, msg) +``` + +A sintaxe com campo direto (`os.lifecycle.suspend_task(...)`) não é requisito +deste corte. A intenção semântica é domínio explícito; a forma prática inicial +deve respeitar ownership e borrowing em Rust. + +Domínios do primeiro corte: + +- `lifecycle()`: lifecycle semântico de task/process. +- `sessions()`: criação de tasks/processos de jogo e shell. +- `vm()`: inicialização e tick da VM. +- `fs()`: filesystem operacional. +- `window()`: operações mínimas de janela/foco. +- `log(...)`: permanece na raiz como operação transversal. + +Campos internos como `task_manager`, `process_manager`, `window_manager`, +`vm_runtime`, `fs`, `fs_state`, `memcard`, `open_files` e `next_handle` devem +deixar de ser públicos já neste corte, salvo se um plan específico demonstrar +necessidade temporária de migração. A política desejada é forçar acesso por +facades dentro do `SystemOS`. + +Testes devem migrar para os subdomínios quando estiverem validando comportamento +de domínio. `SystemOS` deve testar apenas composição e coordenação que realmente +não pertence isoladamente a um domínio. + +## Criterio para Encerrar + +Esta agenda pode virar decisão quando fecharmos: + +- qual forma de facade será normativa: campo direto, método view, ou service + owner; **resolvido: método view borrow-friendly.** +- quais domínios entram no primeiro corte; + **resolvido: lifecycle, sessions, vm, fs, window e log na raiz.** +- quais APIs permanecem no root; + **resolvido: `new`/construção e `log(...)`; demais APIs devem migrar para + facades.** +- se campos internos deixam de ser públicos agora ou em ondas; + **resolvido: deixar de ser públicos já no corte, com exceção transitória + somente se plan demonstrar necessidade.** +- como lifecycle, fs e window serão acessados por firmware, Hub e testes. + **resolvido: por facades; testes de domínio nos subdomínios.** + +## Proximo Passo + +Esta agenda está pronta para decisão normativa se a resolução proposta for +aceita. diff --git a/discussion/workflow/decisions/DEC-0026-systemos-domain-facades.md b/discussion/workflow/decisions/DEC-0026-systemos-domain-facades.md new file mode 100644 index 00000000..f6bf6cc3 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0026-systemos-domain-facades.md @@ -0,0 +1,189 @@ +--- +id: DEC-0026 +ticket: system-os-domain-facades +title: SystemOS Domain Facades +status: accepted +created: 2026-05-15 +ref_agenda: AGD-0034 +tags: [runtime, os, services, api-surface, lifecycle, fs] +--- + +## Status + +Accepted. Esta decisão foi aceita em 2026-05-15 e passa a ser referência +normativa para compartimentalizar a superfície pública do `SystemOS`. + +## Contexto + +`DEC-0024` consolidou `SystemOS` como owner de serviços do Prometeu OS. +`DEC-0025` colocou o lifecycle de `Task` e `Process` sob autoridade do +`SystemOS`. + +Essas decisões estão corretas em termos de ownership, mas fizeram a raiz do +`SystemOS` crescer como superfície pública. Hoje a raiz mistura runtime VM, +filesystem, memcard, window management, lifecycle, managers internos, handles +operacionais e helpers de criação de tasks/processos. + +Esta decisão não muda o ownership: `SystemOS` continua sendo o boundary do OS. +O que muda é a forma normativa de acesso aos domínios. + +## Decisao + +`SystemOS` SHALL expose OS behavior through domain facades. + +The canonical first-wave access shape SHALL be method-based, borrow-friendly +views: + +```rust +os.lifecycle().suspend_task(task_id) +os.sessions().create_vm_game_task(app_id, title) +os.vm().initialize(vm, cartridge) +os.fs().mount(backend) +os.window().set_focus(window_id) +os.log(level, source, tag, msg) +``` + +Direct field syntax such as `os.lifecycle.suspend_task(...)` is NOT required in +this wave. Domain clarity is normative; direct field ownership is not. + +The first-wave `SystemOS` root SHALL keep only minimal root operations: + +```text +SystemOS + new(...) + log(...) + lifecycle() + sessions() + vm() + fs() + window() +``` + +`log(...)` MAY remain on the root because logging is a cross-cutting operation +used by multiple firmware and OS domains. + +The first-wave domain surface SHALL be: + +```text +lifecycle() + set_foreground_task + suspend_task + resume_task + close_task + crash_task + +sessions() + create_vm_game_task + create_vm_shell_task + create_native_shell_task + +vm() + initialize + tick + +fs() + mount + open + read + write + close + +window() + add_window + set_focus + focused_window + close_window +``` + +Internal service state SHALL NOT remain public on `SystemOS` once the +corresponding facade exists. This includes: + +- `task_manager` +- `process_manager` +- `window_manager` +- `vm_runtime` +- `fs` +- `fs_state` +- `memcard` +- `open_files` +- `next_handle` + +Plans MAY use temporary transitional access only when needed to keep migrations +small, but the completed first-wave facade migration MUST remove direct public +access for migrated domains. + +## Rationale + +`SystemOS` should remain the owner of OS services, but callers should not see it +as a bag of unrelated fields and root methods. Domain facades make the API read +like an OS surface: + +- lifecycle operations live under lifecycle; +- session construction lives under sessions; +- VM execution lives under vm; +- filesystem operations live under fs; +- window operations live under window; +- logging remains root-level because it cuts across domains. + +Method-based facades are preferred over direct fields in this wave because they +preserve Rust borrowing flexibility. A facade can be a temporary view over +multiple internal fields without forcing premature service ownership splits. + +This also protects `DEC-0025`: lifecycle remains the semantic coordinator for +`TaskState` and `ProcessState`, but callers reach it through +`os.lifecycle()`, not through root methods or direct manager access. + +## Invariantes / Contrato + +- `SystemOS` MUST remain the OS ownership and coordination boundary. +- Facades MUST be views or handles over `SystemOS` internals unless a later + decision explicitly promotes a domain to an independently owned service. +- Facades MUST NOT duplicate state. +- Root-level `SystemOS` API MUST stay minimal. +- Lifecycle operations MUST move from root-level `SystemOS` methods to + `os.lifecycle()`. +- Session creation helpers MUST move to `os.sessions()`. +- VM initialization and ticking MUST move to `os.vm()`. +- Filesystem operations MUST move to `os.fs()`. +- Window operations MUST move to `os.window()`. +- Logging MAY remain as `os.log(...)`. +- Firmware, Hub and tests SHOULD use facades instead of direct internal fields. +- Tests for domain behavior SHOULD live at the facade/domain level. +- `SystemOS` tests SHOULD focus on composition and cross-domain coordination. + +## Impactos + +- `crates/console/prometeu-system/src/os/` should gain facade/view types for + lifecycle, sessions, VM, filesystem and window domains. +- Existing root methods such as `suspend_task`, `resume_task`, `close_task`, + `crash_task`, `initialize_vm`, `tick_vm`, `mount_fs`, `create_vm_game_task`, + `create_vm_shell_task` and `create_native_shell_task` should migrate behind + domain facades. +- Direct access to managers and service fields from firmware, Hub and tests + should be removed or narrowed. +- Filesystem state may require a dedicated facade because it spans `VirtualFS`, + `FsState`, file handles, memcard-adjacent state and logging. +- Window access should move away from `os.window_manager` to `os.window()`. +- Specs do not need immediate updates unless these facades become part of a + published external API. + +## Referencias + +- Agenda: `AGD-0034` +- Related decision: `DEC-0024` +- Related decision: `DEC-0025` + +## Propagacao Necessaria + +- Plans: create small plans for lifecycle facade, sessions/vm facade, fs facade, + window facade and internal field encapsulation. +- Code: update `prometeu-system`, firmware, Hub and tests to use domain + facades. +- Tests: move lifecycle/session/fs/window assertions to domain-level tests where + possible. +- Discussion: keep this decision separate from future decisions about true + service ownership splits. + +## Revisao + +- 2026-05-15: Initial accepted decision from `AGD-0034`. diff --git a/discussion/workflow/plans/PLN-0058-systemos-domain-facades-first-wave.md b/discussion/workflow/plans/PLN-0058-systemos-domain-facades-first-wave.md new file mode 100644 index 00000000..0ac1a228 --- /dev/null +++ b/discussion/workflow/plans/PLN-0058-systemos-domain-facades-first-wave.md @@ -0,0 +1,223 @@ +--- +id: PLN-0058 +ticket: system-os-domain-facades +title: SystemOS Domain Facades First Wave +status: open +created: 2026-05-15 +ref_decisions: [DEC-0026] +tags: [runtime, os, services, api-surface, lifecycle, fs] +--- + +## Briefing + +Implement the first wave of `SystemOS` domain facades required by `DEC-0026`. +`SystemOS` remains the OS ownership boundary, but callers must access behavior +through domain views instead of root-level methods or public internal fields. + +## Source Decisions + +- `DEC-0026`: `SystemOS` must expose method-based domain facades for lifecycle, + sessions, VM, filesystem and window operations, while keeping `log(...)` on + the root. +- Related: `DEC-0025` keeps lifecycle coordination under `SystemOS`. +- Related: `DEC-0024` keeps services owned or mediated by `SystemOS`. + +## Target + +Create these first-wave access shapes: + +```rust +os.lifecycle().suspend_task(task_id) +os.sessions().create_vm_game_task(app_id, title) +os.vm().initialize(vm, cartridge) +os.vm().tick(vm, signals, hw) +os.fs().mount(backend) +os.window().set_focus(window_id) +os.log(level, source, tag, msg) +``` + +After this plan, migrated domains must no longer require public direct access +to `task_manager`, `process_manager`, `window_manager`, `vm_runtime`, `fs`, +`fs_state`, `memcard`, `open_files` or `next_handle`. + +## Scope + +- Add facade/view types under `crates/console/prometeu-system/src/os/`. +- Add `SystemOS` methods: + - `lifecycle()` + - `sessions()` + - `vm()` + - `fs()` + - `window()` +- Move root lifecycle methods behind `lifecycle()`. +- Move task/process session creation behind `sessions()`. +- Move VM initialization/ticking/reset or execution helpers behind `vm()`. +- Move filesystem mount/open/read/write/close entry points behind `fs()` when + the operation exists at OS level. +- Move window operations behind `window()`. +- Keep `SystemOS::log(...)` at root. +- Migrate firmware, Hub and tests to facade access. +- Make migrated internal `SystemOS` fields private. + +## Out of Scope + +- Changing service ownership established by `DEC-0024`. +- Changing lifecycle semantics established by `DEC-0025`. +- Introducing `os.lifecycle` as a direct public field. +- Background lifecycle semantics. +- New filesystem syscall behavior beyond exposing existing OS-owned operations. +- Diagnostics/crash report persistence. +- Redesigning Hub UI or app switching. +- Replacing VM runtime tests that intentionally test `VirtualMachineRuntime` + directly. + +## Execution Plan + +1. Create facade modules and view types. + - Target files: + - `crates/console/prometeu-system/src/os/mod.rs` + - `crates/console/prometeu-system/src/os/system_os.rs` + - new facade modules under `crates/console/prometeu-system/src/os/` + - Define mutable view structs such as `LifecycleFacade<'a>`, + `SessionsFacade<'a>`, `VmFacade<'a>`, `FsFacade<'a>` and + `WindowFacade<'a>`. + - Each facade should borrow the needed `SystemOS` internals and must not own + duplicated state. + +2. Add `SystemOS` facade accessors. + - Add `SystemOS::lifecycle(&mut self) -> LifecycleFacade<'_>`. + - Add `SystemOS::sessions(&mut self) -> SessionsFacade<'_>`. + - Add `SystemOS::vm(&mut self) -> VmFacade<'_>`. + - Add `SystemOS::fs(&mut self) -> FsFacade<'_>`. + - Add `SystemOS::window(&mut self) -> WindowFacade<'_>`. + - Keep `SystemOS::new(...)` and `SystemOS::log(...)` on the root. + +3. Move lifecycle operations. + - Target existing logic in `crates/console/prometeu-system/src/os/system_os.rs`. + - Move or delegate: + - `set_foreground_task` + - `suspend_task` + - `resume_task` + - `close_task` + - `crash_task` + - The facade must preserve typed `LifecycleError` behavior. + - Callers should use `os.lifecycle().crash_task(...)` rather than + `os.crash_task(...)`. + +4. Move session creation operations. + - Move or delegate: + - `create_vm_game_task` + - `create_vm_shell_task` + - `create_native_shell_task` + - These operations may call the lifecycle facade internally to foreground + newly created tasks. + - Firmware should use `ctx.os.sessions().create_vm_game_task(...)` and + `ctx.os.sessions().create_vm_shell_task(...)`. + +5. Move VM operations. + - Move or delegate: + - `initialize_vm` as `os.vm().initialize(...)` + - `tick_vm` as `os.vm().tick(...)` + - Include reset access if needed to remove direct `os.vm_runtime.reset(...)` + from firmware. + - Preserve VM runtime behavior and logging. + +6. Move filesystem operations. + - Move or delegate existing root filesystem operations: + - `mount_fs` as `os.fs().mount(...)` + - `unmount_fs` if still present, even though `DEC-0026` only names + mount/open/read/write/close for the target surface. + - Expose OS-owned open/read/write/close only if those operations already have + a suitable service-level implementation; otherwise leave them as explicit + follow-up within the facade module without inventing new behavior. + - Keep filesystem state private to `SystemOS`/facade internals. + +7. Move window operations. + - Target: + - `crates/console/prometeu-system/src/services/window_manager.rs` + - `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` + - firmware launch tests and cartridge loading. + - Provide facade operations: + - `add_window` + - `set_focus` + - `focused_window` + - `close_window` + - Add any narrow helper needed to preserve existing behavior such as removing + all windows, but keep it under `window()` rather than exposing + `window_manager`. + +8. Encapsulate migrated fields. + - In `SystemOS`, remove `pub` from migrated internals once callsites have + moved: + - `vm_runtime` + - `process_manager` + - `task_manager` + - `window_manager` + - `fs` + - `fs_state` + - `memcard` + - `open_files` + - `next_handle` + - Keep `log_service` private unless a plan proves a public log handle is + required. + - Avoid adding compatibility root methods for the old surface. + +9. Update tests. + - Move `SystemOS` lifecycle tests to facade calls. + - Update firmware tests to observe state through facades or domain-specific + inspection helpers rather than direct internal fields. + - Preserve direct `VirtualMachineRuntime` tests where they test the VM runtime + service itself, not `SystemOS`. + +## Acceptance Criteria + +- `SystemOS` exposes `lifecycle()`, `sessions()`, `vm()`, `fs()` and `window()`. +- Root `SystemOS` keeps `new(...)` and `log(...)`. +- Lifecycle callsites use `os.lifecycle()`. +- Session creation callsites use `os.sessions()`. +- VM initialization/tick/reset callsites use `os.vm()`. +- Window callsites use `os.window()`. +- Existing root methods migrated by this plan are removed or made private. +- Migrated internal fields no longer remain public on `SystemOS`. +- No facade duplicates service state. +- Existing behavior remains unchanged. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- Run `cargo test -p prometeu-firmware`. +- Run `discussion validate`. +- Confirm with search that migrated callsites no longer use: + - `os.task_manager` + - `os.process_manager` + - `os.window_manager` + - `os.vm_runtime` + - `os.suspend_task` + - `os.resume_task` + - `os.close_task` + - `os.crash_task` + - `os.initialize_vm` + - `os.tick_vm` + - `os.create_vm_game_task` + - `os.create_vm_shell_task` + +## Risks + +- Borrowing may require facades to expose narrow methods rather than returning + long-lived mutable references. Keep facades short-lived and method-based. +- Some tests currently inspect internal fields directly. Prefer domain-level + inspection helpers over keeping fields public. +- Filesystem `open/read/write/close` may not all exist as OS-level operations + yet. Do not invent new filesystem semantics while implementing the facade. +- Removing all public internals in one change can be broad. If a field cannot be + hidden without unrelated redesign, document the temporary exception in the + implementation and leave a follow-up plan. + +## Affected Artifacts + +- `crates/console/prometeu-system/src/os/**` +- `crates/console/prometeu-system/src/programs/prometeu_hub/**` +- `crates/console/prometeu-system/src/services/window_manager.rs` +- `crates/console/prometeu-firmware/src/firmware/**` +- `discussion/index.ndjson` +- `discussion/workflow/plans/PLN-0058-systemos-domain-facades-first-wave.md`