aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-08-17 16:03:21 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-08-17 23:06:23 +0200
commitdf1041d40431251e22e03ec5c4df7fc5767e1543 (patch)
tree182f6b4f45a1814c7a0b1edbc7ee4276fc4f60bc /lib
parentf367d25f0d9d7f0e7580a47294f792143fbb6784 (diff)
downloadtools.nes-df1041d40431251e22e03ec5c4df7fc5767e1543.tar.gz
tools.nes-df1041d40431251e22e03ec5c4df7fc5767e1543.zip
Add the runrom crate and the vnf library
The vnf library supports the runrom crate and they both combined enable users to "run" a ROM file. This is basically an emulator, but with two key differences: 1. It is to be run programatically. That is, you are not expected to play games with this, but to run code by steps, start at a given address, run a function, etc. 2. It is headless: there are no graphics displayed on screen, nor sound being delivered. Thus, the target for both these things are developers, not players. This way developers can validate code paths without needing a full blown emulator. You can write tests with this and be able to run some checks as part of your testing infrastructure. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/vnf/Cargo.toml10
-rw-r--r--lib/vnf/src/lib.rs1178
-rw-r--r--lib/xixanta/src/opcodes.rs6
3 files changed, 1194 insertions, 0 deletions
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