aboutsummaryrefslogtreecommitdiff
path: root/crates/runrom
diff options
context:
space:
mode:
Diffstat (limited to 'crates/runrom')
-rw-r--r--crates/runrom/Cargo.toml10
-rw-r--r--crates/runrom/README.md32
-rw-r--r--crates/runrom/src/main.rs225
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);
+ }
+ }
+}