2026-04-19 08:40:22 +01:00

648 lines
25 KiB
Rust

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, ComposerOpStatus> {
usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
fn int_arg_to_i32_trap(value: i64, name: &str) -> Result<i32, VmFault> {
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, ComposerOpStatus> {
u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
fn int_arg_to_u16_status(value: i64) -> Result<u16, ComposerOpStatus> {
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<String> = 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<String, VmFault> {
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<f64, VmFault> {
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<u8, VmFault> {
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<usize, VmFault> {
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<Vec<u8>, VmFault> {
fn nibble(c: u8) -> Option<u8> {
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)
}