Compare commits

...
7 Commits
10 changed files with 140 additions and 12 deletions
Generated
+1 -1
View File
@@ -2165,7 +2165,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "lemur" name = "lemur"
version = "0.8.2" version = "0.9.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atoi", "atoi",
+1 -1
View File
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur" repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false publish = false
license = "MIT" license = "MIT"
version = "0.8.2" version = "0.9.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+4
View File
@@ -18,6 +18,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}; };
builder builder
.include(Path::new("shrooms-vb-core/core")) .include(Path::new("shrooms-vb-core/core"))
.include(Path::new("shrooms-vb-core/util"))
.opt_level(opt_level) .opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing") .flag_if_supported("-fno-strict-aliasing")
.define("VB_LITTLE_ENDIAN", None) .define("VB_LITTLE_ENDIAN", None)
@@ -29,7 +30,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.define("VB_DIRECT_FRAME", "on_frame") .define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read") .define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write") .define("VB_DIRECT_WRITE", "on_write")
.define("VBU_REALLOC", "vbu_realloc_shim")
.file(Path::new("shrooms-vb-core/core/vb.c")) .file(Path::new("shrooms-vb-core/core/vb.c"))
.file(Path::new("shrooms-vb-core/util/isx.c"))
.file(Path::new("shrooms-vb-core/util/vbu.c"))
.compile("vb"); .compile("vb");
Ok(()) Ok(())
+1
View File
@@ -33,6 +33,7 @@ mod cart;
mod game_info; mod game_info;
mod inline_stack_map; mod inline_stack_map;
mod shrooms_vb_core; mod shrooms_vb_core;
mod shrooms_vb_util;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum SimId { pub enum SimId {
+11 -4
View File
@@ -7,7 +7,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use crate::emulator::{SimId, game_info::GameInfo}; use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx};
pub struct Cart { pub struct Cart {
pub file_path: PathBuf, pub file_path: PathBuf,
@@ -20,8 +20,9 @@ pub struct Cart {
impl Cart { impl Cart {
pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> { pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> {
let rom = fs::read(file_path)?; let rom = fs::read(file_path)?;
let (rom, info) = let (rom, info) = try_parse_isx(file_path, &rom)
try_parse_elf(file_path, &rom).unwrap_or_else(|| (rom, GameInfo::empty(file_path))); .or_else(|| try_parse_elf(file_path, &rom))
.unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
let mut sram_file = File::options() let mut sram_file = File::options()
.read(true) .read(true)
@@ -60,6 +61,12 @@ impl Cart {
} }
} }
fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
let rom = rom_from_isx(data)?;
let info = GameInfo::from_isx(file_path, data);
Some((rom, info))
}
fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> { fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
use object::read::elf::FileHeader; use object::read::elf::FileHeader;
let program = match object::FileKind::parse(data).ok()? { let program = match object::FileKind::parse(data).ok()? {
@@ -73,7 +80,7 @@ fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
} }
_ => return None, _ => return None,
}; };
let info = GameInfo::new(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path)); let info = GameInfo::from_elf(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path));
Some((program, info)) Some((program, info))
} }
+83 -3
View File
@@ -14,7 +14,7 @@ pub struct GameInfo {
} }
impl GameInfo { impl GameInfo {
pub fn new(file_path: &Path, input: &[u8]) -> Result<Self> { pub fn from_elf(file_path: &Path, input: &[u8]) -> Result<Self> {
let file = object::File::parse(input)?; let file = object::File::parse(input)?;
let (name, path) = name_and_path(file_path); let (name, path) = name_and_path(file_path);
@@ -52,6 +52,26 @@ impl GameInfo {
}) })
} }
pub fn from_isx(file_path: &Path, input: &[u8]) -> Self {
let (name, path) = name_and_path(file_path);
let symbols = extract_isx_symbols(input);
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id: DebugId::default(),
code_id: None,
arch: None,
symbol_table: symbols.map(|syms| Arc::new(SymbolTable::new(syms))),
};
let inline_stack_map = InlineStackMap::empty();
Self {
library_info,
inline_stack_map,
}
}
pub fn empty(file_path: &Path) -> Self { pub fn empty(file_path: &Path) -> Self {
let (name, path) = name_and_path(file_path); let (name, path) = name_and_path(file_path);
let library_info = LibraryInfo { let library_info = LibraryInfo {
@@ -115,6 +135,66 @@ fn build_inline_stack_map(file: object::File) -> Result<InlineStackMap> {
Ok(frames.build()) Ok(frames.build())
} }
fn extract_isx_symbols(input: &[u8]) -> Option<Vec<Symbol>> {
let mut syms = vec![];
let (_, mut buf) = input.split_at_checked(32)?;
while !buf.is_empty() {
let typ;
(typ, buf) = buf.split_first()?;
match typ {
0x11 => {
// Code (Virtual Boy)
(_, buf) = buf.split_at_checked(4)?;
let len_bytes;
(len_bytes, buf) = buf.split_first_chunk()?;
let len = u32::from_le_bytes(*len_bytes);
(_, buf) = buf.split_at_checked(len as usize)?;
}
0x13 => {
// Range (Virtual Boy)
let count_bytes;
(count_bytes, buf) = buf.split_first_chunk()?;
let count = u16::from_le_bytes(*count_bytes);
(_, buf) = buf.split_at_checked(count as usize * 9)?;
}
0x14 => {
// Symbol (Virtual Boy)
let count_bytes;
(count_bytes, buf) = buf.split_first_chunk()?;
let count = u16::from_le_bytes(*count_bytes);
for _ in 0..count {
let name_len;
(name_len, buf) = buf.split_first()?;
let name_bytes;
(name_bytes, buf) = buf.split_at_checked(*name_len as usize)?;
(_, buf) = buf.split_at_checked(2)?;
let address_bytes;
(address_bytes, buf) = buf.split_first_chunk()?;
let name_str = String::from_utf8_lossy(name_bytes);
let address = u32::from_le_bytes(*address_bytes);
syms.push(Symbol {
address: address & 0x07ffffff,
size: Some(4),
name: demangle_any(&name_str),
});
}
}
0x20..=0x22 => {
// System (undocumented)
let length_bytes;
(length_bytes, buf) = buf.split_first_chunk()?;
let length = u32::from_le_bytes(*length_bytes);
(_, buf) = buf.split_at_checked(length as usize)?;
}
_ => {
return None;
}
}
}
Some(syms)
}
type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>; type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
struct ParseContext<'a> { struct ParseContext<'a> {
@@ -173,8 +253,8 @@ fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode<Reader>) ->
let name = Arc::new(name); let name = Arc::new(name);
let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?; let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?;
while let Some(range) = ranges.next()? { while let Some(range) = ranges.next()? {
let start = range.begin as u32; let start = range.begin as u32 & 0x07ffffff;
let end = range.end as u32; let end = range.end as u32 & 0x07ffffff;
ctx.frames.add(start, end, name.clone()); ctx.frames.add(start, end, name.clone());
} }
} }
+5 -3
View File
@@ -261,7 +261,7 @@ extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int {
}; };
data.monitor.event = data.monitor.queued_event.take(); data.monitor.event = data.monitor.queued_event.take();
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc); data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc)); data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc & 0x07ffffff));
unsafe { vb_set_exception_callback(sim, None) }; unsafe { vb_set_exception_callback(sim, None) };
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() { if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
1 1
@@ -439,7 +439,9 @@ impl EventMonitor {
// JAL .+4 is how programs get r31 to a known value for indirect calls // JAL .+4 is how programs get r31 to a known value for indirect calls
// (which we detect later.) // (which we detect later.)
// Any other JAL is a function call. // Any other JAL is a function call.
return Some(SimEvent::Call(address.wrapping_add_signed(disp))); return Some(SimEvent::Call(
address.wrapping_add_signed(disp) & 0x07ffffff,
));
} }
} }
@@ -453,7 +455,7 @@ impl EventMonitor {
if r31 as u32 == address.wrapping_add(2) { if r31 as u32 == address.wrapping_add(2) {
// JMP anywhere else, if r31 points to after the JMP, is an indirect call // JMP anywhere else, if r31 points to after the JMP, is an indirect call
let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) }; let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
return Some(SimEvent::Call(target as u32)); return Some(SimEvent::Call(target as u32 & 0x07ffffff));
} }
} }
+31
View File
@@ -0,0 +1,31 @@
use std::ffi::c_void;
#[unsafe(no_mangle)]
unsafe fn vbu_realloc_shim(ptr: *mut c_void, new_size: usize) -> *mut c_void {
if !ptr.is_null() {
// not supporting proper realloc because it needs bookkeeping and it's unnecessary
return std::ptr::null_mut();
}
let allocation = vec![0u8; new_size].into_boxed_slice();
Box::into_raw(allocation).cast()
}
#[link(name = "vb")]
unsafe extern "C" {
#[link_name = "vbuFromISX"]
fn vbu_from_isx(bytes: *const c_void, length: usize, rom_length: *mut usize) -> *mut c_void;
}
pub fn rom_from_isx(bytes: &[u8]) -> Option<Vec<u8>> {
if !bytes.starts_with(b"ISX") {
return None;
}
let mut rom_length = 0;
let raw_rom = unsafe { vbu_from_isx(bytes.as_ptr().cast(), bytes.len(), &mut rom_length) };
if raw_rom.is_null() {
return None;
}
// SAFETY: the rom was allocated by vbu_realloc_shim, which created it from a Vec<u8>.
let rom = unsafe { Vec::from_raw_parts(raw_rom.cast(), rom_length, rom_length) };
Some(rom)
}
+2
View File
@@ -88,6 +88,7 @@ impl GameWindow {
Command::OpenRom => { Command::OpenRom => {
let rom = rfd::FileDialog::new() let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.add_filter("Executable files", &["", "elf", "isx"])
.pick_file(); .pick_file();
if let Some(path) = rom { if let Some(path) = rom {
self.client self.client
@@ -140,6 +141,7 @@ impl GameWindow {
{ {
let rom = rfd::FileDialog::new() let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.add_filter("Executable files", &["", "elf", "isx"])
.pick_file(); .pick_file();
if let Some(path) = rom { if let Some(path) = rom {
self.client self.client
+1
View File
@@ -65,6 +65,7 @@ impl ProfileWindow {
.save_file(); .save_file();
if let Some(path) = file { if let Some(path) = file {
let bytes = pollster::block_on(bytes_receiver)?; let bytes = pollster::block_on(bytes_receiver)?;
let _ = fs::remove_file(&path);
fs::write(&path, bytes)?; fs::write(&path, bytes)?;
Ok(Some(path.display().to_string())) Ok(Some(path.display().to_string()))
} else { } else {