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, /// 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, // --- 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) -> 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) { 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 { 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 { 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 = 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(()) } } } }