diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/runrom/Cargo.toml | 10 | ||||
| -rw-r--r-- | crates/runrom/README.md | 32 | ||||
| -rw-r--r-- | crates/runrom/src/main.rs | 225 |
3 files changed, 267 insertions, 0 deletions
diff --git a/crates/runrom/Cargo.toml b/crates/runrom/Cargo.toml new file mode 100644 index 0000000..e1c70fc --- /dev/null +++ b/crates/runrom/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "runrom" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +header.workspace = true +vnf.workspace = true
\ No newline at end of file diff --git a/crates/runrom/README.md b/crates/runrom/README.md new file mode 100644 index 0000000..c5e7604 --- /dev/null +++ b/crates/runrom/README.md @@ -0,0 +1,32 @@ +This is yet another NES/Famicom emulator. Only this time around it's +specifically tailored to NES/Famicom developers, not players. + +First of all, the `vnf` library used for this binary exposes the virtual machine +with a proper interface. This way, you can run ROM files programmatically. Then, +`runrom` is just a wrapper on top of this library with a set of options that +toggle certain features from it. This is a nice thing to have if you don't need +to write very specific conditions with a tailored program. Second of all, +`runrom` runs with no graphics nor sound. That is, it runs headless. Thus, it +can be run on your testing infrastructure, so you can run continuous integration +on critical paths from your games. + +## Basic usage + +You can run a ROM file by simply: + +``` +$ runrom <your-game-path>/game.nes +``` + +This will display all of the instructions being run. By default it will run from +the reset vector. You can change that with the `-s/--start` option, which +accepts a 16-bit address from where to start execution. + +That being said, most of the times you want to test a specific function. For +that, you can toggle the `-f/--function` option, which tells `runrom` that the +address is just a function and, whenever a top-level `rts`/`rti` instruction is +found, then execution can be halted. + +Moreover, you may also find interesting the `-d/--dump-memory` option, which +will display a summary of memory addresses which have been updated along +execution, and some statistics about them. diff --git a/crates/runrom/src/main.rs b/crates/runrom/src/main.rs new file mode 100644 index 0000000..ab1ea83 --- /dev/null +++ b/crates/runrom/src/main.rs @@ -0,0 +1,225 @@ +use header::Header; +use std::fs::File; +use std::io::{ErrorKind, Read, Seek, SeekFrom}; +use vnf::{Machine, MemoryInitialValue, MemoryPolicy}; + +/// Version for this program. +const VERSION: &str = "0.1.0"; + +#[derive(Default)] +struct Args { + file: String, + start: Option<u16>, + assume_function: bool, + dump_memory: bool, +} + +fn print_help() { + println!("Run an NES/Famicom ROM to test its code under a set of conditions.\n"); + println!("usage: runrom [OPTIONS] <FILE>\n"); + println!("Options:"); + println!(" -d, --dump-memory\tShow the memory that has changed after a run."); + println!(" -f, --function\tRun the code by assuming it's a function."); + println!(" -h, --help\t\tPrint this message and quit."); + println!(" -s, --start\t\tAddress from where to start (default: reset vector)."); + println!(" -v, --version\t\tPrint version information."); + std::process::exit(0); +} + +// Print the given `message` and exit(1). +fn die(message: String) { + eprintln!("error: {message}"); + std::process::exit(1); +} + +fn parse_hex_digit(c: char) -> Result<u16, String> { + match c.to_digit(16) { + Some(val) => Ok(val as u16), + None => Err("cannot convert digit to hexadecimal".to_string()), + } +} + +fn parse_hex_argument(given: &str) -> Result<u16, String> { + // Skip a leading '$' character. + let arg = if given.starts_with('$') { + given.get(1..).unwrap_or("") + } else { + given + }; + let mut chars = arg.chars(); + + match arg.len() { + 0 => Err("you need to provide an address".to_string()), + 1 => Ok(parse_hex_digit(chars.next().unwrap())?), + 2 => Ok((parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + 3 => Ok((parse_hex_digit(chars.next().unwrap())? << 8) + + (parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + 4 => Ok((parse_hex_digit(chars.next().unwrap())? << 12) + + (parse_hex_digit(chars.next().unwrap())? << 8) + + (parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + _ => Err("hex literal is too big".to_string()), + } +} + +fn parse_arguments() -> Args { + let mut args = std::env::args(); + let mut res = Args::default(); + + // Skip command name. + args.next(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "-h" | "--help" => print_help(), + "-s" | "--start" => { + if res.start.is_some() { + die("do not specify the '-s/--start' flag twice".to_string()); + } + let Some(val) = args.next() else { + die("you need to specify a value for the -s/--start flag!".to_string()); + return res; + }; + match parse_hex_argument(&val) { + Ok(n) => res.start = Some(n), + Err(e) => die(e), + } + } + "-d" | "--dump-memory" => { + res.dump_memory = true; + } + "-f" | "--function" => { + res.assume_function = true; + } + "-v" | "--version" => { + println!("runrom {VERSION}"); + std::process::exit(0); + } + _ => { + if arg.starts_with('-') { + die(format!("don't know how to handle the '{arg}' flag")); + } + if !res.file.is_empty() { + die("cannot have multiple source files".to_string()); + } + res.file = arg; + } + } + } + + if res.file.is_empty() { + die("you need to specify the file to be run".to_string()); + } + + res +} + +// Given a ROM file identified by the `file` parameter, fetch the 16-bit address +// as pointed out by the reset vector. +fn start_from_reset_vector(file: &String) -> u16 { + // 1. Read the ROM header so we fetch the size of PRG ROM. + + let Ok(mut input) = File::open(file) else { + die(format!("failed to open the given file '{file}'")); + return 0; + }; + + let mut buf = vec![0u8; 0x10]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => die("malformed ROM file".to_string()), + _ => die(e.to_string()), + } + } + + let header = match Header::try_from(buf.as_slice()) { + Ok(h) => h, + Err(e) => { + die(e.to_string()); + return 0; + } + }; + + // 2. With a known PRG ROM size, fetch the two bytes pertaining to the reset + // vector. + + // The two bytes of the reset address are located as follows: + // 1. Skip the ROM header, guaranteed to be exactly 0x10 bytes long. + // 2. Go to the end of PRG ROM. + // 3. -6: NMI addres; -4: reset addres; -2: IRQ address. + let offset: u64 = (0x10 + (header.prg_rom_size * 16 * 1024) - 4) + .try_into() + .unwrap(); + + if input.seek(SeekFrom::Start(offset)).is_err() { + die("cannot peek into the ROM's reset address".to_string()); + return 0; + }; + let mut buf = [0u8; 0x02]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => die("malformed ROM file".to_string()), + _ => die(e.to_string()), + } + } + + ((buf[1] as u16) << 8) + buf[0] as u16 +} + +fn run(file: &String, start: u16, assume_function: bool, dump_memory: bool) -> Result<(), String> { + let mut machine = Machine::from( + file, + start, + #[allow(clippy::single_range_in_vec_init)] + MemoryPolicy { + initial_value: MemoryInitialValue::Fixed(0), + allowed_reads: vec![(0..0x800)], + allowed_writes: vec![(0..0x800)], + minimum_stack_value: 0, + }, + )?; + machine.verbose = true; + + if assume_function { + machine.run_function()?; + } else { + machine.until_address(0xFFFF)?; + } + + if dump_memory { + let mut title = false; + + for (idx, cell) in machine.ram.iter().enumerate() { + if cell.reads > 0 || cell.writes > 0 { + if !title { + println!("\n== Memory dump ==\n"); + title = true; + } + + println!( + "[${:X}] = ${:02X} [reads={}, writes={}]", + idx, cell.value, cell.reads, cell.writes + ); + } + } + } + + Ok(()) +} + +fn main() { + let args = parse_arguments(); + let start = match args.start { + Some(s) => s, + None => start_from_reset_vector(&args.file), + }; + + match run(&args.file, start, args.assume_function, args.dump_memory) { + Ok(m) => m, + Err(e) => { + die(e); + } + } +} |
