prometeu-runtime/crates/console/prometeu-system/src/virtual_machine_runtime.rs

1441 lines
59 KiB
Rust

use prometeu_hal::syscalls::Syscall;
use crate::fs::{FsBackend, FsState, VirtualFS};
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use std::collections::HashMap;
use std::time::Instant;
use prometeu_abi::{Value, VmFault};
use prometeu_hal::{expect_bool, expect_int, HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_hal::asset::{BankType, LoadStatus, SlotRef};
use prometeu_hal::button::Button;
use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::tile::Tile;
use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
pub struct VirtualMachineRuntime {
/// Host Tick Index: Incremented on every host hardware update (usually 60Hz).
pub tick_index: u64,
/// Logical Frame Index: Incremented only when an app successfully completes a full frame via `FRAME_SYNC`.
pub logical_frame_index: u64,
/// Execution State: True if the VM is currently mid-frame.
pub logical_frame_active: bool,
/// Cycle Budget: The number of PVM cycles remaining for the current logical frame.
pub logical_frame_remaining_cycles: u64,
/// Performance Metric: Time spent by the host CPU processing the last tick (in microseconds).
pub last_frame_cpu_time_us: u64,
// --- Filesystem ---
/// The virtual filesystem interface, abstracting the physical storage.
pub fs: VirtualFS,
/// Current health and connection status of the virtual filesystem.
pub fs_state: FsState,
/// Active file handles mapping IDs to their virtual paths.
pub open_files: HashMap<u32, String>,
/// Generator for unique, non-zero file handles.
pub next_handle: u32,
// --- Logging & Identity ---
/// The centralized service for recording and retrieving system logs.
pub log_service: LogService,
/// Metadata for the currently running application.
pub current_app_id: u32,
pub current_cartridge_title: String,
pub current_cartridge_app_version: String,
pub current_cartridge_app_mode: AppMode,
pub current_entrypoint: String,
/// Rate-limiter to prevent apps from flooding the log buffer and killing performance.
pub logs_written_this_frame: HashMap<u32, u32>,
// --- Monitoring & Debugging ---
/// Running counters for the current execution slice.
pub telemetry_current: TelemetryFrame,
/// The results of the last successfully completed logical frame.
pub telemetry_last: TelemetryFrame,
/// Logic for validating that the app obeys the console's Certification (CAP).
pub certifier: Certifier,
/// Pause state: When true, `tick()` will not advance the VM.
pub paused: bool,
/// Debugging flag to execute exactly one instruction or frame regardless of budget.
pub debug_step_request: bool,
/// When true, the next logical frame must rearm the entrypoint call before running
/// to avoid resuming at a pending RET after a FRAME_SYNC safe point.
needs_prepare_entry_call: bool,
/// Wall-clock time of system startup.
boot_time: Instant,
}
impl VirtualMachineRuntime {
/// Default number of cycles assigned to a single game frame.
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 100_000;
/// Maximum number of cycles allowed to execute in a single host tick.
/// Usually the same as CYCLES_PER_LOGICAL_FRAME to target 60FPS.
pub const SLICE_PER_TICK: u64 = 100_000;
/// Maximum characters allowed per log message.
pub const MAX_LOG_LEN: usize = 256;
/// Maximum log entries an App can emit in a single frame.
pub const MAX_LOGS_PER_FRAME: u32 = 10;
/// Creates a new POS instance with optional certification rules.
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
let boot_time = Instant::now();
let mut os = Self {
tick_index: 0,
logical_frame_index: 0,
logical_frame_active: false,
logical_frame_remaining_cycles: 0,
last_frame_cpu_time_us: 0,
fs: VirtualFS::new(),
fs_state: FsState::Unmounted,
open_files: HashMap::new(),
next_handle: 1,
log_service: LogService::new(4096),
current_app_id: 0,
current_cartridge_title: String::new(),
current_cartridge_app_version: String::new(),
current_cartridge_app_mode: AppMode::Game,
current_entrypoint: String::new(),
logs_written_this_frame: HashMap::new(),
telemetry_current: TelemetryFrame::default(),
telemetry_last: TelemetryFrame::default(),
certifier: Certifier::new(cap_config.unwrap_or_default()),
paused: false,
debug_step_request: false,
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) {
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);
}
pub fn mount_fs(&mut self, backend: Box<dyn FsBackend>) {
self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string());
match self.fs.mount(backend) {
Ok(_) => {
self.fs_state = FsState::Mounted;
self.log(LogLevel::Info, LogSource::Fs, 0, "Filesystem mounted successfully".to_string());
}
Err(e) => {
let err_msg = format!("Failed to mount filesystem: {:?}", e);
self.log(LogLevel::Error, LogSource::Fs, 0, err_msg);
self.fs_state = FsState::Error(e);
}
}
}
pub fn unmount_fs(&mut self) {
self.fs.unmount();
self.fs_state = FsState::Unmounted;
}
fn update_fs(&mut self) {
if self.fs_state == FsState::Mounted {
if !self.fs.is_healthy() {
self.log(LogLevel::Error, LogSource::Fs, 0, "Filesystem became unhealthy, unmounting".to_string());
self.unmount_fs();
}
}
}
pub fn reset(&mut self, vm: &mut VirtualMachine) {
*vm = VirtualMachine::default();
self.tick_index = 0;
self.logical_frame_index = 0;
self.logical_frame_active = false;
self.logical_frame_remaining_cycles = 0;
self.last_frame_cpu_time_us = 0;
}
/// Loads a cartridge into the PVM and resets the execution state.
pub fn initialize_vm(&mut self, vm: &mut VirtualMachine, cartridge: &Cartridge) {
match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) {
Ok(_) => {
// Determines the numeric app_id
self.current_app_id = cartridge.app_id;
self.current_cartridge_title = cartridge.title.clone();
self.current_cartridge_app_version = cartridge.app_version.clone();
self.current_cartridge_app_mode = cartridge.app_mode;
self.current_entrypoint = cartridge.entrypoint.clone();
}
Err(e) => {
self.log(LogLevel::Error, LogSource::Vm, 0, format!("Failed to initialize VM: {:?}", e));
// Fail fast: no program is installed, no app id is switched.
// We don't update current_app_id or other fields.
}
}
}
/// Executes a single VM instruction (Debug).
pub fn debug_step_instruction(&mut self, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Option<String> {
let mut ctx = HostContext::new(Some(hw));
match vm.step(self, &mut ctx) {
Ok(_) => None,
Err(e) => {
let err_msg = format!("PVM Fault during Step: {:?}", e);
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
Some(err_msg)
}
}
}
/// Executes a single host tick (nominally 60Hz).
///
/// This method is responsible for managing the logical frame lifecycle.
/// A single host tick might execute a full logical frame, part of it,
/// or multiple frames depending on the configured slices.
pub fn tick(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> {
let start = Instant::now();
self.tick_index += 1;
// If the system is paused, we don't advance unless there's a debug step request.
if self.paused && !self.debug_step_request {
return None;
}
self.update_fs();
// 1. Frame Initialization
// If we are not currently in the middle of a logical frame, start a new one.
if !self.logical_frame_active {
self.logical_frame_active = true;
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
self.begin_logical_frame(signals, hw);
// If the VM is not currently executing a function (e.g. at the start of the app
// or after the entrypoint function returned), we prepare a new call to the entrypoint.
// Additionally, if the previous slice ended with FRAME_SYNC, we must force a rearm
// so we don't resume execution at a pending RET on the next tick.
if self.needs_prepare_entry_call || vm.call_stack.is_empty() {
vm.prepare_call(&self.current_entrypoint);
self.needs_prepare_entry_call = false;
}
// Reset telemetry for the new logical frame
self.telemetry_current = TelemetryFrame {
frame_index: self.logical_frame_index,
cycles_budget: self.certifier.config.cycles_budget_per_frame.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
..Default::default()
};
}
// 2. Budget Allocation
// Determines how many cycles we can run in this host tick.
let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles);
// 3. VM Execution
if budget > 0 {
// Run the VM until the budget is hit or FRAME_SYNC is reached.
let run_result = {
let mut ctx = HostContext::new(Some(hw));
vm.run_budget(budget, self, &mut ctx)
}; // internally dispatch to frame on SDK
match run_result {
Ok(run) => {
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
// Accumulate metrics for telemetry and certification
self.telemetry_current.cycles_used += run.cycles_used;
self.telemetry_current.vm_steps += run.steps_executed;
// Handle Breakpoints
if run.reason == LogicalFrameEndingReason::Breakpoint {
self.paused = true;
self.debug_step_request = false;
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc));
}
// Handle Panics
if let LogicalFrameEndingReason::Panic(err) = run.reason {
let err_msg = format!("PVM Fault: \"{}\"", err);
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
return Some(err_msg);
}
// 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned)
if run.reason == LogicalFrameEndingReason::FrameSync ||
run.reason == LogicalFrameEndingReason::EndOfRom {
// All drawing commands for this frame are now complete.
// Finalize the framebuffer.
hw.gfx_mut().render_all();
// Finalize frame telemetry
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
// Evaluate CAP (Execution Budget Compliance)
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32;
// Latch telemetry for the Host/Debugger to read.
self.telemetry_last = self.telemetry_current;
self.logical_frame_index += 1;
self.logical_frame_active = false;
self.logical_frame_remaining_cycles = 0;
// If the slice ended in FRAME_SYNC, ensure the next tick starts a fresh
// call to the entrypoint instead of resuming at the RET that follows.
if run.reason == LogicalFrameEndingReason::FrameSync {
self.needs_prepare_entry_call = true;
}
// If we were doing a "step frame" debug command, pause now that the frame is done.
if self.debug_step_request {
self.paused = true;
self.debug_step_request = false;
}
}
}
Err(e) => {
// Fatal VM fault (division by zero, invalid memory access, etc).
let err_msg = format!("PVM Fault: {:?}", e);
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
return Some(err_msg);
}
}
}
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
// Update bank telemetry in the current frame (snapshot)
let gfx_stats = hw.assets().bank_info(BankType::TILES);
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32;
let audio_stats = hw.assets().bank_info(BankType::SOUNDS);
self.telemetry_current.audio_used_bytes = audio_stats.used_bytes;
self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes;
self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32;
// If the frame ended exactly in this tick, we update the final real time in the latch.
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget;
self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes;
self.telemetry_last.gfx_inflight_bytes = self.telemetry_current.gfx_inflight_bytes;
self.telemetry_last.gfx_slots_occupied = self.telemetry_current.gfx_slots_occupied;
self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes;
self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes;
self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied;
}
None
}
fn begin_logical_frame(&mut self, _signals: &InputSignals, hw: &mut dyn HardwareBridge) {
hw.audio_mut().clear_commands();
self.logs_written_this_frame.clear();
}
// Helper for syscalls
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
let level = match level_val {
0 => LogLevel::Trace,
1 => LogLevel::Debug,
2 => LogLevel::Info,
3 => LogLevel::Warn,
4 => LogLevel::Error,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Invalid log level: {}", level_val))),
};
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 {
self.logs_written_this_frame.insert(app_id, count + 1);
self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string());
}
return Ok(());
}
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);
}
self.log(level, LogSource::App { app_id }, tag, final_msg);
Ok(())
}
pub fn get_color(&self, value: i64) -> Color {
// We now use the value directly as RGB565.
Color::from_raw(value as u16)
}
// Helper for syscalls
pub fn get_button<'a>(&self, id: u32, hw: &'a dyn HardwareBridge) -> Option<&'a Button> {
let pad = hw.pad();
match id {
0 => Some(pad.up()),
1 => Some(pad.down()),
2 => Some(pad.left()),
3 => Some(pad.right()),
4 => Some(pad.a()),
5 => Some(pad.b()),
6 => Some(pad.x()),
7 => Some(pad.y()),
8 => Some(pad.l()),
9 => Some(pad.r()),
10 => Some(pad.start()),
11 => Some(pad.select()),
_ => None,
}
}
pub fn is_button_down(&self, id: u32, hw: &mut dyn HardwareBridge) -> bool {
match id {
0 => hw.pad().up().down,
1 => hw.pad().down().down,
2 => hw.pad().left().down,
3 => hw.pad().right().down,
4 => hw.pad().a().down,
5 => hw.pad().b().down,
6 => hw.pad().x().down,
7 => hw.pad().y().down,
8 => hw.pad().l().down,
9 => hw.pad().r().down,
10 => hw.pad().start().down,
11 => hw.pad().select().down,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use prometeu_drivers::hardware::Hardware;
use prometeu_abi::Value;
use prometeu_hal::{HostReturn, InputSignals};
use super::*;
fn call_syscall(os: &mut VirtualMachineRuntime, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<(), VmFault> {
let args_count = Syscall::from_u32(id).expect(&format!("Invalid syscall id: 0x{:08X}", id)).args_count();
let mut args = Vec::new();
for _ in 0..args_count {
// Protege contra underflow/erros de pilha durante testes
match vm.pop() {
Ok(v) => args.push(v),
Err(e) => return Err(VmFault::Panic(e)),
}
}
args.reverse();
let mut ret = HostReturn::new(&mut vm.operand_stack);
os.syscall(id, &args, &mut ret, &mut HostContext::new(Some(hw)))
}
#[test]
fn test_infinite_loop_budget_reset_bug() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
let rom = prometeu_bytecode::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: 6,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
}],
code: vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
debug_info: None,
exports: vec![prometeu_bytecode::Export { symbol: "main".into(), func_idx: 0 }],
}.serialize();
let cartridge = Cartridge {
app_id: 1234,
title: "test".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
entrypoint: "0".to_string(),
program: rom,
assets: vec![],
asset_table: vec![],
preload: vec![],
};
os.initialize_vm(&mut vm, &cartridge);
// First tick
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_1 = vm.cycles;
assert!(cycles_after_tick_1 >= VirtualMachineRuntime::CYCLES_PER_LOGICAL_FRAME);
// Second tick - Now it SHOULD NOT gain more budget
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_2 = vm.cycles;
// FIX: It should not have consumed cycles in the second tick because the logical frame budget ended
println!("Cycles after tick 1: {}, tick 2: {}", cycles_after_tick_1, cycles_after_tick_2);
assert_eq!(cycles_after_tick_2, cycles_after_tick_1, "VM should NOT have consumed more cycles in the second tick because logical frame budget is exhausted");
}
#[test]
fn test_budget_reset_on_frame_sync() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
// Loop that calls FrameSync:
// PUSH_CONST 0 (dummy)
// FrameSync (0x80)
// JMP 0
let rom = prometeu_bytecode::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: 8,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
}],
code: vec![
0x80, 0x00, // FrameSync (2 bytes opcode)
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // JMP 0 (2 bytes opcode + 4 bytes u32)
],
debug_info: None,
exports: vec![prometeu_bytecode::Export { symbol: "main".into(), func_idx: 0 }],
}.serialize();
let cartridge = Cartridge {
app_id: 1234,
title: "test".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
entrypoint: "0".to_string(),
program: rom,
assets: vec![],
asset_table: vec![],
preload: vec![],
};
os.initialize_vm(&mut vm, &cartridge);
// First tick
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_1 = vm.cycles;
// Should have stopped at FrameSync
assert!(cycles_after_tick_1 > 0, "VM should have consumed some cycles");
assert!(cycles_after_tick_1 < VirtualMachineRuntime::CYCLES_PER_LOGICAL_FRAME);
// Second tick - Should reset the budget and run a bit more until the next FrameSync
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_2 = vm.cycles;
assert!(cycles_after_tick_2 > cycles_after_tick_1, "VM should have consumed more cycles because FrameSync reset the budget");
}
#[test]
fn test_telemetry_cycles_budget() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
// Standard budget
os.tick(&mut vm, &signals, &mut hw);
assert_eq!(os.telemetry_current.cycles_budget, VirtualMachineRuntime::CYCLES_PER_LOGICAL_FRAME);
// Custom budget via CAP
let mut config = CertificationConfig::default();
config.enabled = true;
config.cycles_budget_per_frame = Some(50000);
let mut os_custom = VirtualMachineRuntime::new(Some(config));
os_custom.tick(&mut vm, &signals, &mut hw);
assert_eq!(os_custom.telemetry_current.cycles_budget, 50000);
}
#[test]
fn test_get_color_logic() {
let os = VirtualMachineRuntime::new(None);
// Deve retornar a cor raw diretamente
assert_eq!(os.get_color(0x07E0), Color::GREEN);
assert_eq!(os.get_color(0xF800), Color::RED);
assert_eq!(os.get_color(0x001F), Color::BLUE);
assert_eq!(os.get_color(3), Color::from_raw(3));
}
#[test]
fn test_gfx_set_sprite_syscall_pops() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
// Push arguments in order 1 to 10
vm.push(Value::String("mouse_cursor".to_string())); // arg1: assetName
vm.push(Value::Int32(0)); // arg2: id
// Simulating touch.x and touch.y syscalls
vm.push(Value::Int32(10)); // arg3: x (returned from syscall)
vm.push(Value::Int32(20)); // arg4: y (returned from syscall)
vm.push(Value::Int32(0)); // arg5: tileId
vm.push(Value::Int32(0)); // arg6: paletteId
vm.push(Value::Boolean(true)); // arg7: active
vm.push(Value::Boolean(false)); // arg8: flipX
vm.push(Value::Boolean(false)); // arg9: flipY
vm.push(Value::Int32(4)); // arg10: priority
let res = call_syscall(&mut os, 0x1007, &mut vm, &mut hw);
assert!(res.is_ok(), "GfxSetSprite syscall should succeed, but got: {:?}", res.err());
}
#[test]
fn test_gfx_set_sprite_with_swapped_arguments_repro() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
// Repro: what if the compiler is pushing in reverse order?
vm.push(Value::Int32(4)); // arg10?
vm.push(Value::Boolean(false));
vm.push(Value::Boolean(false));
vm.push(Value::Boolean(true));
vm.push(Value::Int32(0));
vm.push(Value::Int32(0));
vm.push(Value::Int32(20));
vm.push(Value::Int32(10));
vm.push(Value::Int32(0));
vm.push(Value::String("mouse_cursor".to_string())); // arg1?
let res = call_syscall(&mut os, 0x1007, &mut vm, &mut hw);
assert!(res.is_err());
// Because it tries to pop priority but gets a string
match res.err().unwrap() {
VmFault::Trap(code, _) => assert_eq!(code, prometeu_bytecode::abi::TRAP_TYPE),
_ => panic!("Expected Trap"),
}
}
#[test]
fn test_syscall_log_write_and_rate_limit() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
os.current_app_id = 123;
// 1. Normal log test
vm.push(Value::Int64(2)); // Info
vm.push(Value::String("Hello Log".to_string()));
let res = call_syscall(&mut os, 0x5001, &mut vm, &mut hw);
assert!(res.is_ok());
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg, "Hello Log");
assert_eq!(recent[0].level, LogLevel::Info);
assert_eq!(recent[0].source, LogSource::App { app_id: 123 });
// 2. Truncation test
let long_msg = "A".repeat(300);
vm.push(Value::Int64(3)); // Warn
vm.push(Value::String(long_msg));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg.len(), 256);
assert!(recent[0].msg.starts_with("AAAAA"));
// 3. Rate Limit Test
// We already made 2 logs. The limit is 10.
for i in 0..8 {
vm.push(Value::Int64(2));
vm.push(Value::String(format!("Log {}", i)));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
}
// The 11th log should be ignored (and generate a system warning)
vm.push(Value::Int64(2));
vm.push(Value::String("Eleventh log".to_string()));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(2);
// The last log should be the rate limit warning (came after the 10th log attempted)
assert_eq!(recent[1].msg, "App exceeded log limit per frame");
assert_eq!(recent[1].level, LogLevel::Warn);
// The log "Eleventh log" should not be there
assert_ne!(recent[0].msg, "Eleventh log");
// 4. Rate limit reset test in the next frame
os.begin_logical_frame(&InputSignals::default(), &mut hw);
vm.push(Value::Int64(2));
vm.push(Value::String("New frame log".to_string()));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg, "New frame log");
// 5. LOG_WRITE_TAG test
vm.push(Value::Int64(2)); // Info
vm.push(Value::Int64(42)); // Tag
vm.push(Value::String("Tagged Log".to_string()));
call_syscall(&mut os, 0x5002, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg, "Tagged Log");
assert_eq!(recent[0].tag, 42);
// Syscall de log é void: não empurra valor na pilha
// 6. GFX Syscall return test
vm.push(Value::Int64(1)); // color_idx
call_syscall(&mut os, 0x1001, &mut vm, &mut hw).unwrap(); // gfx.clear
assert_eq!(vm.pop().unwrap(), Value::Null);
}
#[test]
fn test_entrypoint_called_every_frame() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
// PushI32 0 (0x17), then Ret (0x51)
let rom = prometeu_bytecode::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: 10,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
}],
code: vec![
0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0
0x11, 0x00, // Pop
0x51, 0x00 // Ret
],
debug_info: None,
exports: vec![prometeu_bytecode::Export { symbol: "main".into(), func_idx: 0 }],
}.serialize();
let cartridge = Cartridge {
app_id: 1234,
title: "test".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
entrypoint: "0".to_string(),
program: rom,
assets: vec![],
asset_table: vec![],
preload: vec![],
};
os.initialize_vm(&mut vm, &cartridge);
// First frame
os.tick(&mut vm, &signals, &mut hw);
assert_eq!(os.logical_frame_index, 1);
assert!(!os.logical_frame_active);
assert!(vm.call_stack.is_empty());
// Second frame - Should call entrypoint again
os.tick(&mut vm, &signals, &mut hw);
assert_eq!(os.logical_frame_index, 2);
assert!(!os.logical_frame_active);
assert!(vm.call_stack.is_empty());
}
#[test]
fn test_os_unknown_syscall_returns_trap() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let mut ret = HostReturn::new(&mut vm.operand_stack);
let res = os.syscall(0xDEADBEEF, &[], &mut ret, &mut HostContext::new(Some(&mut hw)));
assert!(res.is_err());
match res.err().unwrap() {
VmFault::Trap(code, _) => assert_eq!(code, prometeu_bytecode::abi::TRAP_INVALID_SYSCALL),
_ => panic!("Expected Trap"),
}
}
#[test]
fn test_gfx_clear565_syscall() {
let mut hw = Hardware::new();
let mut os = VirtualMachineRuntime::new(None);
let mut stack = Vec::new();
// Success case
let args = vec![Value::Bounded(0xF800)]; // Red
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::GfxClear565 as u32, &args, &mut ret, &mut HostContext::new(Some(&mut hw))).unwrap();
}
assert_eq!(stack.len(), 0); // void return
// OOB case
let args = vec![Value::Bounded(0x10000)];
{
let mut ret = HostReturn::new(&mut stack);
let res = os.syscall(Syscall::GfxClear565 as u32, &args, &mut ret, &mut HostContext::new(Some(&mut hw)));
assert!(res.is_err());
match res.err().unwrap() {
VmFault::Trap(trap, _) => assert_eq!(trap, prometeu_bytecode::abi::TRAP_OOB),
_ => panic!("Expected Trap OOB"),
}
}
}
#[test]
fn test_input_snapshots_syscalls() {
let mut hw = Hardware::new();
let mut os = VirtualMachineRuntime::new(None);
// Pad snapshot
let mut stack = Vec::new();
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::InputPadSnapshot as u32, &[], &mut ret, &mut HostContext::new(Some(&mut hw))).unwrap();
}
assert_eq!(stack.len(), 48);
// Touch snapshot
let mut stack = Vec::new();
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::InputTouchSnapshot as u32, &[], &mut ret, &mut HostContext::new(Some(&mut hw))).unwrap();
}
assert_eq!(stack.len(), 6);
}
#[test]
fn test_os_syscall_without_hardware_fails_graciously() {
let mut os = VirtualMachineRuntime::new(None);
let mut stack = Vec::new();
let mut ret = HostReturn::new(&mut stack);
// GfxClear needs hardware
let res = os.syscall(Syscall::GfxClear as u32, &[Value::Int64(0)], &mut ret, &mut HostContext::new(None));
assert_eq!(res, Err(VmFault::Unavailable));
// SystemHasCart DOES NOT need hardware
let res = os.syscall(Syscall::SystemHasCart as u32, &[], &mut ret, &mut HostContext::new(None));
assert!(res.is_ok());
}
}
impl NativeInterface for VirtualMachineRuntime {
/// Dispatches a syscall from the VM to the native implementation.
///
/// Syscalls are grouped by functionality:
/// - 0x0000: System/Cartridge management
/// - 0x1000: Graphics (GFX)
/// - 0x2000: Input
/// - 0x3000: Audio
/// - 0x4000: Filesystem (FS)
/// - 0x5000: Logging
///
/// Each syscall returns the number of virtual cycles it consumed.
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext) -> Result<(), VmFault> {
self.telemetry_current.syscalls += 1;
let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_INVALID_SYSCALL, format!(
"Unknown syscall: 0x{:08X}", id
)))?;
// Handle hardware-less syscalls first
match syscall {
Syscall::SystemHasCart => {
ret.push_bool(true);
return Ok(());
}
Syscall::SystemRunCart => {
ret.push_null();
return Ok(());
}
_ => {}
}
let hw = ctx.require_hw()?;
match syscall {
// --- System Syscalls ---
Syscall::SystemHasCart => unreachable!(),
Syscall::SystemRunCart => unreachable!(),
// --- GFX Syscalls ---
// gfx.clear(color_index) -> null
Syscall::GfxClear => {
let color_val = expect_int(args, 0)?;
let color = self.get_color(color_val);
hw.gfx_mut().clear(color);
ret.push_null();
Ok(())
}
// gfx.draw_rect(x, y, w, h, color_index) -> null
Syscall::GfxFillRect => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let color_val = expect_int(args, 4)?;
let color = self.get_color(color_val);
hw.gfx_mut().fill_rect(x, y, w, h, color);
ret.push_null();
Ok(())
}
// gfx.draw_line(x1, y1, x2, y2, color_index) -> null
Syscall::GfxDrawLine => {
let x1 = expect_int(args, 0)? as i32;
let y1 = expect_int(args, 1)? as i32;
let x2 = expect_int(args, 2)? as i32;
let y2 = expect_int(args, 3)? as i32;
let color_val = expect_int(args, 4)?;
let color = self.get_color(color_val);
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
ret.push_null();
Ok(())
}
// gfx.draw_circle(x, y, r, color_index) -> null
Syscall::GfxDrawCircle => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let color_val = expect_int(args, 3)?;
let color = self.get_color(color_val);
hw.gfx_mut().draw_circle(x, y, r, color);
ret.push_null();
Ok(())
}
// gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) -> null
Syscall::GfxDrawDisc => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let border_color_val = expect_int(args, 3)?;
let fill_color_val = expect_int(args, 4)?;
let fill_color = self.get_color(fill_color_val);
let border_color = self.get_color(border_color_val);
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
ret.push_null();
Ok(())
}
// gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) -> null
Syscall::GfxDrawSquare => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let border_color_val = expect_int(args, 4)?;
let fill_color_val = expect_int(args, 5)?;
let fill_color = self.get_color(fill_color_val);
let border_color = self.get_color(border_color_val);
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
ret.push_null();
Ok(())
}
// gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority)
Syscall::GfxSetSprite => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())),
};
let index = expect_int(args, 1)? as usize;
let x = expect_int(args, 2)? as i32;
let y = expect_int(args, 3)? as i32;
let tile_id = expect_int(args, 4)? as u16;
let palette_id = expect_int(args, 5)? as u8;
let active = expect_bool(args, 6)?;
let flip_x = expect_bool(args, 7)?;
let flip_y = expect_bool(args, 8)?;
let priority = expect_int(args, 9)? as u8;
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
if index < 512 {
*hw.gfx_mut().sprite_mut(index) = Sprite {
tile: Tile { id: tile_id, flip_x: false, flip_y: false, palette_id },
x,
y,
bank_id,
active,
flip_x,
flip_y,
priority,
};
}
ret.push_null();
Ok(())
}
Syscall::GfxDrawText => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
};
let color_val = expect_int(args, 3)?;
let color = self.get_color(color_val);
hw.gfx_mut().draw_text(x, y, &msg, color);
ret.push_null();
Ok(())
}
// gfx.clear565(color_u16) -> void
Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF {
return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_OOB, "Color value out of bounds (bounded)".into()));
}
let color = Color::from_raw(color_val as u16);
hw.gfx_mut().clear(color);
// No return value for void
Ok(())
}
// --- Input Syscalls ---
// input.get_pad(button_id) -> bool
Syscall::InputGetPad => {
let button_id = expect_int(args, 0)? as u32;
let is_down = self.is_button_down(button_id, hw);
ret.push_bool(is_down);
Ok(())
}
Syscall::InputGetPadPressed => {
let button_id = expect_int(args, 0)? as u32;
let val = self.get_button(button_id, hw).map(|b| b.pressed).unwrap_or(false);
ret.push_bool(val);
Ok(())
}
Syscall::InputGetPadReleased => {
let button_id = expect_int(args, 0)? as u32;
let val = self.get_button(button_id, hw).map(|b| b.released).unwrap_or(false);
ret.push_bool(val);
Ok(())
}
Syscall::InputGetPadHold => {
let button_id = expect_int(args, 0)? as u32;
let val = self.get_button(button_id, hw).map(|b| b.hold_frames).unwrap_or(0);
ret.push_int(val as i64);
Ok(())
}
Syscall::TouchGetX => {
ret.push_int(hw.touch().x() as i64);
Ok(())
}
Syscall::TouchGetY => {
ret.push_int(hw.touch().y() as i64);
Ok(())
}
Syscall::TouchIsDown => {
ret.push_bool(hw.touch().f().down);
Ok(())
}
Syscall::TouchIsPressed => {
ret.push_bool(hw.touch().f().pressed);
Ok(())
}
Syscall::TouchIsReleased => {
ret.push_bool(hw.touch().f().released);
Ok(())
}
Syscall::TouchGetHold => {
ret.push_int(hw.touch().f().hold_frames as i64);
Ok(())
}
Syscall::TouchGetFinger => {
let btn = hw.touch().f();
// Return as 4 slots to mirror snapshot order
ret.push_bool(btn.pressed);
ret.push_bool(btn.released);
ret.push_bool(btn.down);
ret.push_int(btn.hold_frames as i64);
Ok(())
}
Syscall::InputPadSnapshot => {
let pad = hw.pad();
for btn in [
pad.up(), pad.down(), pad.left(), pad.right(),
pad.a(), pad.b(), pad.x(), pad.y(),
pad.l(), pad.r(), pad.start(), pad.select(),
] {
ret.push_bool(btn.pressed);
ret.push_bool(btn.released);
ret.push_bool(btn.down);
ret.push_int(btn.hold_frames as i64);
}
Ok(())
}
Syscall::InputTouchSnapshot => {
let touch = hw.touch();
ret.push_bool(touch.f().pressed);
ret.push_bool(touch.f().released);
ret.push_bool(touch.f().down);
ret.push_int(touch.f().hold_frames as i64);
ret.push_int(touch.x() as i64);
ret.push_int(touch.y() as i64);
Ok(())
}
// --- Pad per-button service-based syscalls (return Button as 4 slots) ---
Syscall::PadGetUp => {
let b = hw.pad().up();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetDown => {
let b = hw.pad().down();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetLeft => {
let b = hw.pad().left();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetRight => {
let b = hw.pad().right();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetA => {
let b = hw.pad().a();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetB => {
let b = hw.pad().b();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetX => {
let b = hw.pad().x();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetY => {
let b = hw.pad().y();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetL => {
let b = hw.pad().l();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetR => {
let b = hw.pad().r();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetStart => {
let b = hw.pad().start();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
Syscall::PadGetSelect => {
let b = hw.pad().select();
ret.push_bool(b.pressed);
ret.push_bool(b.released);
ret.push_bool(b.down);
ret.push_int(b.hold_frames as i64);
Ok(())
}
// --- Audio Syscalls ---
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
Syscall::AudioPlaySample => {
let sample_id = expect_int(args, 0)? as u32;
let voice_id = expect_int(args, 1)? as usize;
let volume = expect_int(args, 2)? as u8;
let pan = expect_int(args, 3)? as u8;
let pitch = match args.get(4).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? {
Value::Float(f) => *f,
Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64,
Value::Bounded(b) => *b as f64,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected number for pitch".into())),
};
hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, prometeu_hal::LoopMode::Off);
ret.push_null();
Ok(())
}
// audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode)
Syscall::AudioPlay => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())),
};
let sample_id = expect_int(args, 1)? as u16;
let voice_id = expect_int(args, 2)? as usize;
let volume = expect_int(args, 3)? as u8;
let pan = expect_int(args, 4)? as u8;
let pitch = match args.get(5).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? {
Value::Float(f) => *f,
Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64,
Value::Bounded(b) => *b as f64,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected number for pitch".into())),
};
let loop_mode = match expect_int(args, 6)? {
0 => prometeu_hal::LoopMode::Off,
_ => prometeu_hal::LoopMode::On,
};
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0);
hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode);
ret.push_null();
Ok(())
}
// --- Filesystem Syscalls (0x4000) ---
// FS_OPEN(path) -> handle
Syscall::FsOpen => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
};
if self.fs_state != FsState::Mounted {
ret.push_int(-1);
return Ok(());
}
let handle = self.next_handle;
self.open_files.insert(handle, path);
self.next_handle += 1;
ret.push_int(handle as i64);
Ok(())
}
// FS_READ(handle) -> content
Syscall::FsRead => {
let handle = expect_int(args, 0)? as u32;
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.read_file(path) {
Ok(data) => {
let s = String::from_utf8_lossy(&data).into_owned();
ret.push_string(s);
}
Err(_) => ret.push_null(),
}
Ok(())
}
// FS_WRITE(handle, content)
Syscall::FsWrite => {
let handle = expect_int(args, 0)? as u32;
let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? {
Value::String(s) => s.as_bytes().to_vec(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string content".into())),
};
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.write_file(path, &content) {
Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false),
}
Ok(())
}
// FS_CLOSE(handle)
Syscall::FsClose => {
let handle = expect_int(args, 0)? as u32;
self.open_files.remove(&handle);
ret.push_null();
Ok(())
}
// FS_LIST_DIR(path)
Syscall::FsListDir => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
};
match self.fs.list_dir(&path) {
Ok(entries) => {
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
ret.push_string(names.join(";"));
}
Err(_) => ret.push_null(),
}
Ok(())
}
// FS_EXISTS(path)
Syscall::FsExists => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
};
ret.push_bool(self.fs.exists(&path));
Ok(())
}
// FS_DELETE(path)
Syscall::FsDelete => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
};
match self.fs.delete(&path) {
Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false),
}
Ok(())
}
// --- Log Syscalls (0x5000) ---
// LOG_WRITE(level, msg)
Syscall::LogWrite => {
let level = expect_int(args, 0)?;
let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
};
self.syscall_log_write(level, 0, msg)?;
// void
Ok(())
}
// LOG_WRITE_TAG(level, tag, msg)
Syscall::LogWriteTag => {
let level = expect_int(args, 0)?;
let tag = expect_int(args, 1)? as u16;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
};
self.syscall_log_write(level, tag, msg)?;
// void
Ok(())
}
// --- Asset Syscalls ---
Syscall::AssetLoad => {
let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_id".into())),
};
let asset_type_val = expect_int(args, 1)? as u32;
let slot_index = expect_int(args, 2)? as usize;
let asset_type = match asset_type_val {
0 => BankType::TILES,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())),
};
let slot = SlotRef { asset_type, index: slot_index };
match hw.assets().load(&asset_id, slot) {
Ok(handle) => {
ret.push_int(handle as i64);
Ok(())
}
Err(e) => Err(VmFault::Panic(e)),
}
}
Syscall::AssetStatus => {
let handle = expect_int(args, 0)? as u32;
let status = hw.assets().status(handle);
let status_val = match status {
LoadStatus::PENDING => 0,
LoadStatus::LOADING => 1,
LoadStatus::READY => 2,
LoadStatus::COMMITTED => 3,
LoadStatus::CANCELED => 4,
LoadStatus::ERROR => 5,
};
ret.push_int(status_val);
Ok(())
}
Syscall::AssetCommit => {
let handle = expect_int(args, 0)? as u32;
hw.assets().commit(handle);
ret.push_null();
Ok(())
}
Syscall::AssetCancel => {
let handle = expect_int(args, 0)? as u32;
hw.assets().cancel(handle);
ret.push_null();
Ok(())
}
Syscall::BankInfo => {
let asset_type_val = expect_int(args, 0)? as u32;
let asset_type = match asset_type_val {
0 => BankType::TILES,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())),
};
let info = hw.assets().bank_info(asset_type);
let json = serde_json::to_string(&info).unwrap_or_default();
ret.push_string(json);
Ok(())
}
Syscall::BankSlotInfo => {
let asset_type_val = expect_int(args, 0)? as u32;
let slot_index = expect_int(args, 1)? as usize;
let asset_type = match asset_type_val {
0 => BankType::TILES,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())),
};
let slot = SlotRef { asset_type, index: slot_index };
let info = hw.assets().slot_info(slot);
let json = serde_json::to_string(&info).unwrap_or_default();
ret.push_string(json);
Ok(())
}
}
}
}