implements PLN-0053

This commit is contained in:
bQUARKz 2026-05-14 18:31:58 +01:00
parent ab825d3274
commit d1f4547e44
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
19 changed files with 295 additions and 163 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ impl LoadCartridgeStep {
}
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log(
ctx.os.log(
LogLevel::Info,
LogSource::Pos,
0,
@ -34,7 +34,7 @@ impl LoadCartridgeStep {
self.cartridge.assets.clone(),
);
self.init_error = ctx.os.vm_runtime.initialize_vm(ctx.vm, &self.cartridge).err();
self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err();
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {

View File

@ -8,7 +8,7 @@ pub struct ResetStep;
impl ResetStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.vm_runtime.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
ctx.os.vm_runtime.reset(ctx.vm);
}

View File

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

View File

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

View File

@ -1,25 +1,62 @@
use crate::CrashReport;
use crate::VirtualMachineRuntime;
use crate::process::ProcessManager;
use crate::services::window_manager::WindowManager;
use crate::task::TaskId;
use crate::task::TaskManager;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_vm::VirtualMachine;
use std::sync::Arc;
pub struct SystemOS {
pub vm_runtime: VirtualMachineRuntime,
pub process_manager: ProcessManager,
pub task_manager: TaskManager,
pub window_manager: WindowManager,
pub log_service: LogService,
}
impl SystemOS {
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self {
vm_runtime: VirtualMachineRuntime::new(cap_config),
let log_service = LogService::new(4096);
let mut os = Self {
vm_runtime: VirtualMachineRuntime::new_with_log_counter(
cap_config,
Arc::clone(&log_service.logs_count),
),
process_manager: ProcessManager::new(),
task_manager: TaskManager::new(),
window_manager: WindowManager::new(),
}
log_service,
};
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.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, vm, signals, hw)
}
pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {

View File

@ -34,28 +34,28 @@ impl PrometeuHub {
let mut next_window = None;
if hw.pad().a().pressed {
os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
next_window = Some((
"Green Window".to_string(),
Rect { x: 0, y: 0, w: 160, h: 90 },
Color::GREEN,
));
} else if hw.pad().b().pressed {
os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
next_window = Some((
"Indigo Window".to_string(),
Rect { x: 160, y: 0, w: 160, h: 90 },
Color::INDIGO,
));
} else if hw.pad().x().pressed {
os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
next_window = Some((
"Yellow Window".to_string(),
Rect { x: 0, y: 90, w: 160, h: 90 },
Color::YELLOW,
));
} else if hw.pad().y().pressed {
os.vm_runtime.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
next_window =
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
}
@ -94,7 +94,7 @@ impl PrometeuHub {
if hw.pad().start().down {
os.window_manager.remove_window(focused_id);
} else {
crash = os.vm_runtime.tick(vm, signals, hw);
crash = os.tick_vm(vm, signals, hw);
}
}

View File

@ -13,10 +13,34 @@ use prometeu_hal::{
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
expect_bool, expect_int,
};
use std::ops::{Deref, DerefMut};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::Ordering;
impl VirtualMachineRuntime {
pub(crate) struct VmRuntimeHost<'a> {
pub runtime: &'a mut VirtualMachineRuntime,
pub log_service: &'a mut LogService,
}
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 {
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
@ -40,8 +64,8 @@ impl VirtualMachineRuntime {
let app_id = self.current_app_id;
let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0);
if count >= Self::MAX_LOGS_PER_FRAME {
if count == Self::MAX_LOGS_PER_FRAME {
if count >= VirtualMachineRuntime::MAX_LOGS_PER_FRAME {
if count == VirtualMachineRuntime::MAX_LOGS_PER_FRAME {
self.logs_written_this_frame.insert(app_id, count + 1);
self.log(
LogLevel::Warn,
@ -56,8 +80,8 @@ impl VirtualMachineRuntime {
self.logs_written_this_frame.insert(app_id, count + 1);
let mut final_msg = msg;
if final_msg.len() > Self::MAX_LOG_LEN {
final_msg.truncate(Self::MAX_LOG_LEN);
if final_msg.len() > VirtualMachineRuntime::MAX_LOG_LEN {
final_msg.truncate(VirtualMachineRuntime::MAX_LOG_LEN);
}
self.log(level, LogSource::App { app_id }, tag, final_msg);
@ -122,7 +146,7 @@ impl VirtualMachineRuntime {
}
}
impl NativeInterface for VirtualMachineRuntime {
impl NativeInterface for VmRuntimeHost<'_> {
fn syscall(
&mut self,
id: SyscallId,
@ -410,8 +434,9 @@ impl NativeInterface for VirtualMachineRuntime {
let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.read_file(path) {
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?
.clone();
match self.fs.read_file(&path) {
Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()),
Err(_) => ret.push_null(),
}
@ -423,8 +448,9 @@ impl NativeInterface for VirtualMachineRuntime {
let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.write_file(path, &content) {
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?
.clone();
match self.fs.write_file(&path, &content) {
Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false),
}
@ -516,8 +542,14 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(());
}
let payload = hex_decode(&payload_hex)?;
let write =
self.memcard.slot_write(&self.fs, self.current_app_id, slot, offset, &payload);
let runtime = &mut self.runtime;
let write = runtime.memcard.slot_write(
&runtime.fs,
runtime.current_app_id,
slot,
offset,
&payload,
);
ret.push_int(write.status as i64);
ret.push_int(write.bytes_written as i64);
Ok(())
@ -529,9 +561,10 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(());
}
let status = {
let memcard = &mut self.memcard;
let fs = &mut self.fs;
memcard.slot_commit(fs, self.current_app_id, slot)
let runtime = &mut self.runtime;
let memcard = &mut runtime.memcard;
let fs = &mut runtime.fs;
memcard.slot_commit(fs, runtime.current_app_id, slot)
};
ret.push_int(status as i64);
Ok(())
@ -543,9 +576,10 @@ impl NativeInterface for VirtualMachineRuntime {
return Ok(());
}
let status = {
let memcard = &mut self.memcard;
let fs = &mut self.fs;
memcard.slot_clear(fs, self.current_app_id, slot)
let runtime = &mut self.runtime;
let memcard = &mut runtime.memcard;
let fs = &mut runtime.fs;
memcard.slot_clear(fs, runtime.current_app_id, slot)
};
ret.push_int(status as i64);
Ok(())

View File

@ -5,11 +5,13 @@ use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::{LogLevel, LogSource};
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 log_service = LogService::new(4096);
let atomic_telemetry = Arc::new(AtomicTelemetry::new(Arc::clone(&log_service.logs_count)));
let mut os = Self {
let atomic_telemetry = Arc::new(AtomicTelemetry::new(logs_count));
Self {
tick_index: 0,
logical_frame_index: 0,
logical_frame_active: false,
@ -20,7 +22,6 @@ impl VirtualMachineRuntime {
memcard: MemcardService::new(),
open_files: HashMap::new(),
next_handle: 1,
log_service,
current_app_id: 0,
current_cartridge_title: String::new(),
current_cartridge_app_version: String::new(),
@ -36,25 +37,35 @@ impl VirtualMachineRuntime {
frame_start_string_materializations: 0,
needs_prepare_entry_call: false,
boot_time,
};
os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string());
os
}
}
pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) {
pub fn log(
&self,
log_service: &mut LogService,
level: LogLevel,
source: LogSource,
tag: u16,
msg: String,
) {
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
let frame = self.logical_frame_index;
self.log_service.log(ts_ms, frame, level, source, tag, msg);
log_service.log(ts_ms, frame, level, source, tag, msg);
}
pub fn mount_fs(&mut self, backend: Box<dyn FsBackend>) {
self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string());
pub fn mount_fs(&mut self, log_service: &mut LogService, backend: Box<dyn FsBackend>) {
self.log(
log_service,
LogLevel::Info,
LogSource::Fs,
0,
"Attempting to mount filesystem".to_string(),
);
match self.fs.mount(backend) {
Ok(_) => {
self.fs_state = FsState::Mounted;
self.log(
log_service,
LogLevel::Info,
LogSource::Fs,
0,
@ -63,7 +74,7 @@ impl VirtualMachineRuntime {
}
Err(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);
}
}
@ -74,9 +85,10 @@ impl VirtualMachineRuntime {
self.fs_state = FsState::Unmounted;
}
pub(crate) fn update_fs(&mut self) {
pub(crate) fn update_fs(&mut self, log_service: &mut LogService) {
if self.fs_state == FsState::Mounted && !self.fs.is_healthy() {
self.log(
log_service,
LogLevel::Error,
LogSource::Fs,
0,
@ -120,6 +132,7 @@ impl VirtualMachineRuntime {
pub fn initialize_vm(
&mut self,
log_service: &mut LogService,
vm: &mut VirtualMachine,
cartridge: &Cartridge,
) -> Result<(), CrashReport> {
@ -138,6 +151,7 @@ impl VirtualMachineRuntime {
let report = CrashReport::VmInit { error: e };
self.last_crash_report = Some(report.clone());
self.log(
log_service,
LogLevel::Error,
LogSource::Vm,
report.log_tag(),

View File

@ -14,6 +14,7 @@ use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
use prometeu_vm::VirtualMachine;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
use std::time::Instant;
pub struct VirtualMachineRuntime {
@ -27,7 +28,6 @@ pub struct VirtualMachineRuntime {
pub memcard: MemcardService,
pub open_files: HashMap<u32, String>,
pub next_handle: u32,
pub log_service: LogService,
pub current_app_id: u32,
pub current_cartridge_title: String,
pub current_cartridge_app_version: String,
@ -50,4 +50,8 @@ impl VirtualMachineRuntime {
pub const SLICE_PER_TICK: u64 = 1_500_000;
pub const MAX_LOG_LEN: usize = 256;
pub const MAX_LOGS_PER_FRAME: u32 = 10;
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self::new_with_log_counter(cap_config, Arc::new(AtomicU32::new(0)))
}
}

View File

@ -188,6 +188,7 @@ mod fs_memcard;
#[test]
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -202,7 +203,7 @@ fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
);
let cartridge = cartridge_with_program(program, caps::NONE);
let res = runtime.initialize_vm(&mut vm, &cartridge);
let res = runtime.initialize_vm(&mut log_service, &mut vm, &cartridge);
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
assert_eq!(runtime.current_app_id, 0);
@ -213,6 +214,7 @@ fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
#[test]
fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -227,7 +229,7 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
let res = runtime.initialize_vm(&mut vm, &cartridge);
let res = runtime.initialize_vm(&mut log_service, &mut vm, &cartridge);
assert!(res.is_ok());
assert_eq!(runtime.current_app_id, 42);
@ -237,6 +239,7 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
#[test]
fn tick_returns_error_when_vm_ends_slice_with_trap() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -253,10 +256,11 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report =
runtime.tick(&mut vm, &signals, &mut hardware).expect("trap must surface as runtime error");
let report = runtime
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("trap must surface as runtime error");
match report {
CrashReport::VmTrap { trap } => {
@ -271,6 +275,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
#[test]
fn tick_system_profile_rejects_gfx_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -287,8 +292,10 @@ fn tick_system_profile_rejects_gfx_game_surface() {
);
let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::Shell);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware).expect("system gfx call must trap");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("system gfx call must trap");
match report {
CrashReport::VmTrap { trap } => {
@ -302,6 +309,7 @@ fn tick_system_profile_rejects_gfx_game_surface() {
#[test]
fn tick_system_profile_rejects_composer_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -318,9 +326,10 @@ fn tick_system_profile_rejects_composer_game_surface() {
);
let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::Shell);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report =
runtime.tick(&mut vm, &signals, &mut hardware).expect("system composer call must trap");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("system composer call must trap");
match report {
CrashReport::VmTrap { trap } => {
@ -336,6 +345,7 @@ fn tick_system_profile_rejects_composer_game_surface() {
#[test]
fn tick_system_profile_rejects_bank_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -352,9 +362,10 @@ fn tick_system_profile_rejects_bank_game_surface() {
);
let cartridge = cartridge_with_program_and_mode(program, caps::BANK, AppMode::Shell);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report =
runtime.tick(&mut vm, &signals, &mut hardware).expect("system bank call must trap");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("system bank call must trap");
match report {
CrashReport::VmTrap { trap } => {
@ -368,6 +379,7 @@ fn tick_system_profile_rejects_bank_game_surface() {
#[test]
fn tick_system_profile_can_use_shared_log_transport() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -385,8 +397,8 @@ fn tick_system_profile_can_use_shared_log_transport() {
);
let cartridge = cartridge_with_program_and_mode(program, caps::LOG, AppMode::Shell);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "system log transport must remain internally shared");
assert!(vm.is_halted());
@ -395,12 +407,13 @@ fn tick_system_profile_can_use_shared_log_transport() {
#[test]
fn tick_returns_panic_report_distinct_from_trap() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]);
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let report = runtime
.tick(&mut vm, &signals, &mut hardware)
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("panic must surface as runtime error");
match report {
@ -416,6 +429,7 @@ fn tick_returns_panic_report_distinct_from_trap() {
#[test]
fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let program =
@ -432,8 +446,8 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
glyph_slot_index.rebuild_from_slots(&slots);
assert!(hardware.frame_composer.bind_scene(0));
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "frame render path must not crash");
hardware.gfx.present();
@ -443,6 +457,7 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
#[test]
fn tick_renders_scene_through_public_composer_syscalls() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let code = assemble(
@ -491,8 +506,8 @@ fn tick_renders_scene_through_public_composer_syscalls() {
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "public composer path must not crash");
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]);
@ -503,6 +518,7 @@ fn tick_renders_scene_through_public_composer_syscalls() {
#[test]
fn tick_draw_text_survives_scene_backed_frame_composition() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let code = assemble(
@ -552,8 +568,8 @@ fn tick_draw_text_survives_scene_backed_frame_composition() {
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "scene-backed overlay text must not crash");
hardware.gfx.present();
@ -563,6 +579,7 @@ fn tick_draw_text_survives_scene_backed_frame_composition() {
#[test]
fn tick_draw_text_survives_no_scene_frame_path() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let code = assemble(
@ -585,8 +602,8 @@ fn tick_draw_text_survives_no_scene_frame_path() {
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "no-scene overlay text must not crash");
hardware.gfx.present();
@ -596,6 +613,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
#[test]
fn initialize_vm_success_clears_previous_crash_report() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) });
let mut vm = VirtualMachine::default();
@ -612,7 +630,7 @@ fn initialize_vm_success_clears_previous_crash_report() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
assert!(runtime.last_crash_report.is_none());
}
@ -679,6 +697,7 @@ fn reset_clears_cartridge_scoped_runtime_state() {
#[test]
fn tick_numeric_happy_path_records_zero_internal_allocations() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -687,8 +706,8 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() {
let program = serialized_single_function_module(code, vec![]);
let cartridge = cartridge_with_program(program, caps::NONE);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none());
let snapshot = runtime.atomic_telemetry.snapshot();
@ -699,6 +718,7 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() {
#[test]
fn tick_already_materialized_string_path_records_zero_internal_allocations() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -711,8 +731,8 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() {
);
let cartridge = cartridge_with_program(program, caps::NONE);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none());
let snapshot = runtime.atomic_telemetry.snapshot();
@ -723,6 +743,7 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() {
#[test]
fn initialize_vm_failure_clears_previous_identity_and_handles() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let good_program = serialized_single_function_module(
@ -736,7 +757,9 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
}],
);
let good_cartridge = cartridge_with_program(good_program, caps::GFX);
runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize");
runtime
.initialize_vm(&mut log_service, &mut vm, &good_cartridge)
.expect("runtime must initialize");
runtime.open_files.insert(5, "/save.dat".into());
runtime.next_handle = 6;
@ -756,7 +779,7 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
);
let bad_cartridge = cartridge_with_program(bad_program, caps::NONE);
let res = runtime.initialize_vm(&mut vm, &bad_cartridge);
let res = runtime.initialize_vm(&mut log_service, &mut vm, &bad_cartridge);
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
assert_eq!(runtime.current_app_id, 0);
@ -774,6 +797,7 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
#[test]
fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -790,8 +814,8 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(
@ -803,6 +827,7 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
#[test]
fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -822,8 +847,8 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]);
@ -832,6 +857,7 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
#[test]
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -851,8 +877,8 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid layer must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]);
@ -861,6 +887,7 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
#[test]
fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -887,8 +914,8 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
AssetsPayloadSource::from_bytes(asset_data),
);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid composer parameter range must not crash");
assert!(vm.is_halted());
assert_eq!(
@ -900,6 +927,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
#[test]
fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -919,8 +947,8 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::AUDIO);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]);
@ -929,6 +957,7 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
#[test]
fn tick_audio_play_voice_invalid_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -948,8 +977,8 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::AUDIO);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid voice must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]);
@ -958,6 +987,7 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() {
#[test]
fn tick_audio_play_missing_asset_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -977,8 +1007,8 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::AUDIO);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "missing audio asset must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::BankInvalid as i64)]);
@ -987,6 +1017,7 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() {
#[test]
fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -1006,9 +1037,10 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
);
let cartridge = cartridge_with_program(program, caps::AUDIO);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report =
runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("type mismatch must surface as trap");
match report {
CrashReport::VmTrap { trap } => {
assert_eq!(trap.code, TRAP_TYPE);
@ -1022,6 +1054,7 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
#[test]
fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -1041,9 +1074,10 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report =
runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime
.tick(&mut log_service, &mut vm, &signals, &mut hardware)
.expect("type mismatch must surface as trap");
match report {
CrashReport::VmTrap { trap } => {
assert_eq!(trap.code, TRAP_TYPE);

View File

@ -3,6 +3,7 @@ use super::*;
#[test]
fn tick_asset_commit_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -19,8 +20,8 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
@ -29,6 +30,7 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() {
#[test]
fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -45,8 +47,8 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "missing asset must not crash");
assert!(vm.is_halted());
assert_eq!(
@ -58,6 +60,7 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
#[test]
fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -80,8 +83,8 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid slot must not crash");
assert!(vm.is_halted());
assert_eq!(
@ -93,6 +96,7 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
#[test]
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -109,8 +113,8 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "unknown asset handle must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]);
@ -119,6 +123,7 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() {
#[test]
fn tick_bank_info_returns_slot_summary_not_json() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -141,8 +146,8 @@ fn tick_bank_info_returns_slot_summary_not_json() {
);
let cartridge = cartridge_with_program(program, caps::BANK);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "bank summary must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(16), Value::Int64(1)]);
@ -151,6 +156,7 @@ fn tick_bank_info_returns_slot_summary_not_json() {
#[test]
fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -165,7 +171,7 @@ fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
);
let cartridge = cartridge_with_program(program, caps::BANK);
let res = runtime.initialize_vm(&mut vm, &cartridge);
let res = runtime.initialize_vm(&mut log_service, &mut vm, &cartridge);
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
}
@ -173,6 +179,7 @@ fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
#[test]
fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -207,8 +214,8 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid transition must not crash");
assert!(vm.is_halted());
assert_eq!(handle, 1);
@ -218,6 +225,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
#[test]
fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -234,8 +242,8 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "unknown handle cancel must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
@ -244,6 +252,7 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
#[test]
fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -268,7 +277,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
loop {
match hardware.assets.status(handle) {
@ -284,7 +293,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
hardware.assets.apply_commits();
assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED);
let report = runtime.tick(&mut vm, &signals, &mut hardware);
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "cancel after commit must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
@ -293,6 +302,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
#[test]
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -331,8 +341,8 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
);
let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "mixed status-first surface must not crash");
assert!(vm.is_halted());
assert_eq!(

View File

@ -3,7 +3,8 @@ use super::*;
#[test]
fn tick_memcard_slot_roundtrip_for_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None);
runtime.mount_fs(Box::new(MemFsBackend::default()));
let mut log_service = LogService::new(4096);
runtime.mount_fs(&mut log_service, Box::new(MemFsBackend::default()));
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -50,8 +51,8 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
preload: vec![],
};
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "memcard roundtrip must not crash");
assert!(vm.is_halted());
assert_eq!(
@ -63,6 +64,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
#[test]
fn tick_memcard_access_is_denied_for_non_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
@ -89,8 +91,8 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
preload: vec![],
};
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut log_service, &mut vm, &signals, &mut hardware);
assert!(report.is_none(), "non-game memcard call must return status");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]);

View File

@ -1,7 +1,8 @@
use super::dispatch::VmRuntimeHost;
use super::*;
use crate::CrashReport;
use prometeu_hal::asset::{BankTelemetry, BankType};
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::{HardwareBridge, HostContext, InputSignals};
use prometeu_vm::LogicalFrameEndingReason;
use std::panic::{AssertUnwindSafe, catch_unwind};
@ -52,11 +53,17 @@ impl VirtualMachineRuntime {
pub fn debug_step_instruction(
&mut self,
log_service: &mut LogService,
vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge,
) -> Option<CrashReport> {
let mut ctx = HostContext::new(Some(hw));
match vm.step(self, &mut ctx) {
let step_result = {
let mut ctx = HostContext::new(Some(hw));
let mut host = VmRuntimeHost { runtime: self, log_service };
vm.step(&mut host, &mut ctx)
};
match step_result {
Ok(_) => None,
Err(e) => {
let report = match e {
@ -70,6 +77,7 @@ impl VirtualMachineRuntime {
},
};
self.log(
log_service,
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
@ -83,6 +91,7 @@ impl VirtualMachineRuntime {
pub fn tick(
&mut self,
log_service: &mut LogService,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
@ -94,7 +103,7 @@ impl VirtualMachineRuntime {
return None;
}
self.update_fs();
self.update_fs(log_service);
if !self.logical_frame_active {
self.logical_frame_active = true;
@ -128,7 +137,8 @@ impl VirtualMachineRuntime {
if budget > 0 {
let run_result = {
let mut ctx = HostContext::new(Some(hw));
vm.run_budget(budget, self, &mut ctx)
let mut host = VmRuntimeHost { runtime: self, log_service };
vm.run_budget(budget, &mut host, &mut ctx)
};
match run_result {
@ -143,6 +153,7 @@ impl VirtualMachineRuntime {
self.paused = true;
self.debug_step_request = false;
self.log(
log_service,
LogLevel::Info,
LogSource::Vm,
0xDEB1,
@ -154,6 +165,7 @@ impl VirtualMachineRuntime {
let report =
CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) };
self.log(
log_service,
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
@ -166,6 +178,7 @@ impl VirtualMachineRuntime {
if let LogicalFrameEndingReason::Trap(trap) = &run.reason {
let report = CrashReport::VmTrap { trap: trap.clone() };
self.log(
log_service,
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
@ -182,6 +195,7 @@ impl VirtualMachineRuntime {
let message = Self::host_panic_payload_to_string(payload);
let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) };
self.log(
log_service,
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
@ -229,18 +243,15 @@ impl VirtualMachineRuntime {
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
let telemetry_snapshot = self.atomic_telemetry.snapshot();
let violations = self.certifier.evaluate(
&telemetry_snapshot,
&mut self.log_service,
ts_ms,
) as u32;
let violations =
self.certifier.evaluate(&telemetry_snapshot, log_service, ts_ms) as u32;
self.atomic_telemetry.violations.store(violations, Ordering::Relaxed);
self.atomic_telemetry
.completed_logical_frames
.fetch_add(1, Ordering::Relaxed);
self.log_service.reset_count();
log_service.reset_count();
self.logical_frame_index += 1;
self.logical_frame_active = false;
@ -258,7 +269,13 @@ impl VirtualMachineRuntime {
}
Err(e) => {
let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) };
self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary());
self.log(
log_service,
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
report.summary(),
);
self.last_crash_report = Some(report.clone());
return Some(report);
}

View File

@ -31,4 +31,4 @@
{"type":"discussion","id":"DSC-0030","status":"done","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-28","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0030-internal-viewport-270p/LSN-0039-resolution-baseline-changes-are-runtime-wide-contracts.md","status":"done","created_at":"2026-04-28","updated_at":"2026-04-28"}]}
{"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]}
{"type":"discussion","id":"DSC-0032","status":"open","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-14","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[{"id":"AGD-0032","file":"AGD-0032-system-os-lifecycle-process-task-contract.md","status":"open","created_at":"2026-05-14","updated_at":"2026-05-14"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0033","status":"in_progress","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-14","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[{"id":"AGD-0033","file":"AGD-0033-system-os-service-ownership-and-module-layout.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14"}],"decisions":[{"id":"DEC-0024","file":"DEC-0024-systemos-service-ownership-and-module-layout.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14","ref_agenda":"AGD-0033"}],"plans":[{"id":"PLN-0051","file":"PLN-0051-move-vm-runtime-into-services.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0052","file":"PLN-0052-promote-window-manager-to-systemos-service.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0053","file":"PLN-0053-move-logging-ownership-to-systemos.md","status":"open","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":"open","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]}],"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":"open","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]}],"lessons":[]}

View File

@ -2,7 +2,7 @@
id: PLN-0053
ticket: system-os-service-ownership-and-module-layout
title: Move Logging Ownership To SystemOS
status: open
status: done
created: 2026-05-14
ref_decisions: [DEC-0024]
tags: [runtime, os, services, module-layout, vm, window-manager, logging]