1441 lines
59 KiB
Rust
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(())
|
|
}
|
|
}
|
|
}
|
|
} |