aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml1
-rw-r--r--README.md9
-rw-r--r--crates/runrom/Cargo.toml10
-rw-r--r--crates/runrom/README.md32
-rw-r--r--crates/runrom/src/main.rs225
-rw-r--r--lib/vnf/Cargo.toml10
-rw-r--r--lib/vnf/src/lib.rs1178
-rw-r--r--lib/xixanta/src/opcodes.rs6
-rwxr-xr-xscripts/test-e2e.sh9
-rw-r--r--tests/expected/runrom/arithlog.txt58
-rw-r--r--tests/runrom/arithlog.s68
12 files changed, 1622 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b67e38f..ea29435 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,6 +23,22 @@ dependencies = [
]
[[package]]
+name = "runrom"
+version = "0.1.0"
+dependencies = [
+ "header",
+ "vnf",
+]
+
+[[package]]
+name = "vnf"
+version = "0.1.0"
+dependencies = [
+ "header",
+ "xixanta",
+]
+
+[[package]]
name = "xa65"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index a20253c..11a627c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"]
[workspace.dependencies]
xixanta = { path = "./lib/xixanta", version = "0.1.0" }
header = { path = "./lib/header", version = "0.1.0" }
+vnf = { path = "./lib/vnf", version = "0.1.0" }
[profile.release]
codegen-units = 1
diff --git a/README.md b/README.md
index 80f171d..8537eb7 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,15 @@ will invoke `nasm` with more pedantic features like its address sanitizer.
information about it. Read more about it in
[./crates/readrom/README.md](./crates/readrom/README.md).
+## `runrom`
+
+`runrom` is an NES/Famicom emulator that doesn't attempt to run a ROM
+graphically. Instead, it just runs code and exposes the data on memory,
+registers, etc. for a given run. Thus, `runrom` is a tool to run an NES/Famicom
+programatically, so developers can use it to test their ROM files under certain
+conditions. Read more about it in
+[./crates/runrom/README.md](./crates/runrom/README.md).
+
## License
This repository holds two licenses, as you can also note on the `Cargo.toml`
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);
+ }
+ }
+}
diff --git a/lib/vnf/Cargo.toml b/lib/vnf/Cargo.toml
new file mode 100644
index 0000000..66bed81
--- /dev/null
+++ b/lib/vnf/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "vnf"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+
+[dependencies]
+header.workspace = true
+xixanta.workspace = true
diff --git a/lib/vnf/src/lib.rs b/lib/vnf/src/lib.rs
new file mode 100644
index 0000000..d118157
--- /dev/null
+++ b/lib/vnf/src/lib.rs
@@ -0,0 +1,1178 @@
+use header::Header;
+use std::assert_matches;
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::{ErrorKind, Read};
+use std::ops::Range;
+use xixanta::opcodes::AddressingMode;
+use xixanta::opcodes::{Instruction, InstructionIdentifier, OPCODES};
+
+/// Values on the 'status' register converted to bools for easier use.
+#[derive(Debug)]
+pub struct StatusRegister {
+ pub negative: bool,
+ pub overflow: bool,
+ pub brk: bool,
+ pub decimal: bool,
+ pub interrupt: bool,
+ pub zero: bool,
+ pub carry: bool,
+}
+
+impl Default for StatusRegister {
+ fn default() -> Self {
+ Self {
+ negative: false,
+ overflow: false,
+ brk: false,
+ decimal: false,
+ interrupt: true,
+ zero: false,
+ carry: false,
+ }
+ }
+}
+
+impl StatusRegister {
+ /// Returns a string with the initial letter for each status bit that is
+ /// set. Otherwise, for unset bits, a '-' is given.
+ fn humanize(&self) -> String {
+ let mut res = String::from("");
+
+ if self.negative {
+ res.push('N');
+ } else {
+ res.push('-');
+ }
+ if self.overflow {
+ res.push('V');
+ } else {
+ res.push('-');
+ }
+ if self.brk {
+ res.push('B');
+ } else {
+ res.push('-');
+ }
+ if self.decimal {
+ res.push('D');
+ } else {
+ res.push('-');
+ }
+ if self.interrupt {
+ res.push('I');
+ } else {
+ res.push('-');
+ }
+ if self.zero {
+ res.push('Z');
+ } else {
+ res.push('-');
+ }
+ if self.carry {
+ res.push('C');
+ } else {
+ res.push('-');
+ }
+
+ res.to_string()
+ }
+}
+
+/// Registers from the APU chip.
+#[derive(Debug, Default)]
+pub struct APU {
+ pub dmc: u8,
+ pub frame_counter: u8,
+}
+
+/// Registers from the PPU chip.
+#[derive(Debug, Default)]
+pub struct PPU {
+ pub control: u8,
+ pub mask: u8,
+ pub status: u8,
+ pub scroll: u8,
+ pub address: u8,
+ pub data: u8,
+ pub oam_address: u8,
+ pub oam_dma: u8,
+}
+
+/// A byte from the memory, which other than the actual value, also contains
+/// different stats for it.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct MemoryCell {
+ /// The current value.
+ pub value: u8,
+
+ /// Are writes allowed?
+ pub write_allowed: bool,
+
+ /// Are reads allowed?
+ pub read_allowed: bool,
+
+ /// How many writes have happened?
+ pub writes: usize,
+
+ /// How many reads have happened?
+ pub reads: usize,
+}
+
+/// The initial value to be set for memory cells on a given execution.
+#[derive(Debug)]
+pub enum MemoryInitialValue {
+ Fixed(u8),
+ Random,
+}
+
+/// Allows users to define a policy for how the memory should be initialized for
+/// the given Machine.
+#[derive(Debug)]
+pub struct MemoryPolicy {
+ /// The initial value to be given for each cell.
+ pub initial_value: MemoryInitialValue,
+
+ /// The ranges for which reads are allowed to happen.
+ pub allowed_reads: Vec<Range<usize>>,
+
+ /// The ranges for which writes are allowed to happen.
+ pub allowed_writes: Vec<Range<usize>>,
+
+ /// How low can the stack go. Use this to control stack underflows.
+ pub minimum_stack_value: u8,
+}
+
+/// The state of the Joypad handshake process.
+#[derive(Copy, Clone, Debug, Default)]
+pub enum JoypadState {
+ #[default]
+ Waiting,
+ Received,
+ Sending,
+}
+
+/// The state of a Joypad.
+#[derive(Copy, Clone, Debug, Default)]
+pub struct Joypad {
+ pub state: JoypadState,
+ pub value: u8,
+ pub shift: u8,
+ pub reads: u8,
+}
+
+impl Joypad {
+ /// Initialize the Joypad so it's ready to accept reads.
+ pub fn prepare_for_reads(&mut self) {
+ // TODO: I still have to prepare a proper interface to interact with
+ // joypads.
+ self.value = 0;
+ self.shift = self.value;
+ self.reads = 0;
+ }
+}
+
+/// The core structure for the virtual machine. Use this structure to read and
+/// run a ROM file. At each step it will hold the current status of the machine
+/// so it can be inspected programatically in full detail.
+#[derive(Debug)]
+pub struct Machine {
+ /// Is the machine active at all?
+ active: bool,
+
+ /// Whether the machine is supposed to be running just a function (while
+ /// also going into inner calls). Hence, it will stop whenever an 'rts' or
+ /// 'rti' instruction is found at the top level.
+ run_function_mode: bool,
+
+ /// The PRG ROM pool of bytes.
+ pub prg_rom: Vec<u8>,
+
+ /// The advertised size of PRG ROM. That is, regardless of the length the
+ /// ->prg_rom, what's the supposed size of PRG ROM.
+ pub prg_rom_size: usize,
+
+ /// What is the instruction that is to be run.
+ pub current_instruction: Instruction,
+
+ /// The number of cycles that the CPU has consumed.
+ pub cycles: usize,
+
+ /// The extra cycles to be added to the ones inherent of the current
+ /// instruction.
+ extra_cycles: usize,
+
+ /// The extra cycles to be added as part of a page penalty.
+ pub page_penalty: usize,
+
+ /// Number of instructions that have been run so far.
+ pub instructions: usize,
+
+ /// The program counter register.
+ pub pc: usize,
+
+ /// Whether the program counter should be left untouched as the current
+ /// instruction already messed with it.
+ skip_pc: bool,
+
+ /// The status register.
+ pub status_register: StatusRegister,
+
+ /// The RAM for this machine. Each memory cell contains a value, but also
+ /// read/write policies and statistics.
+ pub ram: Vec<MemoryCell>,
+
+ /// The 'a' register.
+ pub a: u8,
+
+ /// The 'x' register.
+ pub x: u8,
+
+ /// The 'y' register.
+ pub y: u8,
+
+ /// The 's' register.
+ pub s: u8,
+
+ /// Status from the APU chip.
+ pub apu: APU,
+
+ /// Status from the PPU chip.
+ pub ppu: PPU,
+
+ /// Whether the run should be verbose, sending to standard output statistics
+ /// for each instruction being run, the stack, etc.
+ pub verbose: bool,
+
+ /// When 'verbose' is true, whether the APU should also be included into the
+ /// output. This is enabled/disabled automatically whenever the machine
+ /// detects a change on the APU.
+ should_report_apu: bool,
+
+ /// When 'verbose' is true, whether the PPU should also be included into the
+ /// output. This is enabled/disabled automatically whenever the machine
+ /// detects a change on the PPU.
+ should_report_ppu: bool,
+
+ /// The initial value for the stack register. Used to detect stack
+ /// under/over flows.
+ initial_stack_value: u8,
+
+ /// The memory policy for this machine. That is, the value to be used as the
+ /// default for each cell, which regions are allowed for read/writes, etc.
+ policy: MemoryPolicy,
+
+ /// The status of both Joypads.
+ joypads: [Joypad; 2],
+}
+
+// Returns a vector of MemoryCell representing the RAM for a Machine, which
+// follows the memory policy as defined in 'policy'.
+fn init_memory(policy: &MemoryPolicy) -> Vec<MemoryCell> {
+ let mut vec = Vec::with_capacity(0x800);
+
+ for i in 0..0x800 {
+ let read_allowed = policy.allowed_reads.iter().any(|range| range.contains(&i));
+ let write_allowed = policy.allowed_writes.iter().any(|range| range.contains(&i));
+
+ vec.push(MemoryCell {
+ value: match policy.initial_value {
+ MemoryInitialValue::Fixed(n) => n,
+ MemoryInitialValue::Random => todo!(),
+ },
+ write_allowed,
+ read_allowed,
+ reads: 0,
+ writes: 0,
+ });
+ }
+
+ vec
+}
+
+/// For a given u16 expression, return a tuple formatted like so:
+/// .0: The lower byte as u8.
+/// .1: Whether the expression is larger than 8 bits.
+macro_rules! u16_to_u8_with_carry {
+ ($val:expr) => {{
+ let low_byte = ($val & 0x00FF) as u8;
+ (low_byte, ($val & 0xFF00) != 0)
+ }};
+}
+
+impl Machine {
+ /// Initialize a Machine object by reading the ROM file located at
+ /// 'file'. The machine should be initialized to start from the 'start'
+ /// address, and the memory should be initialized with the given 'policy'.
+ pub fn from(file: &String, start: u16, policy: MemoryPolicy) -> Result<Self, String> {
+ let Ok(mut input) = File::open(file) else {
+ return Err(format!("failed to open the given file '{}'", file));
+ };
+
+ // Read the header in order to detect the PRG ROM size.
+ let mut buf = vec![0u8; 0x10];
+ if let Err(e) = input.read_exact(&mut buf) {
+ match e.kind() {
+ ErrorKind::UnexpectedEof => return Err("malformed ROM file".to_string()),
+ _ => return Err(e.to_string()),
+ }
+ }
+ let header = match Header::try_from(buf.as_slice()) {
+ Ok(h) => h,
+ Err(e) => return Err(e.to_string()),
+ };
+
+ let mut prg_rom = vec![0u8; header.prg_rom_size * 16 * 1024];
+ if let Err(e) = input.read_exact(&mut prg_rom) {
+ match e.kind() {
+ ErrorKind::UnexpectedEof => {
+ return Err("could not read advertised PRG ROM space".to_string());
+ }
+ _ => return Err(e.to_string()),
+ }
+ }
+
+ // TODO: allow for randomized initialization.
+ Ok(Self {
+ active: true,
+ run_function_mode: false,
+ prg_rom,
+ prg_rom_size: header.prg_rom_size,
+ pc: start as usize,
+ skip_pc: false,
+ cycles: 7, // NOTE: as per 6502 initialization process.
+ extra_cycles: 0,
+ page_penalty: 0,
+ instructions: 0,
+ current_instruction: Instruction {
+ identifier: InstructionIdentifier::Start,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 0,
+ opcode: 0,
+ size: 0,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ a: 0,
+ x: 0,
+ y: 0,
+ s: 0xFD, // NOTE: as per 6502 initialization process.
+ initial_stack_value: 0xFD,
+ ram: init_memory(&policy),
+ status_register: StatusRegister::default(),
+ apu: APU::default(),
+ ppu: PPU::default(),
+ verbose: false,
+ should_report_apu: false,
+ should_report_ppu: false,
+ policy,
+ joypads: [Joypad::default(); 2],
+ })
+ }
+
+ // Report to the standard output the current status of the machine.
+ fn report(&mut self) {
+ let space = if matches!(
+ self.current_instruction.identifier,
+ InstructionIdentifier::Unknown
+ ) {
+ "\t"
+ } else if matches!(
+ self.current_instruction.addressing_mode,
+ AddressingMode::Implied | AddressingMode::RelativeOrZeropage
+ ) {
+ "\t\t"
+ } else {
+ "\t"
+ };
+
+ let empty = HashMap::new();
+ println!(
+ "{}{}PC: ${:04X}, cycles: {}, registers: [a: ${:02X}, x: ${:02X}, y: ${:02X}, sp: ${:02X}], status: {}",
+ self.current_instruction
+ .to_human(self.pc, None, &empty, &empty),
+ space,
+ self.pc,
+ self.cycles,
+ self.a,
+ self.x,
+ self.y,
+ self.s,
+ self.status_register.humanize(),
+ );
+
+ if self.should_report_apu {
+ println!(
+ "\t\t[APU] DMC ${:X}, Frame counter ${:X}\n",
+ self.apu.dmc, self.apu.frame_counter
+ );
+ self.should_report_apu = false;
+ } else if self.should_report_ppu {
+ println!(
+ "\t\t[PPU] Control: ${:02X}, Mask: ${:02X}, Status: ${:02X}, Scroll: ${:02X}, Address: ${:02X}, Data: ${:02X}, OAM addr: ${:02X}, OAM DMA: ${:02X}\n",
+ self.ppu.control,
+ self.ppu.mask,
+ self.ppu.status,
+ self.ppu.scroll,
+ self.ppu.address,
+ self.ppu.data,
+ self.ppu.oam_address,
+ self.ppu.oam_dma,
+ );
+ self.should_report_ppu = false;
+ }
+
+ if !self.active {
+ println!("<end>");
+ }
+ }
+
+ // Read the joypad identified by 'id' (0 or 1).
+ fn joypad_read(&mut self, id: usize) -> Result<u8, String> {
+ assert_matches!(id, 0 | 1);
+ let jp = self.joypads.get_mut(id).unwrap();
+
+ match jp.state {
+ JoypadState::Waiting | JoypadState::Received => {
+ Err("joypad is not ready to send data!".to_string())
+ }
+ JoypadState::Sending => {
+ jp.reads += 1;
+ if jp.reads > 7 {
+ Err("too many reads for the joypad state".to_string())
+ } else {
+ let val = jp.shift & 0x01; // TODO: actually more bits are to be sent
+ jp.shift >>= 1;
+ Ok(val)
+ }
+ }
+ }
+ }
+
+ // Write to the joypad identified by 'id' (0 or 1) with the given 'value'.
+ fn joypad_write(&mut self, id: usize, value: u8) -> Result<(), String> {
+ assert_matches!(id, 0 | 1);
+ let jp = self.joypads.get_mut(id).unwrap();
+
+ match jp.state {
+ JoypadState::Waiting => {
+ if value != 1 {
+ // NOTE: if we are writing on joypad 2, then there might
+ // be a conflict with the APU frame counter. If that's
+ // the case, then ignore this "error" and just return
+ // early. In any other case, a value != 1 is an error.
+ if id == 0 {
+ return Err(format!("expecting exacly a '1', '{}' received", value));
+ }
+ return Ok(());
+ }
+ jp.state = JoypadState::Received;
+ Ok(())
+ }
+ JoypadState::Received => {
+ if value != 0 {
+ return Err(format!("expecting exacly a '0', '{}' received", value));
+ }
+ jp.prepare_for_reads();
+ jp.state = JoypadState::Sending;
+ Ok(())
+ }
+ JoypadState::Sending => {
+ Err("writing into a controller while it's sending data".to_string())
+ }
+ }
+ }
+
+ // Tick the PPU after an instruction has been run.
+ fn next_ppu(&mut self) -> Result<(), String> {
+ self.ppu.status = 0x80;
+
+ Ok(())
+ }
+
+ /// Step the execution of the machine by one instruction.
+ pub fn next_iteration(&mut self) -> Result<(), String> {
+ // Perform a new iteration of the PPU and the CPU.
+ self.next_ppu()?;
+ self.execute()?;
+
+ // Move the PC automatically unless the current instruction explicitely
+ // did so already.
+ if self.skip_pc {
+ self.skip_pc = false;
+ } else {
+ self.pc += self.current_instruction.size as usize;
+ }
+
+ // Sum up cycles and instructions.
+ self.instructions += 1;
+ self.cycles += self.current_instruction.cycles as usize;
+ if self.extra_cycles > 0 {
+ self.cycles += self.extra_cycles;
+ self.extra_cycles = 0;
+ }
+
+ // At this point we can already send a report of the current status of
+ // the machine.
+ if self.verbose {
+ self.report();
+ }
+
+ // After moving the PC, is it out of bounds?
+ if self.pc < 0x8000 {
+ return Err("out of bounds: program counter is pointing below ROM space".to_string());
+ }
+
+ // Fetch the next instruction.
+ let address = self.pc - 0x8000;
+ let opcode = self.prg_rom.get(address).unwrap();
+ self.current_instruction = match OPCODES.get(opcode) {
+ Some(instr) => instr.clone(),
+ None => {
+ return Err(format!(
+ "could not find instruction with opcode <{:02X}>",
+ opcode
+ ));
+ }
+ };
+
+ // Fetch the bytes for the current instruction.
+ match self.current_instruction.size {
+ 2 => {
+ self.current_instruction.bytes[0] = *self.prg_rom.get(address + 1).unwrap();
+ self.current_instruction.bytes[1] = 0;
+ }
+ 3 => {
+ self.current_instruction.bytes[0] = *self.prg_rom.get(address + 1).unwrap();
+ self.current_instruction.bytes[1] = *self.prg_rom.get(address + 2).unwrap();
+ }
+ _ => {
+ self.current_instruction.bytes = [0, 0];
+ }
+ };
+
+ Ok(())
+ }
+
+ /// Run a top-level function. That is, assume that the current 'start'
+ /// address is the start of a function, and keep on iterating the machine
+ /// until an 'rts'/'rti' instruction is found at the top-level (we still
+ /// allow inner calls).
+ pub fn run_function(&mut self) -> Result<(), String> {
+ self.run_function_mode = true;
+
+ while self.active {
+ self.next_iteration()?;
+ }
+
+ Ok(())
+ }
+
+ /// Run until the program counter reaches the given 'address'.
+ pub fn until_address(&mut self, address: u16) -> Result<(), String> {
+ while self.pc != address as usize {
+ self.next_iteration()?;
+ }
+
+ Ok(())
+ }
+
+ // Perform a read of the given memory 'address'.
+ fn read_memory(&mut self, address: u16) -> Result<u8, String> {
+ let cell = self.ram.get_mut(address as usize).unwrap();
+
+ if !cell.read_allowed {
+ return Err(format!(
+ "reading was not allowed on address '${:04X}'",
+ address
+ ));
+ }
+ cell.reads += 1;
+
+ Ok(cell.value)
+ }
+
+ // Perform a write to the given memory 'address' with the given 'value'.
+ fn write_memory(&mut self, address: u16, value: u8) -> Result<(), String> {
+ let cell = self.ram.get_mut(address as usize).unwrap();
+
+ if !cell.write_allowed {
+ return Err(format!(
+ "writing was not allowed on address '${:04X}'",
+ address
+ ));
+ }
+ cell.writes += 1;
+ cell.value = value;
+
+ Ok(())
+ }
+
+ // Print the current status of the stack.
+ fn put_stack(&mut self) {
+ print!("\t\t[STACK]: ");
+ if self.s == 0xFF {
+ println!("<empty>");
+ return;
+ }
+
+ for i in self.s + 1..=0xFF {
+ let addr = 0x200 + i as usize;
+ print!("{:02X} ", self.ram[addr].value);
+ }
+ println!();
+ }
+
+ // Push the given 'value' to the stack.
+ fn push_stack(&mut self, value: u8) -> Result<(), String> {
+ // Write the given value onto the stack.
+ let address = 0x200 + self.s as u16;
+ self.write_memory(address, value)?;
+
+ // And update the stack pointer if possible.
+ self.s -= 1;
+ if self.s == self.policy.minimum_stack_value {
+ return Err("stack underflow!".to_string());
+ }
+
+ if self.verbose {
+ self.put_stack();
+ }
+
+ Ok(())
+ }
+
+ // Pop the stack once and return the value that was found.
+ fn pop_stack(&mut self) -> Result<u8, String> {
+ if self.s == self.initial_stack_value {
+ return Err("stack overflow!".to_string());
+ }
+
+ self.s += 1;
+
+ if self.verbose {
+ self.put_stack();
+ }
+
+ let address = 0x200 + self.s as u16;
+ self.read_memory(address)
+ }
+
+ // Returns true of the stack is empty, false otherwise. Note that this
+ // just means that the value of the 's' register is the one set as its
+ // initial value.
+ fn is_stack_empty(&mut self) -> bool {
+ self.s == self.initial_stack_value
+ }
+
+ // Compare the given 'value' with the one from the current instruction. Then
+ // set the proper bits from the status register.
+ fn compare(&mut self, value: i16) -> Result<(), String> {
+ let res = value - self.current_instruction.value() as i16;
+
+ self.status_register.zero = res == 0;
+ self.status_register.negative = (res as u8 & 0x80) == 0x80;
+ self.status_register.carry = (res as u16 & 0xFF00) != 0;
+
+ Ok(())
+ }
+
+ /// Execute the current instruction.
+ pub fn execute(&mut self) -> Result<(), String> {
+ self.status_register.overflow = false;
+
+ match self.current_instruction.identifier {
+ // TODO
+ InstructionIdentifier::Brk => todo!(),
+ InstructionIdentifier::Bvc => todo!(),
+ InstructionIdentifier::Bvs => todo!(),
+ InstructionIdentifier::Pha => todo!(),
+ InstructionIdentifier::Pla => todo!(),
+ InstructionIdentifier::Php => todo!(),
+ InstructionIdentifier::Plp => todo!(),
+ InstructionIdentifier::Rti => todo!(),
+
+ // Flag instructions.
+ InstructionIdentifier::Sec => self.status_register.carry = true,
+ InstructionIdentifier::Clc => self.status_register.carry = false,
+ InstructionIdentifier::Sei => self.status_register.interrupt = true,
+ InstructionIdentifier::Cli => self.status_register.interrupt = false,
+ InstructionIdentifier::Sed => self.status_register.decimal = true,
+ InstructionIdentifier::Cld => self.status_register.decimal = false,
+ InstructionIdentifier::Clv => self.status_register.overflow = false,
+
+ // Arithmetic and logic.
+ InstructionIdentifier::Adc => {
+ let mut val = (self.load()? as u16) + self.a as u16;
+ if self.status_register.carry {
+ val += 1;
+ }
+ (self.a, self.status_register.carry) = u16_to_u8_with_carry!(val);
+
+ self.status_register.zero = self.a == 0;
+ self.status_register.negative = (self.a & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Sbc => {
+ let mut val = self.a as i16 - self.load()? as i16;
+ if !self.status_register.carry {
+ val -= 1;
+ }
+ (self.a, self.status_register.carry) = u16_to_u8_with_carry!(val as u16);
+
+ // The carry flag is set as an inverted borrow. Hence, whatever
+ // we got from the operation as a "regular 'adc'", then we
+ // invert it.
+ self.status_register.carry = !self.status_register.carry;
+
+ self.status_register.zero = self.a == 0;
+ self.status_register.negative = (self.a & 0x80) == 0x80;
+ }
+ InstructionIdentifier::And => {
+ let val = self.load()?;
+ self.a &= val;
+ self.status_register.zero = self.a == 0;
+ self.status_register.negative = (self.a & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Ora => {
+ let val = self.load()?;
+ self.a |= val;
+ self.status_register.zero = self.a == 0;
+ self.status_register.negative = (self.a & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Eor => {
+ let val = self.load()?;
+ self.a ^= val;
+ self.status_register.zero = self.a == 0;
+ self.status_register.negative = (self.a & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Inc => {
+ let val = ((self.load()? as u16 + 1) & 0x00FF) as u8;
+
+ self.store(val)?;
+ self.status_register.zero = val == 0;
+ self.status_register.negative = (val & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Inx => {
+ let val = ((self.x as u16 + 1) & 0x00FF) as u8;
+
+ self.x = val;
+ self.status_register.zero = val == 0;
+ self.status_register.negative = (val & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Iny => {
+ let val = ((self.y as u16 + 1) & 0x00FF) as u8;
+
+ self.y = val;
+ self.status_register.zero = val == 0;
+ self.status_register.negative = (val & 0x80) == 0x80;
+ }
+ InstructionIdentifier::Dec => {
+ let mut val = self.load()?;
+ if val == 0x00 {
+ self.store(0xFF)?;
+
+ self.status_register.zero = false;
+ self.status_register.negative = false;
+ } else {
+ val -= 1;
+ self.store(val)?;
+ self.status_register.zero = val == 0;
+ self.status_register.negative = (val & 0x80) == 0x80;
+ }
+ }
+ InstructionIdentifier::Dex => {
+ if self.x == 0x00 {
+ self.x = 0xFF;
+
+ self.status_register.zero = false;
+ self.status_register.negative = false;
+ } else {
+ self.x -= 1;
+ self.status_register.zero = self.x == 0;
+ self.status_register.negative = (self.x & 0x80) == 0x80;
+ }
+ }
+ InstructionIdentifier::Dey => {
+ if self.y == 0x00 {
+ self.y = 0xFF;
+
+ self.status_register.zero = false;
+ self.status_register.negative = false;
+ } else {
+ self.y -= 1;
+ self.status_register.zero = self.y == 0;
+ self.status_register.negative = (self.y & 0x80) == 0x80;
+ }
+ }
+ InstructionIdentifier::Asl => {
+ match self.current_instruction.addressing_mode {
+ AddressingMode::Implied => {
+ let val = (self.a as u16) << 1;
+ (self.a, self.status_register.carry) = u16_to_u8_with_carry!(val);
+ self.status_register.zero = self.a == 0;
+ self.status_register.negative = (val & 0x0080) == 0x0080;
+ }
+ _ => {
+ let val = (self.load()? as u16) << 1;
+ self.status_register.carry = (val & 0xFF00) != 0;
+ self.status_register.zero = val == 0;
+ self.store((val & 0x00FF) as u8)?;
+ self.status_register.negative = (val & 0x0080) == 0x0080;
+ }
+ };
+ }
+ InstructionIdentifier::Lsr => {
+ match self.current_instruction.addressing_mode {
+ AddressingMode::Implied => {
+ self.status_register.carry = (self.a & 0x1) == 0x1;
+ self.a >>= 1;
+ self.status_register.zero = self.a == 0;
+ }
+ _ => {
+ let mut val = self.load()? as u16;
+ self.status_register.carry = (val & 0x1) == 0x1;
+ val >>= 1;
+ self.status_register.zero = self.a == 0;
+ self.store(val as u8)?;
+ }
+ };
+ self.status_register.negative = false;
+ }
+ InstructionIdentifier::Ror => {
+ match self.current_instruction.addressing_mode {
+ AddressingMode::Implied => {
+ let carry = self.status_register.carry;
+ self.status_register.carry = (self.a & 0x1) == 0x1;
+ self.a >>= 1;
+ if carry {
+ self.a |= 0x80;
+ }
+ self.status_register.zero = self.a == 0;
+ }
+ _ => {
+ let mut val = self.load()? as usize;
+ let carry = self.status_register.carry;
+ self.status_register.carry = (val & 0x1) == 0x1;
+ val >>= 1;
+ if carry {
+ val |= 0x80;
+ }
+ self.status_register.zero = self.a == 0;
+ self.store(val as u8)?;
+ }
+ };
+ self.status_register.negative = false;
+ }
+ InstructionIdentifier::Rol => {
+ match self.current_instruction.addressing_mode {
+ AddressingMode::Implied => {
+ let carry = self.status_register.carry;
+ self.status_register.carry = (self.a & 0x80) == 0x80;
+ self.a <<= 1;
+ if carry {
+ self.a |= 0x01;
+ }
+ self.status_register.zero = self.a == 0;
+ }
+ _ => {
+ let mut val = self.load()? as usize;
+ let carry = self.status_register.carry;
+ self.status_register.carry = (val & 0x80) == 0x80;
+ val <<= 1;
+ if carry {
+ val |= 0x01;
+ }
+ self.status_register.zero = self.a == 0;
+ self.store(val as u8)?;
+ }
+ };
+ self.status_register.negative = false;
+ }
+
+ // Compare
+ InstructionIdentifier::Cmp => self.compare(self.a as i16)?,
+ InstructionIdentifier::Cpx => self.compare(self.x as i16)?,
+ InstructionIdentifier::Cpy => self.compare(self.y as i16)?,
+
+ // Load and Store
+ InstructionIdentifier::Lda => self.a = self.load()?,
+ InstructionIdentifier::Ldx => self.x = self.load()?,
+ InstructionIdentifier::Ldy => self.y = self.load()?,
+ InstructionIdentifier::Sta => self.store(self.a)?,
+ InstructionIdentifier::Stx => self.store(self.x)?,
+ InstructionIdentifier::Sty => self.store(self.y)?,
+
+ // Jump and branching.
+ InstructionIdentifier::Jsr => {
+ let address = self.current_instruction.value();
+ if !(0x8000..=0xFFFF).contains(&address) {
+ return Err("invalid jump!".to_string());
+ }
+
+ let next_address = self.pc + self.current_instruction.size as usize;
+ let low = (next_address as u16 & 0x00FF) as u8;
+ let high = ((next_address as u16 & 0xFF00) >> 8) as u8;
+
+ self.push_stack(high)?;
+ self.push_stack(low)?;
+
+ self.pc = address;
+ self.skip_pc = true;
+ }
+ InstructionIdentifier::Jmp => {
+ let address = self.current_instruction.value();
+ if !(0x8000..=0xFFFF).contains(&address) {
+ return Err("invalid jump!".to_string());
+ }
+
+ self.pc = address;
+ self.skip_pc = true;
+ }
+ InstructionIdentifier::Bcs => {
+ if self.status_register.carry {
+ self.branch();
+ }
+ }
+ InstructionIdentifier::Bcc => {
+ if !self.status_register.carry {
+ self.branch();
+ }
+ }
+ InstructionIdentifier::Beq => {
+ if self.status_register.zero {
+ self.branch();
+ }
+ }
+ InstructionIdentifier::Bne => {
+ if !self.status_register.zero {
+ self.branch();
+ }
+ }
+ InstructionIdentifier::Bpl => {
+ if !self.status_register.negative {
+ self.branch();
+ }
+ }
+ InstructionIdentifier::Bmi => {
+ if self.status_register.negative {
+ self.branch();
+ }
+ }
+ InstructionIdentifier::Rts => {
+ // If the stack is empty but we were just running a function,
+ // then assume that the machine is done.
+ if self.is_stack_empty() && self.run_function_mode {
+ if self.active {
+ self.active = false;
+ }
+ return Ok(());
+ }
+
+ // Pull the previous address from the stack and jump there. Note
+ // that we have to subtract the current instruction's size
+ // because it will be re-added after the call to `execute`.
+ let low = self.pop_stack()? as u16;
+ let high = (self.pop_stack()? as u16) << 8;
+ self.pc = (high + low) as usize;
+ self.skip_pc = true;
+ }
+
+ // transfer
+ InstructionIdentifier::Tax => self.x = self.a,
+ InstructionIdentifier::Tay => self.y = self.a,
+ InstructionIdentifier::Tsx => self.x = self.s,
+ InstructionIdentifier::Txa => self.a = self.x,
+ InstructionIdentifier::Txs => {
+ self.s = self.x;
+ self.initial_stack_value = self.x;
+ }
+ InstructionIdentifier::Tya => self.a = self.y,
+
+ // other
+ InstructionIdentifier::Bit => {
+ let val = self.load()?;
+ self.status_register.zero = (val & self.a) == 0;
+ self.status_register.negative = (val & 0x80) == 0x80;
+ self.status_register.overflow = (val & 0x40) == 0x40;
+ }
+
+ InstructionIdentifier::Start | InstructionIdentifier::Nop => {}
+ InstructionIdentifier::Unknown => {
+ return Err("found an unknown instruction!".to_string());
+ }
+ }
+
+ Ok(())
+ }
+
+ // Perform a branch instruction.
+ fn branch(&mut self) {
+ let val = self.current_instruction.value() as i8;
+ let next = if val > 0 {
+ self.pc + val as usize
+ } else {
+ self.pc - val.wrapping_neg() as usize
+ };
+
+ if (next & 0xFF00) == (self.pc & 0xFF00) {
+ self.extra_cycles += 2;
+ self.page_penalty += 1;
+ } else {
+ self.extra_cycles += 1;
+ }
+ self.pc = next;
+ // TODO
+ // self.skip_pc = true;
+ }
+
+ // Perform a load instruction and return the read value.
+ fn load(&mut self) -> Result<u8, String> {
+ let val = self.current_instruction.value();
+ let byte = if matches!(
+ self.current_instruction.addressing_mode,
+ AddressingMode::Immediate
+ ) {
+ val as u8
+ } else {
+ let address = self.target_address()?;
+
+ match address {
+ 0x2000 => self.ppu.control,
+ 0x2001 => self.ppu.mask,
+ 0x2002 => {
+ // TODO: also clear the address latch
+ let val = self.ppu.status;
+ self.ppu.status = 0x00;
+ val
+ }
+ 0x2003 => self.ppu.oam_address, // TODO: maybe read fault
+ 0x2005 => self.ppu.scroll,
+ 0x2006 => self.ppu.address,
+ 0x2007 => self.ppu.data,
+ 0x4010 => self.apu.dmc,
+ 0x4014 => self.ppu.oam_dma, // TODO: maybe read fault
+ 0x00..0x2000 => {
+ // NOTE: 0x0800 until 0x2000 are simply mirrors of the first
+ // 2KB. Let's mask out the upper bits.
+ let real = address & 0x07FF;
+ self.read_memory(real as u16)?
+ }
+ 0x8000..=0xFFFF => {
+ let real = address - 0x8000;
+ *self.prg_rom.get(real).unwrap()
+ }
+ 0x4016 => self.joypad_read(0)?,
+ // NOTE: the joypad 2 and the APU frame counter share the same
+ // address, but read is only reserved for joypad 2.
+ 0x4017 => self.joypad_read(1)?,
+ _ => todo!(),
+ }
+ };
+
+ // Set proper flags from the final value.
+ self.status_register.zero = byte == 0;
+ self.status_register.negative = (byte & 0x80) == 0x80;
+
+ Ok(byte)
+ }
+
+ // Returns the effective address which the current instruction is
+ // targetting.
+ fn target_address(&mut self) -> Result<usize, String> {
+ match self.current_instruction.addressing_mode {
+ AddressingMode::Absolute | AddressingMode::RelativeOrZeropage => {
+ Ok(self.current_instruction.value())
+ }
+ AddressingMode::ZeropageIndexedX | AddressingMode::IndexedX => {
+ Ok(self.current_instruction.value() + self.x as usize)
+ }
+ AddressingMode::ZeropageIndexedY | AddressingMode::IndexedY => {
+ Ok(self.current_instruction.value() + self.y as usize)
+ }
+ AddressingMode::IndirectY => {
+ let ptr = self.current_instruction.value() as u16;
+ let value =
+ self.read_memory(ptr)? as u16 + ((self.read_memory(ptr + 1)? as u16) << 8);
+ Ok(value as usize + self.y as usize)
+ }
+ _ => {
+ self.report();
+ todo!();
+ // Err("bad addressing mode".to_string())
+ }
+ }
+ }
+
+ // Perform a store instruction with the given 'value'.
+ fn store(&mut self, value: u8) -> Result<(), String> {
+ let address = self.target_address()?;
+
+ match address {
+ 0x2000 => {
+ self.ppu.control = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x2001 => {
+ self.ppu.mask = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x2002 => {
+ self.ppu.status = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x2003 => {
+ self.ppu.oam_address = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x2005 => {
+ self.ppu.scroll = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x2006 => {
+ self.ppu.address = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x2007 => {
+ self.ppu.data = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x4010 => {
+ self.apu.dmc = value;
+ self.should_report_apu = self.verbose;
+ }
+ 0x4014 => {
+ self.ppu.oam_dma = value;
+ self.should_report_ppu = self.verbose;
+ }
+ 0x4016 => self.joypad_write(0, value)?,
+ 0x4017 => {
+ // NOTE: a write on $4017 affects both the APU frame counter and
+ // the joypad 2 read sequence.
+
+ self.apu.frame_counter = value;
+ self.should_report_apu = self.verbose;
+
+ self.joypad_write(1, value)?;
+ }
+ 0x00..0x2000 => {
+ // NOTE: 0x0800 until 0x2000 are simply mirrors of the first
+ // 2KB. Let's mask out the upper bits.
+ let real = address & 0x07FF;
+ self.write_memory(real as u16, value)?;
+ }
+ _ => {
+ self.report();
+ todo!()
+ }
+ };
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ // use super::*;
+
+ // TODO
+}
diff --git a/lib/xixanta/src/opcodes.rs b/lib/xixanta/src/opcodes.rs
index d5bb97a..a296aa5 100644
--- a/lib/xixanta/src/opcodes.rs
+++ b/lib/xixanta/src/opcodes.rs
@@ -96,6 +96,10 @@ pub enum InstructionIdentifier {
Txa,
Txs,
Tya,
+
+ // Pseudo-instructions used by 'vnf'.
+ Start,
+ Unknown,
}
/// An entry to the 'INSTRUCTIONS' map, which holds some values relevant for
@@ -215,6 +219,8 @@ impl Instruction {
InstructionIdentifier::Txa => "txa",
InstructionIdentifier::Txs => "txs",
InstructionIdentifier::Tya => "tya",
+ InstructionIdentifier::Start => "<start>",
+ InstructionIdentifier::Unknown => "<unknown>",
};
// On most addressing modes, if the current value is actually found on
diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh
index 7546666..0620f25 100755
--- a/scripts/test-e2e.sh
+++ b/scripts/test-e2e.sh
@@ -267,6 +267,15 @@ rm tests/out/jetpac.NTSC.nes
rm -f tests/jetpac.nes/.nasm/addresses.txt
##
+# runrom
+
+echo "test: runrom => arithlog.nes"
+./target/debug/nasm -Werror -o tests/out/arithlog.nes tests/runrom/arithlog.s
+./target/debug/runrom --function --dump-memory tests/out/arithlog.nes > tests/out/arithlog.txt
+diff tests/out/arithlog.txt tests/expected/runrom/arithlog.txt
+exit_code=$((exit_code + $?))
+
+##
# Done!
exit $exit_code
diff --git a/tests/expected/runrom/arithlog.txt b/tests/expected/runrom/arithlog.txt
new file mode 100644
index 0000000..aa31569
--- /dev/null
+++ b/tests/expected/runrom/arithlog.txt
@@ -0,0 +1,58 @@
+<start> PC: $8000, cycles: 7, registers: [a: $00, x: $00, y: $00, sp: $FD], status: ----I--
+lda #$01 PC: $8002, cycles: 9, registers: [a: $01, x: $00, y: $00, sp: $FD], status: ----I--
+sta $00 PC: $8004, cycles: 12, registers: [a: $01, x: $00, y: $00, sp: $FD], status: ----I--
+sec PC: $8005, cycles: 14, registers: [a: $01, x: $00, y: $00, sp: $FD], status: ----I-C
+adc $00 PC: $8007, cycles: 17, registers: [a: $03, x: $00, y: $00, sp: $FD], status: ----I--
+ldx #$FF PC: $8009, cycles: 19, registers: [a: $03, x: $FF, y: $00, sp: $FD], status: N---I--
+inx PC: $800A, cycles: 21, registers: [a: $03, x: $00, y: $00, sp: $FD], status: ----IZ-
+inx PC: $800B, cycles: 23, registers: [a: $03, x: $01, y: $00, sp: $FD], status: ----I--
+sta $00, x PC: $800D, cycles: 27, registers: [a: $03, x: $01, y: $00, sp: $FD], status: ----I--
+clc PC: $800E, cycles: 29, registers: [a: $03, x: $01, y: $00, sp: $FD], status: ----I--
+adc $00 PC: $8010, cycles: 32, registers: [a: $04, x: $01, y: $00, sp: $FD], status: ----I--
+sta $02 PC: $8012, cycles: 35, registers: [a: $04, x: $01, y: $00, sp: $FD], status: ----I--
+txa PC: $8013, cycles: 37, registers: [a: $01, x: $01, y: $00, sp: $FD], status: ----I--
+sec PC: $8014, cycles: 39, registers: [a: $01, x: $01, y: $00, sp: $FD], status: ----I-C
+sbc $00 PC: $8016, cycles: 42, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC
+sta $03 PC: $8018, cycles: 45, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC
+sec PC: $8019, cycles: 47, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC
+rol $03 PC: $801B, cycles: 52, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ-
+sta $04 PC: $801D, cycles: 55, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ-
+sec PC: $801E, cycles: 57, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC
+ror $02 PC: $8020, cycles: 62, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ-
+sta $05 PC: $8022, cycles: 65, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ-
+asl PC: $8023, cycles: 67, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ-
+sta $06 PC: $8025, cycles: 70, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ-
+ldy #$01 PC: $8027, cycles: 72, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----I--
+tya PC: $8028, cycles: 74, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I--
+and $00, x PC: $802A, cycles: 78, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I--
+sta $07 PC: $802C, cycles: 81, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I--
+ora $00 PC: $802E, cycles: 84, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I--
+sta $08 PC: $8030, cycles: 87, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I--
+inc $00 PC: $8032, cycles: 92, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I--
+eor $00 PC: $8034, cycles: 95, registers: [a: $03, x: $01, y: $01, sp: $FD], status: ----I--
+sta $09 PC: $8036, cycles: 98, registers: [a: $03, x: $01, y: $01, sp: $FD], status: ----I--
+lda #$FF PC: $8038, cycles: 100, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I--
+clc PC: $8039, cycles: 102, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I--
+adc #$01 PC: $803B, cycles: 104, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC
+sta $0A PC: $803D, cycles: 107, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC
+lda #$00 PC: $803F, cycles: 109, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC
+sec PC: $8040, cycles: 111, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC
+sbc #$01 PC: $8042, cycles: 113, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I--
+sta $0B PC: $8044, cycles: 116, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I--
+rts PC: $8045, cycles: 122, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I--
+<end>
+
+== Memory dump ==
+
+[$0] = $02 [reads=6, writes=2]
+[$1] = $03 [reads=1, writes=1]
+[$2] = $82 [reads=1, writes=2]
+[$3] = $01 [reads=1, writes=2]
+[$4] = $00 [reads=0, writes=1]
+[$5] = $00 [reads=0, writes=1]
+[$6] = $00 [reads=0, writes=1]
+[$7] = $01 [reads=0, writes=1]
+[$8] = $01 [reads=0, writes=1]
+[$9] = $03 [reads=0, writes=1]
+[$A] = $00 [reads=0, writes=1]
+[$B] = $FF [reads=0, writes=1]
diff --git a/tests/runrom/arithlog.s b/tests/runrom/arithlog.s
new file mode 100644
index 0000000..99648cb
--- /dev/null
+++ b/tests/runrom/arithlog.s
@@ -0,0 +1,68 @@
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+ .byte $02, $01
+ .byte $00
+ .byte $00
+
+.segment "CHARS"
+.byte 0
+
+.segment "VECTORS"
+ .addr reset, reset, reset
+
+.segment "CODE"
+
+reset:
+ lda #1
+ sta $00
+
+ sec
+ adc $00
+ ldx #$FF
+ inx
+ inx
+ sta $00, x
+
+ clc
+ adc $00
+ sta $02
+
+ txa
+ sec
+ sbc $00
+ sta $03
+
+ sec
+ rol $03
+ sta $04
+
+ sec
+ ror $02
+ sta $05
+
+ asl
+ sta $06
+
+ ldy #1
+ tya
+ and $00, x
+ sta $07
+
+ ora $00
+ sta $08
+
+ inc $00
+ eor $00
+ sta $09
+
+ lda #$FF
+ clc
+ adc #1
+ sta $0A
+
+ lda #$00
+ sec
+ sbc #1
+ sta $0B
+
+ rts