Writing the NES enumerator has been an interesting exercise. There is a lot of opcodes and logic that needs to be implemented and while you can test each opcode in isolation, it doesn't provide any sense of security that you have is correct until you start stitching more pieces together.

How do I know that my logic holds up once we are a few thousand cycles into a program where we hit some edge case I didn't think about?

What is Nestest?

nestest.nes from what I can determine seems to be the holy grail of NES CPU testing. Written by Kevin Horton (kevtris), who is well known in the NES emulator community, created a test ROM that exhaustively checks every official (and many unofficial) instructions.

The ROM itself is only half to tool, the power lies in the Golden Log which is a text file generated by the cycle-accurate Nintendulator that lists the exact state of every register and flag after every single instruction.

My goal? Run my emulator and compare it to the Golden Log, line for line.

Setting Up The Test

After some digging around and reading on how to even load a ROM and run it. "Simulating NROM-128 Mirroring" Since nestest is a 16KB ROM but the NES CPU expects a 32KB cartridge address space, we must mirror the data. We copy the 16KB payload into both the lower bank ($8000) and the upper bank ($C000). This ensures that the Interrupt Vectors at $FFFC (which tell the CPU where to start) are present and pointing to the correct code.

// Mock Bus for testing
struct TestBus {
    memory: [u8; 65536],
}

impl TestBus {
    fn new() -> Self {
        TestBus {
            memory: [0; 65536],
        }
    }	
	/* Since nestest is a 16KB ROM but the NES CPU expects a 32KB cartridge address space, we must mirror the data. We copy the 16KB payload into both the lower bank ($8000) and the upper bank ($C000). This ensures that the Interrupt Vectors at $FFFC (which tell the CPU where to start) are present and pointing to the correct code.*/
    fn load_rom(&mut self, data: &[u8]) {
        // nestest.nes is a specialized ROM.
        // It maps $C000-$FFFF directly.
        // The file has a 16-byte iNES header we must skip.
        let rom_data = &data[16..16 + 16384];

        // Loop through every byte of the 16KB ROM data
        for (i, &byte) in rom_data.iter().enumerate() {
	        // Map to Upper Bank ($C000 - $FFFF)
            self.memory[0xC000 + i] = byte;
            // Map to Lower Bank ($8000 - $BFFF)
            self.memory[0x8000 + i] = byte;
        }
    }
}

impl Bus for TestBus {
    fn read(&mut self, addr: u16) -> u8 {
        self.memory[addr as usize]
    }
    fn write(&mut self, addr: u16, data: u8) {
        self.memory[addr as usize] = data;
    }
}

With that out of the way now we can read in the nestest.nes load it and run it.

fn main() -> io::Result<()> {
    let mut file = File::open("nestest.nes")?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;

    let mut bus = TestBus::new();
    bus.load_rom(&buffer);

    let mut cpu = CPU::new();

    // AUTOMATION START:
    // Ideally, the Reset Vector is at $FFFC -> $C004.
    // But for automation comparison with the log, we typically force PC to $C000.
    cpu.registers.program_counter = 0xC000;

    // Loop until we hit a known "finish" address or crash
    println!("==== Starting nestest ====");
    for i in 1..=8991 {
        println!("{}", cpu.trace());
        cpu.step(&mut bus);
    }
    println!("==== Nestest finished ====");
    Ok(())
}

Unoffical Opcodes

While I had implemented all the supported opcodes, it was kind of funny to watch this fail on so quickly.

C6BC A:AA X:97 Y:4E P:A5 SP:F8 CYC:12476
C6BD A:AA X:97 Y:4E P:EF SP:F9 CYC:12478

thread 'main' (483340) panicked at src/hardware/cpu/mod.rs:53:32:
Unknown opcode $04 at PC=$C6BD

$04 is a Zero Page NOP. The difference here from a regular NOP is that this takes 2 bytes. It fetches an operand from Zero Page and ignores it. Why? I need to read a little more and try to understand the benefit of this...

Anyway, after I implemented the rest of the NES opcodes I was able to execute this test.

cargo run --bin nestest > nestest.log

==== Starting nestest ====
C000 A:00 X:00 Y:00 P:24 SP:FD CYC:0
C5F5 A:00 X:00 Y:00 P:24 SP:FD CYC:3
C5F7 A:00 X:00 Y:00 P:26 SP:FD CYC:5
C5F9 A:00 X:00 Y:00 P:26 SP:FD CYC:8
...
EB2B A:36 X:FF Y:A6 P:67 SP:FB CYC:18195
==== Nestest finished ====

So I was at least able to process and run the ROM to completion.

Since the Golden Log is from an actual emulator with PPU and includes a disassembler. I need to find a way to see if I can match my output with the Gold Logs. If not ill have to revisit the Golden Log validation later on in the process. Maybe I should look into building a disassembler...