Merge pull request 'dev/system-os-service-ownership-and-module-layout' (#26) from dev/system-os-service-ownership-and-module-layout into master
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good

Reviewed-on: #26
This commit is contained in:
bquarkz 2026-05-15 04:26:47 +00:00
commit 5282089c7d
34 changed files with 1944 additions and 237 deletions

View File

@ -339,8 +339,8 @@ mod tests {
firmware.tick(&signals, &mut hardware); firmware.tick(&signals, &mut hardware);
assert!(matches!(firmware.state, FirmwareState::SystemRunning(_))); assert!(matches!(firmware.state, FirmwareState::SystemRunning(_)));
assert!(firmware.hub.window_manager.focused.is_some()); assert!(firmware.os.window_manager.focused.is_some());
assert_eq!(firmware.hub.window_manager.windows.len(), 1); assert_eq!(firmware.os.window_manager.windows.len(), 1);
} }
#[test] #[test]

View File

@ -15,12 +15,7 @@ impl AppCrashesStep {
} }
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log( ctx.os.log(LogLevel::Error, LogSource::Pos, self.report.log_tag(), self.log_message());
LogLevel::Error,
LogSource::Pos,
self.report.log_tag(),
self.log_message(),
);
} }
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {

View File

@ -15,12 +15,7 @@ impl GameRunningStep {
} }
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log( ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Entering GameRunning".to_string());
LogLevel::Info,
LogSource::Pos,
0,
"Entering GameRunning".to_string(),
);
} }
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
@ -40,7 +35,7 @@ impl GameRunningStep {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); 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 { if !ctx.os.vm_runtime.logical_frame_active {
ctx.hw.gfx_mut().present(); ctx.hw.gfx_mut().present();

View File

@ -7,7 +7,7 @@ pub struct HubHomeStep;
impl HubHomeStep { impl HubHomeStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { 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<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {

View File

@ -9,7 +9,7 @@ pub struct LaunchHubStep;
impl LaunchHubStep { impl LaunchHubStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { 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(); ctx.hub.init();
} }
@ -22,7 +22,7 @@ impl LaunchHubStep {
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge))); return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
} }
Err(e) => { Err(e) => {
ctx.os.vm_runtime.log( ctx.os.log(
LogLevel::Error, LogLevel::Error,
LogSource::Pos, LogSource::Pos,
0, 0,

View File

@ -20,7 +20,7 @@ impl LoadCartridgeStep {
} }
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log( ctx.os.log(
LogLevel::Info, LogLevel::Info,
LogSource::Pos, LogSource::Pos,
0, 0,
@ -34,7 +34,7 @@ impl LoadCartridgeStep {
self.cartridge.assets.clone(), 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<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
@ -43,12 +43,12 @@ impl LoadCartridgeStep {
} }
if self.cartridge.app_mode == AppMode::Shell { 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(), self.cartridge.title.clone(),
Rect { x: 40, y: 20, w: 240, h: 140 }, Rect { x: 40, y: 20, w: 240, h: 140 },
Color::WHITE, Color::WHITE,
); );
ctx.hub.window_manager.set_focus(id); ctx.os.window_manager.set_focus(id);
let task_id = let task_id =
ctx.os.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone()); ctx.os.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone());

View File

@ -8,7 +8,7 @@ pub struct ResetStep;
impl ResetStep { impl ResetStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { 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); ctx.os.vm_runtime.reset(ctx.vm);
} }

View File

@ -10,12 +10,7 @@ pub struct SplashScreenStep {
impl SplashScreenStep { impl SplashScreenStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log( ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Showing SplashScreen".to_string());
LogLevel::Info,
LogSource::Pos,
0,
"Showing SplashScreen".to_string(),
);
// Play sound on enter // Play sound on enter
// ctx.hw.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off); // ctx.hw.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off);
} }

View File

@ -14,12 +14,7 @@ impl SystemRunningStep {
} }
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log( ctx.os.log(LogLevel::Info, LogSource::Hub, 0, "Entering SystemRunning".to_string());
LogLevel::Info,
LogSource::Hub,
0,
"Entering SystemRunning".to_string(),
);
} }
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {

View File

@ -2,7 +2,6 @@ mod crash_report;
mod os; mod os;
mod programs; mod programs;
mod services; mod services;
mod virtual_machine_runtime;
pub use crash_report::CrashReport; pub use crash_report::CrashReport;
pub use os::SystemOS; pub use os::SystemOS;
@ -10,4 +9,4 @@ pub use programs::PrometeuHub;
pub use services::fs; pub use services::fs;
pub use services::process; pub use services::process;
pub use services::task; pub use services::task;
pub use virtual_machine_runtime::VirtualMachineRuntime; pub use services::vm_runtime::VirtualMachineRuntime;

View File

@ -1,22 +1,101 @@
use crate::CrashReport;
use crate::VirtualMachineRuntime; use crate::VirtualMachineRuntime;
use crate::fs::{FsBackend, FsState, VirtualFS};
use crate::process::ProcessManager; use crate::process::ProcessManager;
use crate::services::memcard::MemcardService;
use crate::services::window_manager::WindowManager;
use crate::task::TaskId; use crate::task::TaskId;
use crate::task::TaskManager; use crate::task::TaskManager;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::telemetry::CertificationConfig; 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 struct SystemOS {
pub vm_runtime: VirtualMachineRuntime, pub vm_runtime: VirtualMachineRuntime,
pub process_manager: ProcessManager, pub process_manager: ProcessManager,
pub task_manager: TaskManager, 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<u32, String>,
pub next_handle: u32,
} }
impl SystemOS { impl SystemOS {
pub fn new(cap_config: Option<CertificationConfig>) -> Self { pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self { let log_service = LogService::new(4096);
vm_runtime: VirtualMachineRuntime::new(cap_config), let mut os = Self {
vm_runtime: VirtualMachineRuntime::new_with_log_counter(
cap_config,
Arc::clone(&log_service.logs_count),
),
process_manager: ProcessManager::new(), process_manager: ProcessManager::new(),
task_manager: TaskManager::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<CrashReport> {
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<dyn FsBackend>) {
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<String>) -> TaskId { pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {

View File

@ -1,5 +1,4 @@
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod prometeu_hub; mod prometeu_hub;
mod window_manager;
pub use prometeu_hub::PrometeuHub; pub use prometeu_hub::PrometeuHub;

View File

@ -1,4 +1,3 @@
use crate::programs::prometeu_hub::window_manager::WindowManager;
use crate::{CrashReport, SystemOS}; use crate::{CrashReport, SystemOS};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
@ -7,9 +6,7 @@ use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_vm::VirtualMachine; use prometeu_vm::VirtualMachine;
/// PrometeuHub: Launcher and system UI environment. /// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub { pub struct PrometeuHub;
pub window_manager: WindowManager,
}
pub struct SystemProfileUpdate { pub struct SystemProfileUpdate {
pub crash: Option<CrashReport>, pub crash: Option<CrashReport>,
@ -24,7 +21,7 @@ impl Default for PrometeuHub {
impl PrometeuHub { impl PrometeuHub {
pub fn new() -> Self { pub fn new() -> Self {
Self { window_manager: WindowManager::new() } Self
} }
pub fn init(&mut self) { pub fn init(&mut self) {
@ -37,41 +34,41 @@ impl PrometeuHub {
let mut next_window = None; let mut next_window = None;
if hw.pad().a().pressed { 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(( next_window = Some((
"Green Window".to_string(), "Green Window".to_string(),
Rect { x: 0, y: 0, w: 160, h: 90 }, Rect { x: 0, y: 0, w: 160, h: 90 },
Color::GREEN, Color::GREEN,
)); ));
} else if hw.pad().b().pressed { } 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(( next_window = Some((
"Indigo Window".to_string(), "Indigo Window".to_string(),
Rect { x: 160, y: 0, w: 160, h: 90 }, Rect { x: 160, y: 0, w: 160, h: 90 },
Color::INDIGO, Color::INDIGO,
)); ));
} else if hw.pad().x().pressed { } 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(( next_window = Some((
"Yellow Window".to_string(), "Yellow Window".to_string(),
Rect { x: 0, y: 90, w: 160, h: 90 }, Rect { x: 0, y: 90, w: 160, h: 90 },
Color::YELLOW, Color::YELLOW,
)); ));
} else if hw.pad().y().pressed { } 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 = next_window =
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED)); Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
} }
if let Some((title, rect, color)) = next_window { if let Some((title, rect, color)) = next_window {
self.window_manager.remove_all_windows(); os.window_manager.remove_all_windows();
let id = self.window_manager.add_window(title, rect, color); let id = os.window_manager.add_window(title, rect, color);
self.window_manager.set_focus(id); os.window_manager.set_focus(id);
} }
} }
pub fn render(&mut self, _os: &mut SystemOS, hw: &mut dyn HardwareBridge) { pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) {
for window in &self.window_manager.windows { for window in &os.window_manager.windows {
hw.gfx_mut().fill_rect( hw.gfx_mut().fill_rect(
window.viewport.x, window.viewport.x,
window.viewport.y, window.viewport.y,
@ -93,17 +90,17 @@ impl PrometeuHub {
self.gui_update(os, hw); 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 { if hw.pad().start().down {
self.window_manager.remove_window(focused_id); os.window_manager.remove_window(focused_id);
} else { } else {
crash = os.vm_runtime.tick(vm, signals, hw); crash = os.tick_vm(vm, signals, hw);
} }
} }
self.render(os, hw); self.render(os, hw);
hw.gfx_mut().present(); 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() }
} }
} }

View File

@ -2,3 +2,5 @@ pub mod fs;
pub mod memcard; pub mod memcard;
pub mod process; pub mod process;
pub mod task; pub mod task;
pub mod vm_runtime;
pub mod window_manager;

View File

@ -1,5 +1,6 @@
use super::*; 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_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef}; use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
use prometeu_hal::cartridge::AppMode; use prometeu_hal::cartridge::AppMode;
@ -13,10 +14,40 @@ use prometeu_hal::{
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
expect_bool, expect_int, expect_bool, expect_int,
}; };
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::panic::{AssertUnwindSafe, catch_unwind}; use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::Ordering; 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<u32, String>,
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<dyn std::any::Any + Send>) -> String { fn panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<String>() { if let Some(message) = payload.downcast_ref::<String>() {
return message.clone(); return message.clone();
@ -40,8 +71,8 @@ impl VirtualMachineRuntime {
let app_id = self.current_app_id; let app_id = self.current_app_id;
let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0); let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0);
if count >= Self::MAX_LOGS_PER_FRAME { if count >= VirtualMachineRuntime::MAX_LOGS_PER_FRAME {
if count == Self::MAX_LOGS_PER_FRAME { if count == VirtualMachineRuntime::MAX_LOGS_PER_FRAME {
self.logs_written_this_frame.insert(app_id, count + 1); self.logs_written_this_frame.insert(app_id, count + 1);
self.log( self.log(
LogLevel::Warn, LogLevel::Warn,
@ -56,8 +87,8 @@ impl VirtualMachineRuntime {
self.logs_written_this_frame.insert(app_id, count + 1); self.logs_written_this_frame.insert(app_id, count + 1);
let mut final_msg = msg; let mut final_msg = msg;
if final_msg.len() > Self::MAX_LOG_LEN { if final_msg.len() > VirtualMachineRuntime::MAX_LOG_LEN {
final_msg.truncate(Self::MAX_LOG_LEN); final_msg.truncate(VirtualMachineRuntime::MAX_LOG_LEN);
} }
self.log(level, LogSource::App { app_id }, tag, final_msg); 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( fn syscall(
&mut self, &mut self,
id: SyscallId, id: SyscallId,
@ -395,13 +426,13 @@ impl NativeInterface for VirtualMachineRuntime {
} }
Syscall::FsOpen => { Syscall::FsOpen => {
let path = expect_string(args, 0, "path")?; let path = expect_string(args, 0, "path")?;
if self.fs_state != FsState::Mounted { if *self.fs_state != FsState::Mounted {
ret.push_int(-1); ret.push_int(-1);
return Ok(()); return Ok(());
} }
let handle = self.next_handle; let handle = *self.next_handle;
self.open_files.insert(handle, path); self.open_files.insert(handle, path);
self.next_handle += 1; *self.next_handle += 1;
ret.push_int(handle as i64); ret.push_int(handle as i64);
Ok(()) Ok(())
} }
@ -410,8 +441,9 @@ impl NativeInterface for VirtualMachineRuntime {
let path = self let path = self
.open_files .open_files
.get(&handle) .get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?
match self.fs.read_file(path) { .clone();
match self.fs.read_file(&path) {
Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()), Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()),
Err(_) => ret.push_null(), Err(_) => ret.push_null(),
} }
@ -423,8 +455,9 @@ impl NativeInterface for VirtualMachineRuntime {
let path = self let path = self
.open_files .open_files
.get(&handle) .get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?
match self.fs.write_file(path, &content) { .clone();
match self.fs.write_file(&path, &content) {
Ok(_) => ret.push_bool(true), Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false), Err(_) => ret.push_bool(false),
} }
@ -476,7 +509,7 @@ impl NativeInterface for VirtualMachineRuntime {
ret.push_int(0); ret.push_int(0);
return Ok(()); 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 { let status = if stat.state == MemcardSlotState::Corrupt {
MemcardStatus::Corrupt MemcardStatus::Corrupt
} else { } else {
@ -500,7 +533,7 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(()); return Ok(());
} }
let read = 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_int(read.status as i64);
ret.push_string(hex_encode(&read.bytes)); ret.push_string(hex_encode(&read.bytes));
ret.push_int(read.bytes_read as i64); ret.push_int(read.bytes_read as i64);
@ -516,8 +549,8 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(()); return Ok(());
} }
let payload = hex_decode(&payload_hex)?; let payload = hex_decode(&payload_hex)?;
let write = let app_id = self.current_app_id;
self.memcard.slot_write(&self.fs, self.current_app_id, slot, offset, &payload); let write = self.memcard.slot_write(self.fs, app_id, slot, offset, &payload);
ret.push_int(write.status as i64); ret.push_int(write.status as i64);
ret.push_int(write.bytes_written as i64); ret.push_int(write.bytes_written as i64);
Ok(()) Ok(())
@ -529,9 +562,10 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(()); return Ok(());
} }
let status = { let status = {
let app_id = self.current_app_id;
let memcard = &mut self.memcard; let memcard = &mut self.memcard;
let fs = &mut self.fs; 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); ret.push_int(status as i64);
Ok(()) Ok(())
@ -543,9 +577,10 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(()); return Ok(());
} }
let status = { let status = {
let app_id = self.current_app_id;
let memcard = &mut self.memcard; let memcard = &mut self.memcard;
let fs = &mut self.fs; 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); ret.push_int(status as i64);
Ok(()) Ok(())

View File

@ -1,26 +1,23 @@
use super::*; use super::*;
use crate::CrashReport; use crate::CrashReport;
use crate::fs::FsBackend; use crate::fs::{FsBackend, FsState, VirtualFS};
use prometeu_hal::cartridge::Cartridge; use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use std::collections::HashMap;
impl VirtualMachineRuntime { impl VirtualMachineRuntime {
pub fn new(cap_config: Option<CertificationConfig>) -> Self { pub fn new_with_log_counter(
cap_config: Option<CertificationConfig>,
logs_count: Arc<AtomicU32>,
) -> Self {
let boot_time = Instant::now(); let boot_time = Instant::now();
let log_service = LogService::new(4096); let atomic_telemetry = Arc::new(AtomicTelemetry::new(logs_count));
let atomic_telemetry = Arc::new(AtomicTelemetry::new(Arc::clone(&log_service.logs_count))); Self {
let mut os = Self {
tick_index: 0, tick_index: 0,
logical_frame_index: 0, logical_frame_index: 0,
logical_frame_active: false, logical_frame_active: false,
logical_frame_remaining_cycles: 0, logical_frame_remaining_cycles: 0,
last_frame_cpu_time_us: 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_app_id: 0,
current_cartridge_title: String::new(), current_cartridge_title: String::new(),
current_cartridge_app_version: String::new(), current_cartridge_app_version: String::new(),
@ -36,25 +33,41 @@ impl VirtualMachineRuntime {
frame_start_string_materializations: 0, frame_start_string_materializations: 0,
needs_prepare_entry_call: false, needs_prepare_entry_call: false,
boot_time, 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 ts_ms = self.boot_time.elapsed().as_millis() as u64;
let frame = self.logical_frame_index; 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<dyn FsBackend>) { pub fn mount_fs(
self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string()); &mut self,
match self.fs.mount(backend) { log_service: &mut LogService,
fs: &mut VirtualFS,
fs_state: &mut FsState,
backend: Box<dyn FsBackend>,
) {
self.log(
log_service,
LogLevel::Info,
LogSource::Fs,
0,
"Attempting to mount filesystem".to_string(),
);
match fs.mount(backend) {
Ok(_) => { Ok(_) => {
self.fs_state = FsState::Mounted; *fs_state = FsState::Mounted;
self.log( self.log(
log_service,
LogLevel::Info, LogLevel::Info,
LogSource::Fs, LogSource::Fs,
0, 0,
@ -63,26 +76,32 @@ impl VirtualMachineRuntime {
} }
Err(e) => { Err(e) => {
let err_msg = format!("Failed to mount filesystem: {:?}", e); let err_msg = format!("Failed to mount filesystem: {:?}", e);
self.log(LogLevel::Error, LogSource::Fs, 0, err_msg); self.log(log_service, LogLevel::Error, LogSource::Fs, 0, err_msg);
self.fs_state = FsState::Error(e); *fs_state = FsState::Error(e);
} }
} }
} }
pub fn unmount_fs(&mut self) { pub fn unmount_fs(&mut self, fs: &mut VirtualFS, fs_state: &mut FsState) {
self.fs.unmount(); fs.unmount();
self.fs_state = FsState::Unmounted; *fs_state = FsState::Unmounted;
} }
pub(crate) fn update_fs(&mut self) { pub(crate) fn update_fs(
if self.fs_state == FsState::Mounted && !self.fs.is_healthy() { &mut self,
log_service: &mut LogService,
fs: &mut VirtualFS,
fs_state: &mut FsState,
) {
if *fs_state == FsState::Mounted && !fs.is_healthy() {
self.log( self.log(
log_service,
LogLevel::Error, LogLevel::Error,
LogSource::Fs, LogSource::Fs,
0, 0,
"Filesystem became unhealthy, unmounting".to_string(), "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.last_frame_cpu_time_us = 0;
self.atomic_telemetry.reset(); self.atomic_telemetry.reset();
self.open_files.clear();
self.next_handle = 1;
self.memcard.clear_all_staging();
self.current_app_id = 0; self.current_app_id = 0;
self.current_cartridge_title.clear(); self.current_cartridge_title.clear();
self.current_cartridge_app_version.clear(); self.current_cartridge_app_version.clear();
@ -120,6 +135,7 @@ impl VirtualMachineRuntime {
pub fn initialize_vm( pub fn initialize_vm(
&mut self, &mut self,
log_service: &mut LogService,
vm: &mut VirtualMachine, vm: &mut VirtualMachine,
cartridge: &Cartridge, cartridge: &Cartridge,
) -> Result<(), CrashReport> { ) -> Result<(), CrashReport> {
@ -138,6 +154,7 @@ impl VirtualMachineRuntime {
let report = CrashReport::VmInit { error: e }; let report = CrashReport::VmInit { error: e };
self.last_crash_report = Some(report.clone()); self.last_crash_report = Some(report.clone());
self.log( self.log(
log_service,
LogLevel::Error, LogLevel::Error,
LogSource::Vm, LogSource::Vm,
report.log_tag(), report.log_tag(),

View File

@ -5,8 +5,6 @@ mod tests;
mod tick; mod tick;
use crate::CrashReport; use crate::CrashReport;
use crate::fs::{FsState, VirtualFS};
use crate::services::memcard::MemcardService;
use prometeu_bytecode::string_materialization_count; use prometeu_bytecode::string_materialization_count;
use prometeu_hal::cartridge::AppMode; use prometeu_hal::cartridge::AppMode;
use prometeu_hal::log::LogService; use prometeu_hal::log::LogService;
@ -14,6 +12,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
use prometeu_vm::VirtualMachine; use prometeu_vm::VirtualMachine;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicU32;
use std::time::Instant; use std::time::Instant;
pub struct VirtualMachineRuntime { pub struct VirtualMachineRuntime {
@ -22,12 +21,6 @@ pub struct VirtualMachineRuntime {
pub logical_frame_active: bool, pub logical_frame_active: bool,
pub logical_frame_remaining_cycles: u64, pub logical_frame_remaining_cycles: u64,
pub last_frame_cpu_time_us: u64, pub last_frame_cpu_time_us: u64,
pub fs: VirtualFS,
pub fs_state: FsState,
pub memcard: MemcardService,
pub open_files: HashMap<u32, String>,
pub next_handle: u32,
pub log_service: LogService,
pub current_app_id: u32, pub current_app_id: u32,
pub current_cartridge_title: String, pub current_cartridge_title: String,
pub current_cartridge_app_version: String, pub current_cartridge_app_version: String,
@ -50,4 +43,8 @@ impl VirtualMachineRuntime {
pub const SLICE_PER_TICK: u64 = 1_500_000; pub const SLICE_PER_TICK: u64 = 1_500_000;
pub const MAX_LOG_LEN: usize = 256; pub const MAX_LOG_LEN: usize = 256;
pub const MAX_LOGS_PER_FRAME: u32 = 10; pub const MAX_LOGS_PER_FRAME: u32 = 10;
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self::new_with_log_counter(cap_config, Arc::new(AtomicU32::new(0)))
}
} }

View File

@ -1,5 +1,6 @@
use super::*; 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::TRAP_TYPE;
use prometeu_bytecode::Value; use prometeu_bytecode::Value;
use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::assembler::assemble;
@ -188,6 +189,7 @@ mod fs_memcard;
#[test] #[test]
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
let mut runtime = VirtualMachineRuntime::new(None); let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module( 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 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!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
assert_eq!(runtime.current_app_id, 0); assert_eq!(runtime.current_app_id, 0);
@ -213,6 +215,7 @@ fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
#[test] #[test]
fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
let mut runtime = VirtualMachineRuntime::new(None); let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module( 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 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!(res.is_ok());
assert_eq!(runtime.current_app_id, 42); assert_eq!(runtime.current_app_id, 42);
@ -237,6 +240,12 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
#[test] #[test]
fn tick_returns_error_when_vm_ends_slice_with_trap() { fn tick_returns_error_when_vm_ends_slice_with_trap() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); 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 = let report = runtime
runtime.tick(&mut vm, &signals, &mut hardware).expect("trap must surface as runtime error"); .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 { match report {
CrashReport::VmTrap { trap } => { CrashReport::VmTrap { trap } => {
@ -271,6 +291,12 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
#[test] #[test]
fn tick_system_profile_rejects_gfx_game_surface() { fn tick_system_profile_rejects_gfx_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::Shell);
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("system gfx call must trap"); 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 { match report {
CrashReport::VmTrap { trap } => { CrashReport::VmTrap { trap } => {
@ -302,6 +340,12 @@ fn tick_system_profile_rejects_gfx_game_surface() {
#[test] #[test]
fn tick_system_profile_rejects_composer_game_surface() { fn tick_system_profile_rejects_composer_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::Shell);
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 = let report = runtime
runtime.tick(&mut vm, &signals, &mut hardware).expect("system composer call must trap"); .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 { match report {
CrashReport::VmTrap { trap } => { CrashReport::VmTrap { trap } => {
@ -336,6 +391,12 @@ fn tick_system_profile_rejects_composer_game_surface() {
#[test] #[test]
fn tick_system_profile_rejects_bank_game_surface() { fn tick_system_profile_rejects_bank_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program_and_mode(program, caps::BANK, AppMode::Shell);
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 = let report = runtime
runtime.tick(&mut vm, &signals, &mut hardware).expect("system bank call must trap"); .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 { match report {
CrashReport::VmTrap { trap } => { CrashReport::VmTrap { trap } => {
@ -368,6 +440,12 @@ fn tick_system_profile_rejects_bank_game_surface() {
#[test] #[test]
fn tick_system_profile_can_use_shared_log_transport() { fn tick_system_profile_can_use_shared_log_transport() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program_and_mode(program, caps::LOG, AppMode::Shell);
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); 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!(report.is_none(), "system log transport must remain internally shared");
assert!(vm.is_halted()); assert!(vm.is_halted());
@ -395,12 +483,28 @@ fn tick_system_profile_can_use_shared_log_transport() {
#[test] #[test]
fn tick_returns_panic_report_distinct_from_trap() { fn tick_returns_panic_report_distinct_from_trap() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]);
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); let signals = InputSignals::default();
let report = runtime 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"); .expect("panic must surface as runtime error");
match report { match report {
@ -416,6 +520,12 @@ fn tick_returns_panic_report_distinct_from_trap() {
#[test] #[test]
fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() { fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let signals = InputSignals::default(); let signals = InputSignals::default();
let program = let program =
@ -432,8 +542,18 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
glyph_slot_index.rebuild_from_slots(&slots); glyph_slot_index.rebuild_from_slots(&slots);
assert!(hardware.frame_composer.bind_scene(0)); assert!(hardware.frame_composer.bind_scene(0));
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); 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"); assert!(report.is_none(), "frame render path must not crash");
hardware.gfx.present(); hardware.gfx.present();
@ -443,6 +563,12 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
#[test] #[test]
fn tick_renders_scene_through_public_composer_syscalls() { fn tick_renders_scene_through_public_composer_syscalls() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let signals = InputSignals::default(); let signals = InputSignals::default();
let code = assemble( let code = assemble(
@ -491,8 +617,18 @@ fn tick_renders_scene_through_public_composer_syscalls() {
hardware.gfx.scene_fade_level = 31; hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31; hardware.gfx.hud_fade_level = 31;
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); 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!(report.is_none(), "public composer path must not crash");
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]); 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] #[test]
fn tick_draw_text_survives_scene_backed_frame_composition() { fn tick_draw_text_survives_scene_backed_frame_composition() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let signals = InputSignals::default(); let signals = InputSignals::default();
let code = assemble( let code = assemble(
@ -552,8 +694,18 @@ fn tick_draw_text_survives_scene_backed_frame_composition() {
hardware.gfx.scene_fade_level = 31; hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31; hardware.gfx.hud_fade_level = 31;
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); 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"); assert!(report.is_none(), "scene-backed overlay text must not crash");
hardware.gfx.present(); hardware.gfx.present();
@ -563,6 +715,12 @@ fn tick_draw_text_survives_scene_backed_frame_composition() {
#[test] #[test]
fn tick_draw_text_survives_no_scene_frame_path() { fn tick_draw_text_survives_no_scene_frame_path() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let signals = InputSignals::default(); let signals = InputSignals::default();
let code = assemble( let code = assemble(
@ -585,8 +743,18 @@ fn tick_draw_text_survives_no_scene_frame_path() {
hardware.gfx.scene_fade_level = 31; hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31; hardware.gfx.hud_fade_level = 31;
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); 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"); assert!(report.is_none(), "no-scene overlay text must not crash");
hardware.gfx.present(); hardware.gfx.present();
@ -596,6 +764,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
#[test] #[test]
fn initialize_vm_success_clears_previous_crash_report() { fn initialize_vm_success_clears_previous_crash_report() {
let mut runtime = VirtualMachineRuntime::new(None); 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) }); runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) });
let mut vm = VirtualMachine::default(); 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); 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()); 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() { fn reset_clears_cartridge_scoped_runtime_state() {
let mut runtime = VirtualMachineRuntime::new(None); let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let fs_state = FsState::Mounted;
let mut open_files: HashMap<u32, String> = HashMap::new();
let next_handle = 9;
runtime.tick_index = 77; runtime.tick_index = 77;
runtime.logical_frame_index = 12; runtime.logical_frame_index = 12;
runtime.logical_frame_active = true; runtime.logical_frame_active = true;
runtime.logical_frame_remaining_cycles = 345; runtime.logical_frame_remaining_cycles = 345;
runtime.last_frame_cpu_time_us = 678; runtime.last_frame_cpu_time_us = 678;
runtime.fs_state = FsState::Mounted; open_files.insert(7, "/save.dat".into());
runtime.open_files.insert(7, "/save.dat".into());
runtime.next_handle = 9;
runtime.current_app_id = 42; runtime.current_app_id = 42;
runtime.current_cartridge_title = "Cart".into(); runtime.current_cartridge_title = "Cart".into();
runtime.current_cartridge_app_version = "1.2.3".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); runtime.reset(&mut vm);
assert_eq!(runtime.tick_index, 77); 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_eq!(runtime.logical_frame_index, 0);
assert!(!runtime.logical_frame_active); assert!(!runtime.logical_frame_active);
assert_eq!(runtime.logical_frame_remaining_cycles, 0); assert_eq!(runtime.logical_frame_remaining_cycles, 0);
assert_eq!(runtime.last_frame_cpu_time_us, 0); assert_eq!(runtime.last_frame_cpu_time_us, 0);
assert!(runtime.open_files.is_empty()); assert_eq!(open_files.len(), 1);
assert_eq!(runtime.next_handle, 1); assert_eq!(next_handle, 9);
assert_eq!(runtime.current_app_id, 0); assert_eq!(runtime.current_app_id, 0);
assert!(runtime.current_cartridge_title.is_empty()); assert!(runtime.current_cartridge_title.is_empty());
assert!(runtime.current_cartridge_app_version.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty());
@ -679,6 +849,12 @@ fn reset_clears_cartridge_scoped_runtime_state() {
#[test] #[test]
fn tick_numeric_happy_path_records_zero_internal_allocations() { fn tick_numeric_happy_path_records_zero_internal_allocations() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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 program = serialized_single_function_module(code, vec![]);
let cartridge = cartridge_with_program(program, caps::NONE); let cartridge = cartridge_with_program(program, caps::NONE);
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); 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()); assert!(report.is_none());
let snapshot = runtime.atomic_telemetry.snapshot(); let snapshot = runtime.atomic_telemetry.snapshot();
@ -699,6 +885,12 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() {
#[test] #[test]
fn tick_already_materialized_string_path_records_zero_internal_allocations() { fn tick_already_materialized_string_path_records_zero_internal_allocations() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::NONE);
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); 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()); assert!(report.is_none());
let snapshot = runtime.atomic_telemetry.snapshot(); let snapshot = runtime.atomic_telemetry.snapshot();
@ -723,6 +925,8 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() {
#[test] #[test]
fn initialize_vm_failure_clears_previous_identity_and_handles() { fn initialize_vm_failure_clears_previous_identity_and_handles() {
let mut runtime = VirtualMachineRuntime::new(None); let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let good_program = serialized_single_function_module( 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); 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()); open_files.insert(5, "/save.dat".into());
runtime.next_handle = 6;
let next_handle = 6;
runtime.paused = true; runtime.paused = true;
runtime.debug_step_request = true; runtime.debug_step_request = true;
runtime.atomic_telemetry.cycles_used.store(123, Ordering::Relaxed); 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 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!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
assert_eq!(runtime.current_app_id, 0); assert_eq!(runtime.current_app_id, 0);
assert!(runtime.current_cartridge_title.is_empty()); assert!(runtime.current_cartridge_title.is_empty());
assert!(runtime.current_cartridge_app_version.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty());
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
assert!(runtime.open_files.is_empty()); assert_eq!(open_files.len(), 1);
assert_eq!(runtime.next_handle, 1); assert_eq!(next_handle, 6);
assert!(!runtime.paused); assert!(!runtime.paused);
assert!(!runtime.debug_step_request); assert!(!runtime.debug_step_request);
assert_eq!(runtime.atomic_telemetry.cycles_used.load(Ordering::Relaxed), 0); 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] #[test]
fn tick_composer_bind_scene_operational_error_returns_status_not_crash() { fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); 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); 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!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!( assert_eq!(
@ -803,6 +1026,12 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
#[test] #[test]
fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); 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); 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!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]); 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] #[test]
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); 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); 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!(report.is_none(), "invalid layer must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]); 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] #[test]
fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() { fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); let signals = InputSignals::default();
@ -887,8 +1148,18 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
AssetsPayloadSource::from_bytes(asset_data), AssetsPayloadSource::from_bytes(asset_data),
); );
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); 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!(report.is_none(), "invalid composer parameter range must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!( assert_eq!(
@ -900,6 +1171,12 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
#[test] #[test]
fn tick_audio_play_sample_operational_error_returns_status_not_crash() { fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::AUDIO);
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); 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!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]); 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] #[test]
fn tick_audio_play_voice_invalid_returns_status_not_crash() { fn tick_audio_play_voice_invalid_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::AUDIO);
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); 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!(report.is_none(), "invalid voice must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]); 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] #[test]
fn tick_audio_play_missing_asset_returns_status_not_crash() { fn tick_audio_play_missing_asset_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::AUDIO);
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); 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!(report.is_none(), "missing audio asset must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::BankInvalid as i64)]); 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] #[test]
fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::AUDIO);
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 = let report = runtime
runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); .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 { match report {
CrashReport::VmTrap { trap } => { CrashReport::VmTrap { trap } => {
assert_eq!(trap.code, TRAP_TYPE); assert_eq!(trap.code, TRAP_TYPE);
@ -1022,6 +1358,12 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
#[test] #[test]
fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() { fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); 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 = let report = runtime
runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); .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 { match report {
CrashReport::VmTrap { trap } => { CrashReport::VmTrap { trap } => {
assert_eq!(trap.code, TRAP_TYPE); assert_eq!(trap.code, TRAP_TYPE);

View File

@ -3,6 +3,12 @@ use super::*;
#[test] #[test]
fn tick_asset_commit_operational_error_returns_status_not_crash() { fn tick_asset_commit_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::ASSET);
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); 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!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); 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] #[test]
fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::ASSET);
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); 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!(report.is_none(), "missing asset must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!( assert_eq!(
@ -58,6 +90,12 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
#[test] #[test]
fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::ASSET);
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); 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!(report.is_none(), "invalid slot must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!( assert_eq!(
@ -93,6 +141,12 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
#[test] #[test]
fn tick_asset_status_unknown_handle_returns_status_not_crash() { fn tick_asset_status_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::ASSET);
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); 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!(report.is_none(), "unknown asset handle must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]); 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] #[test]
fn tick_bank_info_returns_slot_summary_not_json() { fn tick_bank_info_returns_slot_summary_not_json() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::BANK);
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); 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!(report.is_none(), "bank summary must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(16), Value::Int64(1)]); 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] #[test]
fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() { fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
let mut runtime = VirtualMachineRuntime::new(None); let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module( 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 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(_) }))); assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
} }
@ -173,6 +254,12 @@ fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
#[test] #[test]
fn tick_asset_commit_invalid_transition_returns_status_not_crash() { fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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"); 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");
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(), "invalid transition must not crash"); assert!(report.is_none(), "invalid transition must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(handle, 1); assert_eq!(handle, 1);
@ -218,6 +315,12 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
#[test] #[test]
fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::ASSET);
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); 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!(report.is_none(), "unknown handle cancel must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); 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] #[test]
fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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"); 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 { loop {
match hardware.assets.status(handle) { match hardware.assets.status(handle) {
@ -284,7 +403,17 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
hardware.assets.apply_commits(); hardware.assets.apply_commits();
assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED); 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!(report.is_none(), "cancel after commit must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]); 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] #[test]
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() { fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); 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); let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET);
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); 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!(report.is_none(), "mixed status-first surface must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!( assert_eq!(

View File

@ -3,7 +3,13 @@ use super::*;
#[test] #[test]
fn tick_memcard_slot_roundtrip_for_game_profile() { fn tick_memcard_slot_roundtrip_for_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = 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 vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); let signals = InputSignals::default();
@ -50,8 +56,18 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
preload: vec![], preload: vec![],
}; };
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); 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!(report.is_none(), "memcard roundtrip must not crash");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!( assert_eq!(
@ -63,6 +79,12 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
#[test] #[test]
fn tick_memcard_access_is_denied_for_non_game_profile() { fn tick_memcard_access_is_denied_for_non_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None); 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<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new(); let mut hardware = Hardware::new();
let signals = InputSignals::default(); let signals = InputSignals::default();
@ -89,8 +111,18 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
preload: vec![], preload: vec![],
}; };
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); 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!(report.is_none(), "non-game memcard call must return status");
assert!(vm.is_halted()); assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]); assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]);

View File

@ -1,9 +1,13 @@
use super::dispatch::VmRuntimeHost;
use super::*; use super::*;
use crate::CrashReport; use crate::CrashReport;
use crate::fs::{FsState, VirtualFS};
use crate::services::memcard::MemcardService;
use prometeu_hal::asset::{BankTelemetry, BankType}; 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_hal::{HardwareBridge, HostContext, InputSignals};
use prometeu_vm::LogicalFrameEndingReason; use prometeu_vm::LogicalFrameEndingReason;
use std::collections::HashMap;
use std::panic::{AssertUnwindSafe, catch_unwind}; use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -52,11 +56,30 @@ impl VirtualMachineRuntime {
pub fn debug_step_instruction( pub fn debug_step_instruction(
&mut self, &mut self,
log_service: &mut LogService,
vm: &mut VirtualMachine, vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge, hw: &mut dyn HardwareBridge,
) -> Option<CrashReport> { ) -> Option<CrashReport> {
let mut ctx = HostContext::new(Some(hw)); let step_result = {
match vm.step(self, &mut ctx) { 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, Ok(_) => None,
Err(e) => { Err(e) => {
let report = match e { let report = match e {
@ -70,6 +93,7 @@ impl VirtualMachineRuntime {
}, },
}; };
self.log( self.log(
log_service,
LogLevel::Error, LogLevel::Error,
LogSource::Vm, LogSource::Vm,
report.log_tag(), report.log_tag(),
@ -83,6 +107,12 @@ impl VirtualMachineRuntime {
pub fn tick( pub fn tick(
&mut self, &mut self,
log_service: &mut LogService,
fs: &mut VirtualFS,
fs_state: &mut FsState,
mem_card: &mut MemcardService,
open_files: &mut HashMap<u32, String>,
next_handle: &mut u32,
vm: &mut VirtualMachine, vm: &mut VirtualMachine,
signals: &InputSignals, signals: &InputSignals,
hw: &mut dyn HardwareBridge, hw: &mut dyn HardwareBridge,
@ -94,7 +124,7 @@ impl VirtualMachineRuntime {
return None; return None;
} }
self.update_fs(); self.update_fs(log_service, fs, fs_state);
if !self.logical_frame_active { if !self.logical_frame_active {
self.logical_frame_active = true; self.logical_frame_active = true;
@ -128,7 +158,16 @@ impl VirtualMachineRuntime {
if budget > 0 { if budget > 0 {
let run_result = { let run_result = {
let mut ctx = HostContext::new(Some(hw)); 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 { match run_result {
@ -143,6 +182,7 @@ impl VirtualMachineRuntime {
self.paused = true; self.paused = true;
self.debug_step_request = false; self.debug_step_request = false;
self.log( self.log(
log_service,
LogLevel::Info, LogLevel::Info,
LogSource::Vm, LogSource::Vm,
0xDEB1, 0xDEB1,
@ -154,6 +194,7 @@ impl VirtualMachineRuntime {
let report = let report =
CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) }; CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) };
self.log( self.log(
log_service,
LogLevel::Error, LogLevel::Error,
LogSource::Vm, LogSource::Vm,
report.log_tag(), report.log_tag(),
@ -166,6 +207,7 @@ impl VirtualMachineRuntime {
if let LogicalFrameEndingReason::Trap(trap) = &run.reason { if let LogicalFrameEndingReason::Trap(trap) = &run.reason {
let report = CrashReport::VmTrap { trap: trap.clone() }; let report = CrashReport::VmTrap { trap: trap.clone() };
self.log( self.log(
log_service,
LogLevel::Error, LogLevel::Error,
LogSource::Vm, LogSource::Vm,
report.log_tag(), report.log_tag(),
@ -182,6 +224,7 @@ impl VirtualMachineRuntime {
let message = Self::host_panic_payload_to_string(payload); let message = Self::host_panic_payload_to_string(payload);
let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }; let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) };
self.log( self.log(
log_service,
LogLevel::Error, LogLevel::Error,
LogSource::Vm, LogSource::Vm,
report.log_tag(), report.log_tag(),
@ -229,18 +272,15 @@ impl VirtualMachineRuntime {
let ts_ms = self.boot_time.elapsed().as_millis() as u64; let ts_ms = self.boot_time.elapsed().as_millis() as u64;
let telemetry_snapshot = self.atomic_telemetry.snapshot(); let telemetry_snapshot = self.atomic_telemetry.snapshot();
let violations = self.certifier.evaluate( let violations =
&telemetry_snapshot, self.certifier.evaluate(&telemetry_snapshot, log_service, ts_ms) as u32;
&mut self.log_service,
ts_ms,
) as u32;
self.atomic_telemetry.violations.store(violations, Ordering::Relaxed); self.atomic_telemetry.violations.store(violations, Ordering::Relaxed);
self.atomic_telemetry self.atomic_telemetry
.completed_logical_frames .completed_logical_frames
.fetch_add(1, Ordering::Relaxed); .fetch_add(1, Ordering::Relaxed);
self.log_service.reset_count(); log_service.reset_count();
self.logical_frame_index += 1; self.logical_frame_index += 1;
self.logical_frame_active = false; self.logical_frame_active = false;
@ -258,7 +298,13 @@ impl VirtualMachineRuntime {
} }
Err(e) => { Err(e) => {
let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) }; 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()); self.last_crash_report = Some(report.clone());
return Some(report); return Some(report);
} }

View File

@ -58,7 +58,11 @@ impl HostDebugger {
// Pre-load cartridge metadata so the Handshake message can contain // Pre-load cartridge metadata so the Handshake message can contain
// valid information about the App being debugged. // valid information about the App being debugged.
if let Ok(cartridge) = CartridgeLoader::load(path) { 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)) { match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
@ -201,7 +205,11 @@ impl HostDebugger {
DebugCommand::Step => { DebugCommand::Step => {
// Execute exactly one instruction and keep paused. // Execute exactly one instruction and keep paused.
firmware.os.vm_runtime.paused = true; 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 => { DebugCommand::StepFrame => {
// Execute until the end of the current logical frame. // Execute until the end of the current logical frame.
@ -308,7 +316,7 @@ impl HostDebugger {
} }
// 1. Process and send new log entries. // 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 { for event in new_events {
self.last_log_seq = event.seq; self.last_log_seq = event.seq;

View File

@ -59,7 +59,7 @@ struct FrameCanvas<'a> {
pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot { pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot {
let tel = firmware.os.vm_runtime.atomic_telemetry.snapshot(); 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 = let violations_count =
recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count(); recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count();

View File

@ -153,7 +153,7 @@ impl HostRunner {
let mut firmware = Firmware::new(cap_config); let mut firmware = Firmware::new(cap_config);
if let Some(root) = &fs_root { if let Some(root) = &fs_root {
let backend = HostDirBackend::new(root); let backend = HostDirBackend::new(root);
firmware.os.vm_runtime.mount_fs(Box::new(backend)); firmware.os.mount_fs(Box::new(backend));
} }
Self { Self {
@ -333,13 +333,11 @@ impl ApplicationHandler for HostRunner {
// 2. Maintain filesystem connection if it was lost (e.g., directory removed). // 2. Maintain filesystem connection if it was lost (e.g., directory removed).
if let Some(root) = &self.fs_root { if let Some(root) = &self.fs_root {
use prometeu_system::fs::FsState; use prometeu_system::fs::FsState;
if matches!( if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_))
self.firmware.os.vm_runtime.fs_state, && std::path::Path::new(root).exists()
FsState::Unmounted | FsState::Error(_)
) && std::path::Path::new(root).exists()
{ {
let backend = HostDirBackend::new(root); 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. // Synchronize system logs to the host console.
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX); let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
let new_events = if last_seq == 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 { } 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); self.log_sink.process_events(new_events);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,33 @@
{ {
"version": 1, "version": 1,
"domains": { "domains": {
"system/runtime": { "system/os": {
"baseline_percent": 68, "baseline_percent": 78,
"paths": [ "paths": [
"crates/console/prometeu-system/src/virtual_machine_runtime.rs", "crates/console/prometeu-system/src/os/",
"crates/console/prometeu-system/src/virtual_machine_runtime/" "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, "baseline_percent": 77,
"paths": [ "paths": [
"crates/console/prometeu-system/src/services/fs/", "crates/console/prometeu-system/src/services/fs/",
"crates/console/prometeu-system/src/services/memcard.rs" "crates/console/prometeu-system/src/services/memcard.rs"
] ]
}, },
"system/programs": {
"baseline_percent": 55,
"paths": [
"crates/console/prometeu-system/src/programs/"
]
},
"asset/bank": { "asset/bank": {
"baseline_percent": 86, "baseline_percent": 86,
"paths": [ "paths": [