use super::*; use crate::services::memcard::{MemcardSlotState, MemcardStatus}; use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef}; use prometeu_hal::cartridge::AppMode; use prometeu_hal::color::Color; use prometeu_hal::glyph::Glyph; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::sprite::Sprite; use prometeu_hal::syscalls::Syscall; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{ AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int, }; use std::sync::atomic::Ordering; impl VirtualMachineRuntime { 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(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(crate) fn get_color(&self, value: i64) -> Color { Color::from_raw(value as u16) } fn int_arg_to_usize_status(value: i64) -> Result { usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) } fn int_arg_to_i32_trap(value: i64, name: &str) -> Result { i32::try_from(value) .map_err(|_| VmFault::Trap(TRAP_OOB, format!("{name} value out of bounds"))) } fn int_arg_to_u8_status(value: i64) -> Result { u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) } fn int_arg_to_u16_status(value: i64) -> Result { u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) } } impl NativeInterface for VirtualMachineRuntime { fn syscall( &mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext, ) -> Result<(), VmFault> { self.atomic_telemetry.syscalls.fetch_add(1, Ordering::Relaxed); let syscall = Syscall::from_u32(id).ok_or_else(|| { VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id)) })?; match syscall { Syscall::SystemHasCart => { ret.push_bool(true); return Ok(()); } Syscall::SystemRunCart => return Ok(()), _ => {} } let hw = ctx.require_hw()?; match syscall { Syscall::SystemHasCart => unreachable!(), Syscall::SystemRunCart => unreachable!(), Syscall::GfxClear => { let color = self.get_color(expect_int(args, 0)?); hw.gfx_mut().clear(color); Ok(()) } 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 = self.get_color(expect_int(args, 4)?); hw.gfx_mut().fill_rect(x, y, w, h, color); Ok(()) } 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 = self.get_color(expect_int(args, 4)?); hw.gfx_mut().draw_line(x1, y1, x2, y2, color); Ok(()) } 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 = self.get_color(expect_int(args, 3)?); hw.gfx_mut().draw_circle(x, y, r, color); Ok(()) } 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 = self.get_color(expect_int(args, 3)?); let fill_color = self.get_color(expect_int(args, 4)?); hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); Ok(()) } 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 = self.get_color(expect_int(args, 4)?); let fill_color = self.get_color(expect_int(args, 5)?); hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); Ok(()) } Syscall::GfxDrawText => { let x = expect_int(args, 0)? as i32; let y = expect_int(args, 1)? as i32; let msg = expect_string(args, 2, "message")?; let color = self.get_color(expect_int(args, 3)?); hw.gfx_mut().draw_text(x, y, &msg, color); Ok(()) } Syscall::GfxClear565 => { let color_val = expect_int(args, 0)? as u32; if color_val > 0xFFFF { return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds".into())); } hw.gfx_mut().clear(Color::from_raw(color_val as u16)); Ok(()) } Syscall::ComposerBindScene => { let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) { Ok(id) => id, Err(status) => { ret.push_int(status as i64); return Ok(()); } }; let status = if hw.bind_scene(scene_bank_id) { ComposerOpStatus::Ok } else { ComposerOpStatus::SceneUnavailable }; ret.push_int(status as i64); Ok(()) } Syscall::ComposerUnbindScene => { hw.unbind_scene(); ret.push_int(ComposerOpStatus::Ok as i64); Ok(()) } Syscall::ComposerSetCamera => { let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?; let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?; hw.set_camera(x, y); Ok(()) } Syscall::ComposerEmitSprite => { let glyph_id = match Self::int_arg_to_u16_status(expect_int(args, 0)?) { Ok(value) => value, Err(status) => { ret.push_int(status as i64); return Ok(()); } }; let palette_id = match Self::int_arg_to_u8_status(expect_int(args, 1)?) { Ok(value) if value < 64 => value, _ => { ret.push_int(ComposerOpStatus::ArgRangeInvalid as i64); return Ok(()); } }; let x = Self::int_arg_to_i32_trap(expect_int(args, 2)?, "sprite x")?; let y = Self::int_arg_to_i32_trap(expect_int(args, 3)?, "sprite y")?; let layer = match Self::int_arg_to_u8_status(expect_int(args, 4)?) { Ok(value) if value < 4 => value, Ok(_) => { ret.push_int(ComposerOpStatus::LayerInvalid as i64); return Ok(()); } Err(status) => { ret.push_int(status as i64); return Ok(()); } }; let bank_id = match Self::int_arg_to_u8_status(expect_int(args, 5)?) { Ok(value) => value, Err(status) => { ret.push_int(status as i64); return Ok(()); } }; let flip_x = expect_bool(args, 6)?; let flip_y = expect_bool(args, 7)?; let priority = match Self::int_arg_to_u8_status(expect_int(args, 8)?) { Ok(value) => value, Err(status) => { ret.push_int(status as i64); return Ok(()); } }; if !hw.has_glyph_bank(bank_id as usize) { ret.push_int(ComposerOpStatus::BankInvalid as i64); return Ok(()); } let emitted = hw.emit_sprite(Sprite { glyph: Glyph { glyph_id, palette_id }, x, y, layer, bank_id, active: false, flip_x, flip_y, priority, }); let status = if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow }; ret.push_int(status as i64); Ok(()) } Syscall::AudioPlaySample => { let sample_id_raw = expect_int(args, 0)?; let voice_id_raw = expect_int(args, 1)?; let volume_raw = expect_int(args, 2)?; let pan_raw = expect_int(args, 3)?; let pitch = expect_number(args, 4, "pitch")?; if !(0..16).contains(&voice_id_raw) { ret.push_int(AudioOpStatus::VoiceInvalid as i64); return Ok(()); } if sample_id_raw < 0 || sample_id_raw > u16::MAX as i64 || !(0..=255).contains(&volume_raw) || !(0..=255).contains(&pan_raw) || !pitch.is_finite() || pitch <= 0.0 { ret.push_int(AudioOpStatus::ArgRangeInvalid as i64); return Ok(()); } let status = hw.audio_mut().play( 0, sample_id_raw as u16, voice_id_raw as usize, volume_raw as u8, pan_raw as u8, pitch, 0, prometeu_hal::LoopMode::Off, ); ret.push_int(status as i64); Ok(()) } Syscall::AudioPlay => { let bank_id = expect_int(args, 0)? as u8; let sample_id_raw = expect_int(args, 1)?; let voice_id_raw = expect_int(args, 2)?; let volume_raw = expect_int(args, 3)?; let pan_raw = expect_int(args, 4)?; let pitch = expect_number(args, 5, "pitch")?; let loop_mode = match expect_int(args, 6)? { 0 => prometeu_hal::LoopMode::Off, _ => prometeu_hal::LoopMode::On, }; if !(0..16).contains(&voice_id_raw) { ret.push_int(AudioOpStatus::VoiceInvalid as i64); return Ok(()); } if hw.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none() { ret.push_int(AudioOpStatus::BankInvalid as i64); return Ok(()); } if sample_id_raw < 0 || sample_id_raw > u16::MAX as i64 || !(0..=255).contains(&volume_raw) || !(0..=255).contains(&pan_raw) || !pitch.is_finite() || pitch <= 0.0 { ret.push_int(AudioOpStatus::ArgRangeInvalid as i64); return Ok(()); } let status = hw.audio_mut().play( bank_id, sample_id_raw as u16, voice_id_raw as usize, volume_raw as u8, pan_raw as u8, pitch, 0, loop_mode, ); ret.push_int(status as i64); Ok(()) } Syscall::FsOpen => { let path = expect_string(args, 0, "path")?; 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(()) } 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) => ret.push_string(String::from_utf8_lossy(&data).into_owned()), Err(_) => ret.push_null(), } Ok(()) } Syscall::FsWrite => { let handle = expect_int(args, 0)? as u32; let content = expect_string(args, 1, "content")?.into_bytes(); 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(()) } Syscall::FsClose => { self.open_files.remove(&(expect_int(args, 0)? as u32)); Ok(()) } Syscall::FsListDir => { let path = expect_string(args, 0, "path")?; 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(()) } Syscall::FsExists => { ret.push_bool(self.fs.exists(&expect_string(args, 0, "path")?)); Ok(()) } Syscall::FsDelete => { match self.fs.delete(&expect_string(args, 0, "path")?) { Ok(_) => ret.push_bool(true), Err(_) => ret.push_bool(false), } Ok(()) } Syscall::MemSlotCount => { if self.current_cartridge_app_mode != AppMode::Game { ret.push_int(MemcardStatus::AccessDenied as i64); ret.push_int(0); return Ok(()); } ret.push_int(MemcardStatus::Ok as i64); ret.push_int(self.memcard.slot_count() as i64); Ok(()) } Syscall::MemSlotStat => { let slot = expect_slot_index(args, 0)?; if self.current_cartridge_app_mode != AppMode::Game { ret.push_int(MemcardStatus::AccessDenied as i64); ret.push_int(MemcardSlotState::Empty as i64); ret.push_int(0); ret.push_int(0); ret.push_int(0); return Ok(()); } let stat = self.memcard.slot_stat(&self.fs, self.current_app_id, slot); let status = if stat.state == MemcardSlotState::Corrupt { MemcardStatus::Corrupt } else { MemcardStatus::Ok }; ret.push_int(status as i64); ret.push_int(stat.state as i64); ret.push_int(stat.used_bytes as i64); ret.push_int(stat.generation as i64); ret.push_int(stat.checksum as i64); Ok(()) } Syscall::MemSlotRead => { let slot = expect_slot_index(args, 0)?; let offset = expect_non_negative_usize(args, 1, "offset")?; let max_bytes = expect_non_negative_usize(args, 2, "max_bytes")?; if self.current_cartridge_app_mode != AppMode::Game { ret.push_int(MemcardStatus::AccessDenied as i64); ret.push_string(String::new()); ret.push_int(0); return Ok(()); } let read = self.memcard.slot_read(&self.fs, self.current_app_id, slot, offset, max_bytes); ret.push_int(read.status as i64); ret.push_string(hex_encode(&read.bytes)); ret.push_int(read.bytes_read as i64); Ok(()) } Syscall::MemSlotWrite => { let slot = expect_slot_index(args, 0)?; let offset = expect_non_negative_usize(args, 1, "offset")?; let payload_hex = expect_string(args, 2, "payload_hex")?; if self.current_cartridge_app_mode != AppMode::Game { ret.push_int(MemcardStatus::AccessDenied as i64); ret.push_int(0); return Ok(()); } let payload = hex_decode(&payload_hex)?; let write = self.memcard.slot_write(&self.fs, self.current_app_id, slot, offset, &payload); ret.push_int(write.status as i64); ret.push_int(write.bytes_written as i64); Ok(()) } Syscall::MemSlotCommit => { let slot = expect_slot_index(args, 0)?; if self.current_cartridge_app_mode != AppMode::Game { ret.push_int(MemcardStatus::AccessDenied as i64); return Ok(()); } let status = { let memcard = &mut self.memcard; let fs = &mut self.fs; memcard.slot_commit(fs, self.current_app_id, slot) }; ret.push_int(status as i64); Ok(()) } Syscall::MemSlotClear => { let slot = expect_slot_index(args, 0)?; if self.current_cartridge_app_mode != AppMode::Game { ret.push_int(MemcardStatus::AccessDenied as i64); return Ok(()); } let status = { let memcard = &mut self.memcard; let fs = &mut self.fs; memcard.slot_clear(fs, self.current_app_id, slot) }; ret.push_int(status as i64); Ok(()) } Syscall::LogWrite => { self.syscall_log_write( expect_int(args, 0)?, 0, expect_string(args, 1, "message")?, )?; Ok(()) } Syscall::LogWriteTag => { self.syscall_log_write( expect_int(args, 0)?, expect_int(args, 1)? as u16, expect_string(args, 2, "message")?, )?; Ok(()) } Syscall::AssetLoad => { let raw_asset_id = expect_int(args, 0)?; let asset_id = AssetId::try_from(raw_asset_id).map_err(|_| { VmFault::Trap(TRAP_TYPE, format!("asset_id out of i32 range: {}", raw_asset_id)) })?; let slot_index = expect_int(args, 1)? as usize; match hw.assets().load(asset_id, slot_index) { Ok(handle) => { ret.push_int(AssetOpStatus::Ok as i64); ret.push_int(handle as i64); Ok(()) } Err(status) => { ret.push_int(status as i64); ret.push_int(0); Ok(()) } } } Syscall::AssetStatus => { ret.push_int(hw.assets().status(expect_int(args, 0)? as u32) as i64); Ok(()) } Syscall::AssetCommit => { let status = hw.assets().commit(expect_int(args, 0)? as u32); ret.push_int(status as i64); Ok(()) } Syscall::AssetCancel => { let status = hw.assets().cancel(expect_int(args, 0)? as u32); ret.push_int(status as i64); Ok(()) } Syscall::BankInfo => { let asset_type = match expect_int(args, 0)? as u32 { 0 => BankType::GLYPH, 1 => BankType::SOUNDS, _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), }; let telemetry = hw .assets() .bank_telemetry() .into_iter() .find(|entry| entry.bank_type == asset_type) .unwrap_or(prometeu_hal::asset::BankTelemetry { bank_type: asset_type, used_slots: 0, total_slots: 0, }); ret.push_int(telemetry.used_slots as i64); ret.push_int(telemetry.total_slots as i64); Ok(()) } } } } fn expect_string(args: &[Value], index: usize, field: &str) -> Result { match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? { Value::String(value) => Ok(value.clone()), _ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected string {}", field))), } } fn expect_number(args: &[Value], index: usize, field: &str) -> Result { match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? { Value::Float(f) => Ok(*f), Value::Int32(i) => Ok(*i as f64), Value::Int64(i) => Ok(*i as f64), _ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected number for {}", field))), } } fn expect_slot_index(args: &[Value], index: usize) -> Result { let slot = expect_int(args, index)?; if !(0..32).contains(&slot) { return Err(VmFault::Trap(TRAP_OOB, format!("slot index out of bounds: {}", slot))); } Ok(slot as u8) } fn expect_non_negative_usize(args: &[Value], index: usize, field: &str) -> Result { let val = expect_int(args, index)?; if val < 0 { return Err(VmFault::Trap(TRAP_OOB, format!("{} must be non-negative", field))); } Ok(val as usize) } fn hex_encode(bytes: &[u8]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(bytes.len() * 2); for &b in bytes { out.push(HEX[(b >> 4) as usize] as char); out.push(HEX[(b & 0x0f) as usize] as char); } out } fn hex_decode(s: &str) -> Result, VmFault> { fn nibble(c: u8) -> Option { match c { b'0'..=b'9' => Some(c - b'0'), b'a'..=b'f' => Some(10 + c - b'a'), b'A'..=b'F' => Some(10 + c - b'A'), _ => None, } } let bytes = s.as_bytes(); if !bytes.len().is_multiple_of(2) { return Err(VmFault::Trap(TRAP_TYPE, "payload_hex must have even length".to_string())); } let mut out = Vec::with_capacity(bytes.len() / 2); let mut i = 0usize; while i < bytes.len() { let hi = nibble(bytes[i]).ok_or_else(|| { VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string()) })?; let lo = nibble(bytes[i + 1]).ok_or_else(|| { VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string()) })?; out.push((hi << 4) | lo); i += 2; } Ok(out) }