"First" commit

This commit is contained in:
Henrik Persson 2022-09-27 21:23:42 +02:00
commit 61f57a85d9
63 changed files with 58740 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
.DS_Store
/legal-roms

12
.gitmodules vendored Normal file
View file

@ -0,0 +1,12 @@
[submodule "6502_65C02_functional_tests"]
path = test-roms/6502_65C02_functional_tests
url = git@github.com:amb5l/6502_65C02_functional_tests.git
[submodule "nes-test-roms"]
path = test-roms/nes-test-roms
url = git@github.com:christopherpow/nes-test-roms.git
[submodule "test-roms/nes-test-roms"]
path = test-roms/nes-test-roms
url = git@github.com:christopherpow/nes-test-roms.git
[submodule "test-roms/6502_65C02_functional_tests"]
path = test-roms/6502_65C02_functional_tests
url = git@github.com:amb5l/6502_65C02_functional_tests.git

303
Cargo.lock generated Normal file
View file

@ -0,0 +1,303 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "common"
version = "0.1.0"
[[package]]
name = "getch"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13990e2d5b29e1770ddf7fc000afead4acb9bd8f8a9602de63bf189e261b1ba8"
dependencies = [
"libc",
"termios",
]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
[[package]]
name = "mos6502"
version = "0.1.0"
dependencies = [
"common",
"getch",
]
[[package]]
name = "nes"
version = "0.1.0"
dependencies = [
"bitflags",
"common",
"mos6502",
]
[[package]]
name = "nes-sdl"
version = "0.1.0"
dependencies = [
"common",
"nes",
"sdl2",
"structopt",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sdl2"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
dependencies = [
"bitflags",
"lazy_static",
"libc",
"sdl2-sys",
]
[[package]]
name = "sdl2-sys"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0"
dependencies = [
"cfg-if",
"libc",
"version-compare",
]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termios"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
dependencies = [
"libc",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version-compare"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

5
Cargo.toml Normal file
View file

@ -0,0 +1,5 @@
[workspace]
members = ["common", "mos6502", "nes", "nes-sdl"]
[profile.test]
opt-level = 3 # Otherwise integration tests take too long

92
README.md Normal file
View file

@ -0,0 +1,92 @@
# 🥔 Potatis
<img width="500" alt="pm" src="screenshots/dk.png"><img width="500" alt="dk" src="screenshots/pm.png">
- `/mos6502` - Generic CPU emulator. Passes all tests, including illegal ops. (No BCD mode).
- `/nes` - A very incomplete NES emulator.
- `/nes-sdl` - Binary target that uses SDL for I/O.
## /mos6502
```rust
let load_base = 0x2000;
let mem = NonMappedMemory::load(&program[..], load_base);
let cpu = Cpu::new(Box::new(mem));
let mut machine = Mos6502::new(cpu);
loop {
machine.tick()
println!("{}", machine); // Will print nestest-like output
}
```
### Debugging
```rust
let mut debugger = machine.debugger();
debugger.verbose(true); // Trace, dumps disassembled instructions to stdout
debugger.add_breakpoint(Breakpoint::Address(0x0666));
debugger.add_breakpoint(Breakpoint::Opcode("RTI"));
debugger.watch_memory_range(0x6004..=0x6104, |mem| {
// Invoked when memory in range changes
});
```
## /nes ⚠️ WIP ⚠️
No scrolling support, so only non-scrolling games like Donkey Kong and Pac-Man "works". Still a lot of glitches, the PPU is a mess!!
Supported mappers:
- NROM (mapper 0)
- CNROM (mapper 3)
- MMC3 (mapper 4) (not complete)
```rust
impl nes::HostSystem for MyHost {
fn render(&mut self, frame: &RenderFrame) {
// frame.pixels() == 256 * 240 * 3 RGB array
}
fn poll_events(&mut self, joypad: &mut Joypad) {
// pump events and forward to joypad
}
}
let cart = Cartridge::blow_dust("path/to/rom.nes")?;
let mut nes = Nes::insert(cart, MyHost::new());
loop {
nes.tick();
println!("{:?}", nes); // Complete nestest formatted output
}
```
## /nes-sdl
`cargo run --release path/to/rom.nes`
`cargo run -- --help` for options
# Test
Run all unit and integration tests (for all crates):
`cargo test`
# TODO
- Fix PPU
- Implement scrolling
- More mappers
- WASM target
# Thanks
- https://www.masswerk.at/6502/6502_instruction_set.html
- https://github.com/amb5l/6502_65C02_functional_tests
- http://www.baltissen.org/newhtm/ttl6502.htm (TTL6502.bin test)
- https://www.nesdev.com/neshdr20.txt
- https://github.com/christopherpow/nes-test-roms
- http://nesdev.org/loopyppu.zip
- https://www.youtube.com/watch?v=-THeUXqR3zY
- https://archive.nes.science/nesdev-forums/f2/t664.xhtml

11
common/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[lib]
doctest = false

40
common/src/lib.rs Normal file
View file

@ -0,0 +1,40 @@
pub mod kilobytes {
pub const KB1: usize = 1024;
pub const KB2: usize = 2048;
pub const KB4: usize = 4096;
pub const KB8: usize = 8192;
pub const KB16: usize = 16384;
pub const KB32: usize = 32768;
}
pub mod utils {
pub fn parse_hex(src: &str) -> Result<u16, std::num::ParseIntError> {
u16::from_str_radix(src, 16)
}
}
#[allow(dead_code)]
pub mod bits {
pub const BIT0: usize = 1;
pub const BIT1: usize = 1 << 1;
pub const BIT2: usize = 1 << 2;
pub const BIT3: usize = 1 << 3;
pub const BIT4: usize = 1 << 4;
pub const BIT5: usize = 1 << 5;
pub const BIT6: usize = 1 << 6;
pub const BIT7: usize = 1 << 7;
pub fn is_signed(n: u8) -> bool {
n & (1 << 7) != 0
}
pub fn is_overflow(res: u8, lhs: u8, rhs: u8) -> bool {
if is_signed(lhs) && is_signed(rhs) && !is_signed(res) {
true
}
else {
!is_signed(lhs) && !is_signed(rhs) && is_signed(res)
}
}
}

13
mos6502/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "mos6502"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
getch = "0.3.1"
common = { path = "../common" }
[lib]
doctest = false

View file

@ -0,0 +1,94 @@
use crate::{cpu::{Cpu, Reg}, memory::Bus};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AddressMode {
Abs,
AbsX,
AbsY,
Imm,
Impl,
Ind,
IndX,
IndY,
Rel,
Zero,
ZeroX,
ZeroY,
Nop, // Not official.. used for dev
}
impl AddressMode {
pub fn resolve(&self, cpu: &mut Cpu, operands: &[u8], num_extra_cycles: usize) -> u16 {
// Guard for debug..
match self {
AddressMode::Imm => panic!("no need to resolve imm"),
AddressMode::Impl => panic!("no need to resolve impl"),
AddressMode::Rel => panic!("not sure but I think only branch opc uses relative addressing..."),
AddressMode::Nop => panic!("resolving nop"),
_ => ()
}
let mem = cpu.bus();
if self.is_zeropage() {
self.resolve_zeropage(cpu, operands[0], num_extra_cycles)
}
else {
let low = operands[0];
let high = operands[1];
let address: u16 = ((high as u16) << 8) | low as u16;
match self {
AddressMode::Abs => address,
AddressMode::AbsX => self.cycle_aware_add(cpu, address, cpu[Reg::X], num_extra_cycles),
AddressMode::AbsY => self.cycle_aware_add(cpu, address, cpu[Reg::Y], num_extra_cycles),
AddressMode::Ind => self.read16(mem, low, high),
_ => panic!()
}
}
}
fn resolve_zeropage(&self, cpu: &mut Cpu, operand: u8, likes_extra_cycles: usize) -> u16 {
// Zeropage indices should wrap!
// Casting everything to u16 here is safe because hi == 0x00 == zeropage!
match self {
AddressMode::IndX => self.read16(cpu.bus(), operand.wrapping_add(cpu[Reg::X]), 0x00), // Zeropage, no carry
AddressMode::IndY => {
let address = self.read16(cpu.bus(), operand, 0x00);
self.cycle_aware_add(cpu, address, cpu[Reg::Y], likes_extra_cycles)
}
AddressMode::Zero => operand as u16,
AddressMode::ZeroX => operand.wrapping_add(cpu[Reg::X]) as u16, // Zeropage
AddressMode::ZeroY => operand.wrapping_add(cpu[Reg::Y]) as u16, // zeropage
_ => panic!()
}
}
fn cycle_aware_add(&self, cpu: &mut Cpu, address: u16, v: u8, likes_extra_cycles: usize) -> u16 {
let res = address.wrapping_add(v as u16);
// println!("{:#06x} + after: {:#06x}", address, res);
if res & 0xff00 != address & 0xff00 {
// page cross
cpu.add_extra_cycles(likes_extra_cycles);
}
res
}
fn read16(&self, mem: &Box<dyn Bus>, address_low: u8, address_hi: u8) -> u16 {
let byte1_address = ((address_hi as u16) << 8) | address_low as u16;
let byte2_address = ((address_hi as u16) << 8) | address_low.wrapping_add(1) as u16;
let val_low = mem.read8(byte1_address) as u16;
let val_high = mem.read8(byte2_address) as u16;
(val_high << 8) | val_low
}
fn is_zeropage(&self) -> bool {
matches!(self,
AddressMode::IndX |
AddressMode::IndY |
AddressMode::Zero |
AddressMode::ZeroX |
AddressMode::ZeroY
)
}
}

853
mos6502/src/cpu.rs Normal file
View file

@ -0,0 +1,853 @@
use std::ops::{Index, IndexMut};
use crate::address_mode::AddressMode;
use crate::memory::Bus;
use crate::instructions::{Instruction, Opcode};
#[derive(Debug, Clone, Copy)]
pub enum Reg {
AC = 0,
X = 1,
Y = 2,
SP = 3
}
#[derive(Debug, Clone, Copy)]
pub enum Flag {
BUNUSEDMASK = 0b00110000,
N = 7, // Negative
V = 6, // Overflow
// These are only set when SR is pushed to stack by software. Should never be accessed by the CPU.
UNUSED = 5,
B = 4, // Break
D = 3, // Decimal (use BCD for arithmetics)
I = 2, // Interrupt (IRQ disable)
Z = 1, // Zero
C = 0, // Carry
}
pub struct Cpu {
pc: u16,
flags: [u8; 8], // All flags fit in one byte. But this is more readable. Or is it???
regs: [u8; 4],
mem: Box<dyn Bus>,
extra_cycles: usize,
}
impl Cpu {
// LIFO, top-down, 8 bit range, 0x0100 - 0x01FF
pub const STACK_TOP: usize = 0x0100;
pub const STACK_BOTTOM: usize = 0x01ff;
const NMI_VECTOR: u16 = 0xfffa;
const RESET_VECTOR: u16 = 0xfffc;
const IRQ_VECTOR: u16 = 0xfffe;
pub fn new(mem: Box<dyn Bus>) -> Self {
Self {
pc: 0,
flags: [0; 8],
regs: [0; 4],
mem,
extra_cycles: 0,
}
}
pub fn fetch_next_instruction(&mut self) -> Instruction {
let pc = self.pc();
let opbyte = self.mem.read8(pc);
let operand1 = self.mem.read8(pc + 1);
let operand2 = self.mem.read8(pc + 2);
// Decode
Instruction::disassemble(opbyte, operand1, operand2)
}
pub fn execute(&mut self, inst: &Instruction) -> usize {
let operands = &inst.operands();
let pc_before_exec = self.pc();
// println!("exec: {:?}", inst.opcode());
// println!("nmi: {:#06x}", mem.read16(Self::NMI_VECTOR));
// println!("reset: {:#06x}", mem.read16(Self::RESET_VECTOR));
// println!("irq/brk: {:#06x}", mem.read16(Self::IRQ_VECTOR));
self.extra_cycles = 0;
match inst.opcode() {
Opcode::JAM => panic!("jammed"), //println!("WARN!!!! JAMMED"), // TODO, Illegal opcode.. might be used somewhere as a nice HLT?
Opcode::NOP => {
// cycles on absX nops
if inst.mode() == AddressMode::AbsX {
let _ = inst.resolve_operand_value(self);
}
},
Opcode::DEX => self.dec_reg(Reg::X),
Opcode::DEY => self.dec_reg(Reg::Y),
Opcode::INX => self.inc_reg(Reg::X),
Opcode::INY => self.inc_reg(Reg::Y),
Opcode::DEC => {
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = val.wrapping_sub(1);
self.flags_set_neg_zero(res);
self.mem.write8(res, address);
}
Opcode::INC => {
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = val.wrapping_add(1);
self.flags_set_neg_zero(res);
self.mem.write8(res, address);
}
Opcode::DCP => {
// DEC oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = val.wrapping_sub(1);
self.mem.write8(res, address);
// CMP oper
self.cmp(Reg::AC, res);
}
Opcode::CLC => self[Flag::C] = 0,
Opcode::CLD => self[Flag::D] = 0,
Opcode::CLI => self[Flag::I] = 0,
Opcode::CLV => self[Flag::V] = 0,
Opcode::LDX => {
let res = inst.resolve_operand_value(self);
self[Reg::X] = res;
self.flags_set_neg_zero(res)
}
Opcode::LAX => {
let res = inst.resolve_operand_value(self);
self[Reg::AC] = res;
self[Reg::X] = res;
self.flags_set_neg_zero(res);
}
Opcode::SAX => {
let address = inst.resolve_operand_address(self);
let res = self[Reg::AC] & self[Reg::X];
self.mem.write8(res, address);
}
Opcode::TAX => self.mv_with_neg_zero(Reg::AC, Reg::X),
Opcode::TAY => self.mv_with_neg_zero(Reg::AC, Reg::Y),
Opcode::TSX => self.mv_with_neg_zero(Reg::SP, Reg::X),
Opcode::TXA => self.mv_with_neg_zero(Reg::X, Reg::AC),
Opcode::TXS => self[Reg::SP] = self[Reg::X],
Opcode::TYA => self.mv_with_neg_zero(Reg::Y, Reg::AC),
Opcode::SEC => self[Flag::C] = 1,
Opcode::SED => self[Flag::D] = 1,
Opcode::SEI => self[Flag::I] = 1,
Opcode::LDA => {
let res = inst.resolve_operand_value(self);
self[Reg::AC] = res;
self.flags_set_neg_zero(res)
}
Opcode::LDY => {
let res = inst.resolve_operand_value(self);
self[Reg::Y] = res;
self.flags_set_neg_zero(res)
}
Opcode::STA => {
let address = inst.resolve_operand_address(self);
self.mem.write8(self[Reg::AC], address);
}
Opcode::STX => {
let address = inst.resolve_operand_address(self);
self.mem.write8(self[Reg::X], address);
}
Opcode::STY => {
let address = inst.resolve_operand_address(self);
self.mem.write8(self[Reg::Y], address);
}
Opcode::JMP => {
let target = inst.resolve_operand_address(self);
self.set_pc(target);
}
Opcode::JSR => {
self.push_word(self.pc() + 2);
let target = inst.resolve_operand_address(self);
self.set_pc(target);
}
Opcode::RTS => {
let ret = self.pop_word();
self.set_pc(ret + 1); // pull PC, PC+1 -> PC
}
Opcode::BRK => {
self.push_word(self.pc() + 2);
let mut res = self.flags_as_byte();
res |= Flag::BUNUSEDMASK as u8; // break and 5 should always be set to 1 on stack
self.push(res);
self[Flag::I] = 1;
// Jump to IRQ vector, TODO cycles
self.set_pc(self.read16(Self::IRQ_VECTOR));
}
Opcode::RTI => {
let flags = self.pop();
self.set_flags_ignore_5_4(flags);
let ret = self.pop_word();
self.set_pc(ret);
}
Opcode::BNE => {
self.branch_if(operands[0], |cpu| cpu[Flag::Z] == 0);
}
Opcode::BEQ => {
self.branch_if(operands[0], |cpu| cpu[Flag::Z] == 1);
}
Opcode::BPL => {
self.branch_if(operands[0], |cpu| cpu[Flag::N] == 0);
}
Opcode::BMI => {
self.branch_if(operands[0], |cpu| cpu[Flag::N] == 1);
}
Opcode::BCC => {
self.branch_if(operands[0], |cpu| cpu[Flag::C] == 0);
}
Opcode::BCS => {
self.branch_if(operands[0], |cpu| cpu[Flag::C] == 1);
}
Opcode::BVC => {
self.branch_if(operands[0], |cpu| cpu[Flag::V] == 0);
}
Opcode::BVS => {
self.branch_if(operands[0], |cpu| cpu[Flag::V] == 1);
}
Opcode::CPY => {
let val = inst.resolve_operand_value(self);
self.cmp(Reg::Y, val);
}
Opcode::CPX => {
let val = inst.resolve_operand_value(self);
self.cmp(Reg::X, val);
}
Opcode::CMP => {
let val = inst.resolve_operand_value(self);
self.cmp(Reg::AC, val);
}
Opcode::SRE => {
// LSR oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.shift_right(val);
self.mem.write8(res, address);
// EOR oper
let res = self[Reg::AC] ^ res;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::LSR => {
match inst.mode() {
AddressMode::Impl => {
let res = self.shift_right(self[Reg::AC]);
self[Reg::AC] = res;
}
_ => {
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.shift_right(val);
self.mem.write8(res, address);
}
};
}
Opcode::SLO => {
// ASL oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.shift_left(val);
self.mem.write8(res, address);
// ORA oper
let res = self[Reg::AC] | res;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::RLA => {
// ROL oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.rotate_left(val);
self.mem.write8(res, address);
// AND oper
let res = self[Reg::AC] & res;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::RRA => {
// ROR oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.rotate_right(val);
self.mem.write8(res, address);
// ADC oper
self[Reg::AC] = self.add_with_carry(self[Reg::AC], res);
}
Opcode::ASL => {
match inst.mode() {
AddressMode::Impl => {
let res = self.shift_left(self[Reg::AC]);
self[Reg::AC] = res;
}
_ => {
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.shift_left(val);
self.mem.write8(res, address);
}
};
}
Opcode::ROL => {
match inst.mode() {
AddressMode::Impl => {
let res = self.rotate_left(self[Reg::AC]);
self[Reg::AC] = res;
}
_ => {
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.rotate_left(val);
self.mem.write8(res, address);
}
};
}
Opcode::ROR => {
match inst.mode() {
AddressMode::Impl => {
let res = self.rotate_right(self[Reg::AC]);
self[Reg::AC] = res;
}
_ => {
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.rotate_right(val);
self.mem.write8(res, address);
}
};
}
Opcode::ADC => {
let val = inst.resolve_operand_value(self);
self[Reg::AC] = self.add_with_carry(self[Reg::AC], val);
}
Opcode::SBC | Opcode::USBC => {
let val = inst.resolve_operand_value(self);
self[Reg::AC] = self.sub_with_borrow(self[Reg::AC], val);
}
Opcode::ISC => {
// cycles! should to as many cycles as ISC + INC + SBC LOOL
// INC oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = val.wrapping_add(1);
self.mem.write8(res, address);
// SBC oper
self[Reg::AC] = self.sub_with_borrow(self[Reg::AC], res);
// self.add_extra_cycles(10);
}
Opcode::EOR => {
let val = inst.resolve_operand_value(self);
let res = self[Reg::AC] ^ val;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::ORA => {
let val = inst.resolve_operand_value(self);
let res = self[Reg::AC] | val;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::AND => {
let val = inst.resolve_operand_value( self);
let res = self[Reg::AC] & val;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::PHA => {
self.push(self[Reg::AC]);
}
Opcode::PLA => {
let res = self.pop();
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
}
Opcode::PHX => self.push(self[Reg::X]),
Opcode::PHY => self.push(self[Reg::Y]),
Opcode::PLX => {
let res = self.pop();
self[Reg::X] = res;
self.flags_set_neg_zero(res);
}
Opcode::PLY => {
let res = self.pop();
self[Reg::Y] = res;
self.flags_set_neg_zero(res);
}
Opcode::PLP => {
let res = self.pop();
self.set_flags_ignore_5_4(res);
}
Opcode::PHP => {
let mut res = self.flags_as_byte();
res |= Flag::BUNUSEDMASK as u8; // break and 5 should always be set to 1 on stack
self.push(res);
}
Opcode::BIT => {
let val = inst.resolve_operand_value(self);
let res = self[Reg::AC] & val;
self[Flag::Z] = if res == 0 { 1 } else { 0 };
self[Flag::N] = if (val & (1 << 7)) == 0 { 0 } else { 1 };
self[Flag::V] = if (val & (1 << 6)) == 0 { 0 } else { 1 };
}
Opcode::ANC | Opcode::ANC2 => {
let val = inst.resolve_operand_value(self);
let res = self[Reg::AC] & val;
self[Flag::C] = common::bits::is_signed(res) as u8;
self.flags_set_neg_zero(res);
}
Opcode::ALR => {
let val = inst.resolve_operand_value(self);
let res = self[Reg::AC] & val;
self[Flag::C] = res & 1u8;
self.flags_set_neg_zero(res);
}
};
// No jmp; advance.
if pc_before_exec == self.pc() {
self.inc_pc(inst.size());
}
inst.cycles() + self.extra_cycles
}
pub fn add_extra_cycles(&mut self, cycles: usize) {
self.extra_cycles += cycles;
}
pub fn bus(&self) -> &Box<dyn Bus> {
&self.mem
}
pub fn pc(&self) -> u16 {
self.pc
}
pub fn set_pc(&mut self, pc: u16) {
self.pc = pc
}
pub fn inc_pc(&mut self, inc: u8) {
self.pc += inc as u16
}
pub fn reset(&mut self) {
// TODO: Cycles
self[Reg::AC] = 0;
self[Reg::X] = 0;
self[Reg::Y] = 0;
self[Reg::SP] = 0xfd;
self.flags = [0; 8];
self[Flag::UNUSED] = 1;
self[Flag::I] = 1;
let start = self.read16(Self::RESET_VECTOR);
println!("------------> RESET VECTOR: {:#06x}", start);
self.set_pc(start);
}
pub fn interrupt_nmi(&mut self) {
// TODO: Cycles
self.push_word(self.pc());
let mut stackflags = self.flags_as_byte();
stackflags &= 0b11101111; // B should be off
stackflags |= 0b00100000; // unused should be on
self.push(stackflags);
self[Flag::I] = 1;
// Jump to NMI vector, TODO cycles
let vector = self.read16(Self::NMI_VECTOR);
// println!("NMI interrupt -> {:#06x}", vector);
self.add_extra_cycles(2);
self.set_pc(vector);
}
fn push_word(&mut self, val: u16) {
let ret_high = (val >> 8) as u8;
let ret_low = (val & 0x00ff) as u8;
self.push(ret_high);
self.push(ret_low);
}
fn pop_word(&mut self) -> u16 {
let ret_low = self.pop();
let ret_high = self.pop();
(ret_high as u16) << 8 | ret_low as u16
}
fn inc_reg(&mut self, reg: Reg) {
let res = self[reg].wrapping_add(1);
self[reg] = res;
self.flags_set_neg_zero(res);
}
fn dec_reg(&mut self, reg: Reg) {
let res = self[reg].wrapping_sub(1);
self[reg] = res;
self.flags_set_neg_zero(res);
}
fn mv_with_neg_zero(&mut self, src: Reg, dst: Reg) {
let val = self[src];
self[dst] = val;
self.flags_set_neg_zero(val);
}
fn flags_set_neg_zero(&mut self, res: u8) {
self[Flag::Z] = (res == 0) as u8;
self[Flag::N] = (res & (1 << 7) != 0) as u8;
}
fn cmp(&mut self, reg: Reg, val: u8) {
let (res, overflow) = self[reg].overflowing_sub(val);
self[Flag::C] = !overflow as u8;
self.flags_set_neg_zero(res);
}
fn shift_right(&mut self, val: u8) -> u8 {
// All shift and rotate instructions preserve the bit shifted out in the carry flag.
self[Flag::C] = (val & 1 != 0) as u8;
let (res, _) = val.overflowing_shr(1);
self.flags_set_neg_zero(res);
res
}
fn shift_left(&mut self, val: u8) -> u8 {
// All shift and rotate instructions preserve the bit shifted out in the carry flag.
self[Flag::C] = (val & (1 << 7) != 0) as u8;
let (res, _) = val.overflowing_shl(1);
self.flags_set_neg_zero(res);
res
}
fn rotate_left(&mut self, val: u8) -> u8 {
// All shift and rotate instructions preserve the bit shifted out in the carry flag.
// rotate left (shifts in carry bit on the right)
let carry_bit_before_shift = self[Flag::C];
self[Flag::C] = (val & (1 << 7) != 0) as u8;
let (mut res, _) = val.overflowing_shl(1);
res |= carry_bit_before_shift;
self.flags_set_neg_zero(res);
res
}
fn rotate_right(&mut self, val: u8) -> u8 {
// All shift and rotate instructions preserve the bit shifted out in the carry flag.
// rotate right (shifts in CARRY bit on the left) (masswerk says zero bit but I think it's an error)
let carry_bit_before_shift = self[Flag::C];
self[Flag::C] = (val & 1 != 0) as u8;
let (mut res, _) = val.overflowing_shr(1);
res |= carry_bit_before_shift << 7;
self.flags_set_neg_zero(res);
res
}
fn calc_offset_pc(&self, offset: u8) -> u16 {
let signed = offset as i8;
if signed >= 0 {
let effective_offset = offset as u16;
self.pc.wrapping_add(effective_offset)
}
else {
let signed_offset = ((offset as u16) | 0xff00) as i16;
let effective_offset = (-signed_offset) as u16;
self.pc.wrapping_sub(effective_offset)
}
}
fn add_with_carry(&mut self, lhs: u8, rhs: u8) -> u8 {
if self[Flag::D] == 1 {
// panic!("implement decimal mode");
}
let (step1, carry1) = lhs.overflowing_add(self[Flag::C]);
let (res, carry2) = step1.overflowing_add(rhs);
self[Flag::V] = common::bits::is_overflow(res, lhs, rhs) as u8;
self[Flag::C] = (carry1 || carry2) as u8;
self.flags_set_neg_zero(res);
res
}
fn sub_with_borrow(&mut self, lhs: u8, rhs: u8) -> u8 {
// Do not understand how this works, but it works.
self.add_with_carry(lhs, rhs ^ 0xff)
}
fn push(&mut self, val: u8) {
let sp = self[Reg::SP] as usize;
let address = (Cpu::STACK_TOP + sp) as u16;
self.mem.write8(val, address);
self[Reg::SP] = self[Reg::SP].wrapping_sub(1);
}
fn pop(&mut self) -> u8 {
self[Reg::SP] = self[Reg::SP].wrapping_add(1);
let sp = self[Reg::SP] as usize;
let address = (Cpu::STACK_TOP + sp) as u16;
self.mem.read8(address)
}
fn set_flags_ignore_5_4(&mut self, val: u8) {
for bit in 0..=7usize {
match bit {
5 | 4 => (), // ignore break and 5
_ => self.flags[bit] = if val & (1 << bit) == 0 { 0 } else { 1 }
}
}
}
pub fn flags_as_byte(&self) -> u8 {
let mut res = 0x00;
for bit in 0..=7usize {
res |= self.flags[bit] << bit
}
res
}
fn branch_if(&mut self, offset: u8, cond: impl Fn(&Cpu) -> bool) {
if offset == 0 {
// (An offset of #0 corresponds to the immedately following address — or a rather odd and expensive NOP.)
return;
}
if cond(self) {
self.inc_pc(2);
let branch_target = self.calc_offset_pc(offset);
// if hi byte changes, we crossed a page boundary and should add extra cycles
// "add 1 to cycles if branch occurs on same page, add 2 to cycles if branch occurs to different page"
let crossed_page = self.pc() & 0xff00 != branch_target & 0xff00;
if crossed_page {
self.add_extra_cycles(2);
} else {
self.add_extra_cycles(1);
}
self.set_pc(branch_target);
}
}
fn read16(&self, address: u16) -> u16 {
let val_low = self.mem.read8(address) as u16;
let val_high = self.mem.read8(address + 1) as u16;
(val_high << 8) | val_low
}
}
impl Index<Reg> for Cpu {
type Output = u8;
fn index(&self, index: Reg) -> &u8 {
&self.regs[index as usize]
}
}
impl IndexMut<Reg> for Cpu {
fn index_mut(&mut self, index: Reg) -> &mut u8 {
&mut self.regs[index as usize]
}
}
impl Index<Flag> for Cpu {
type Output = u8;
fn index(&self, index: Flag) -> &u8 {
&self.flags[index as usize]
}
}
impl IndexMut<Flag> for Cpu {
fn index_mut(&mut self, index: Flag) -> &mut u8 {
&mut self.flags[index as usize]
}
}
impl std::fmt::Debug for Cpu {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn hexdec(val: u8) -> String {
format!("{:#04x} ({})", val, val)
}
write!(f, "--------\n")?;
write!(f, "pc:\t{:#06x}\nsp:\t{}\nacc:\t{}\nx:\t{}\ny:\t{}\n", self.pc, hexdec(self[Reg::SP]), hexdec(self[Reg::AC]), hexdec(self[Reg::X]), hexdec(self[Reg::Y]))?;
write!(f, "NEG={}, OVF={}, DEC={}, INT={}, ZER={}, CAR={} ({:#010b}) ({:#04x})", self[Flag::N], self[Flag::V], self[Flag::D], self[Flag::I], self[Flag::Z], self[Flag::C], self.flags_as_byte(), self.flags_as_byte())
}
}
impl std::fmt::Display for Cpu {
// nestest format
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X}", self[Reg::AC], self[Reg::X], self[Reg::Y], self.flags_as_byte(), self[Reg::SP])
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestBus([u8; 0xffff + 1]);
impl Bus for TestBus {
fn read8(&self, address: u16) -> u8 {
self.0[address as usize]
}
fn write8(&mut self, val: u8, address: u16) {
self.0[address as usize] = val;
}
}
fn sut() -> Cpu {
Cpu::new(Box::new(TestBus([0; 0xffff + 1])))
}
#[test]
fn stack_pop_push_should_wrap() {
let mut cpu = sut();
cpu[Reg::SP] = 0;
cpu.push(42);
assert_eq!(cpu[Reg::SP], 0xff);
let val = cpu.pop();
assert_eq!(val, 42);
assert_eq!(cpu[Reg::SP], 0);
}
#[test]
fn inc_dec_regs_should_wrap() {
let mut cpu = sut();
cpu[Reg::X] = 0xff;
cpu[Reg::Y] = 0xff;
cpu.inc_reg(Reg::X);
cpu.inc_reg(Reg::Y);
assert_eq!(cpu[Reg::X], 0);
assert_eq!(cpu[Reg::Y], 0);
cpu.dec_reg(Reg::X);
cpu.dec_reg(Reg::Y);
assert_eq!(cpu[Reg::X], 0xff);
assert_eq!(cpu[Reg::Y], 0xff);
}
#[test]
fn set_flags() {
let mut cpu = sut();
cpu.set_flags_ignore_5_4(0b10100001);
assert_eq!(cpu[Flag::N], 1);
assert_eq!(cpu[Flag::V], 0);
assert_eq!(cpu[Flag::D], 0);
assert_eq!(cpu[Flag::I], 0);
assert_eq!(cpu[Flag::Z], 0);
assert_eq!(cpu[Flag::C], 1);
let mut cpu = sut();
cpu.set_flags_ignore_5_4(0b11001010);
assert_eq!(cpu[Flag::N], 1);
assert_eq!(cpu[Flag::V], 1);
assert_eq!(cpu[Flag::D], 1);
assert_eq!(cpu[Flag::I], 0);
assert_eq!(cpu[Flag::Z], 1);
assert_eq!(cpu[Flag::C], 0);
}
#[test]
fn get_flags() {
let mut cpu = sut();
assert_eq!(cpu.flags_as_byte(), 0b00000000);
cpu[Flag::N] = 1;
cpu[Flag::Z] = 1;
assert_eq!(cpu.flags_as_byte(), 0b10000010);
}
#[test]
fn add_with_carry() {
let mut cpu = sut();
// -5 + -124
cpu.add_with_carry(0b11111011, 0b10000100);
assert_eq!(cpu[Flag::V], 1);
let mut cpu = sut();
cpu.add_with_carry(255, 1);
assert_eq!(cpu[Flag::V], 0);
assert_eq!(cpu[Flag::C], 1);
assert_eq!(cpu[Flag::Z], 1);
assert_eq!(cpu[Flag::N], 0);
let mut cpu = sut();
cpu.add_with_carry(254, 1);
assert_eq!(cpu[Flag::V], 0);
assert_eq!(cpu[Flag::C], 0);
assert_eq!(cpu[Flag::Z], 0);
assert_eq!(cpu[Flag::N], 1);
}
#[test]
fn cmp() {
let mut cpu = sut();
cpu[Reg::Y] = 10;
cpu.cmp(Reg::Y, 11);
assert_eq!(cpu[Flag::Z], 0);
assert_eq!(cpu[Flag::C], 0);
assert_eq!(cpu[Flag::N], 1);
cpu[Reg::Y] = 10;
cpu.cmp(Reg::Y, 10);
assert_eq!(cpu[Flag::Z], 1);
assert_eq!(cpu[Flag::C], 1);
assert_eq!(cpu[Flag::N], 0);
cpu[Reg::Y] = 11;
cpu.cmp(Reg::Y, 10);
assert_eq!(cpu[Flag::Z], 0);
assert_eq!(cpu[Flag::C], 1);
assert_eq!(cpu[Flag::N], 0);
}
#[test]
fn shift_right() {
let mut cpu = sut();
cpu.shift_right(0b001);
cpu[Flag::Z] = 1;
cpu[Flag::C] = 1;
cpu.shift_right(0b100);
cpu[Flag::Z] = 0;
cpu[Flag::C] = 0;
}
#[test]
fn offset_pc() {
let mut cpu = sut();
cpu.set_pc(0x10);
assert_eq!(cpu.calc_offset_pc(1), 0x11); // + 1
cpu.set_pc(0x20); // 32
assert_eq!(cpu.calc_offset_pc(0xf4), 0x14); // // -12 == 20
cpu.set_pc(0x0000);
assert_eq!(cpu.calc_offset_pc(255), 0xffff); // -1
cpu.set_pc(0xFFFF);
assert_eq!(cpu.calc_offset_pc(1), 0x0000); // +1
cpu.set_pc(0x0000);
assert_eq!(cpu.calc_offset_pc(0xc1), 0xffc1); // -63
}
}

241
mos6502/src/debugger.rs Normal file
View file

@ -0,0 +1,241 @@
use getch::Getch;
use std::{fmt::Write, collections::{VecDeque}, ops::RangeInclusive};
use crate::{cpu::{Cpu, Reg}, instructions::{Instruction, Opcode}};
const BACKTRACE_LIMIT: usize = 11;
pub struct Debugger {
stdin: Getch,
breakpoints: Vec<Breakpoint>,
last_pc: Option<u16>,
suspended: bool,
verbose: bool,
backtrace: VecDeque<BacktraceEntry>,
watches: Vec<Watch>,
}
struct BacktraceEntry {
inst: Instruction,
pc: u16,
opbyte: u8
}
#[derive(PartialEq, Eq)]
pub enum Breakpoint {
Address(u16),
Opcode(String),
OpcodeSequence(Vec<&'static str>)
// TODO add support to break on opcode WITH operands
}
enum Watch {
Range { address: RangeInclusive<u16>, state: Option<Vec<u8>>, f: Box<dyn Fn(Vec<u8>)> },
Address { address: u16, state: Option<u8>, f: Box<dyn Fn(u8)> },
// TODO: Reg, Flag, PC watches
}
impl Debugger {
pub fn new() -> Self {
Self {
stdin: Getch::new(),
breakpoints: Vec::with_capacity(2),
last_pc: None,
suspended: false,
verbose: false,
backtrace: VecDeque::with_capacity(BACKTRACE_LIMIT),
watches: Vec::new(),
}
}
pub fn verbose(&mut self, v: bool) {
self.verbose = v;
}
pub fn add_breakpoint(&mut self, breakpoint: Breakpoint) {
let mut breakpoint = breakpoint;
if let Breakpoint::Opcode(opstr) = &breakpoint {
breakpoint = Breakpoint::Opcode(opstr.to_uppercase());
}
self.breakpoints.push(breakpoint);
}
pub fn on_tick(&mut self, cpu: &Cpu, next_inst: &Instruction) {
let pc = cpu.pc();
let opbyte = cpu.bus().read8(pc);
self.backtrace.push_back(BacktraceEntry { inst: next_inst.clone(), pc, opbyte });
if self.backtrace.len() == BACKTRACE_LIMIT {
self.backtrace.remove(0);
}
if self.suspended || self.verbose {
Debugger::print_instruction(pc, opbyte, &next_inst);
}
self.check_watches(cpu);
if self.suspended {
self.user_input(cpu);
}
else if self.is_breakpoint(pc, next_inst.opcode()) {
self.suspend(cpu, pc);
}
self.last_pc = Some(pc);
}
fn is_breakpoint(&self, pc: u16, opcode: Opcode) -> bool {
for b in &self.breakpoints {
match b {
Breakpoint::Address(addr) => {
if *addr == pc {
return true
}
},
Breakpoint::Opcode(opstr) => {
if *opstr == opcode.to_string() {
return true;
}
},
Breakpoint::OpcodeSequence(seq) => {
let history: Vec<String> = self.backtrace.iter()
.rev()
.take(seq.len())
.map(|b| b.inst.opcode().to_string())
.collect();
let upper: Vec<String> = seq.iter()
.rev()
.map(|&s| s.to_uppercase())
.collect();
if history == upper {
return true;
}
},
}
}
false
}
pub fn watch_memory_range(&mut self, range: RangeInclusive<u16>, f: impl Fn(Vec<u8>) + 'static) {
let watch = Watch::Range { address: range, state: None, f: Box::new(f) };
self.watches.push(watch)
}
pub fn watch_memory(&mut self, address: u16, f: impl Fn(u8) + 'static) {
let watch = Watch::Address { address, state: None, f: Box::new(f) };
self.watches.push(watch)
}
fn check_watches(&mut self, cpu: &Cpu) {
for watch in self.watches.iter_mut() {
match watch {
Watch::Range { address, state, f } => {
// let start = *address.start() as usize;
let current_state: Vec<u8> = cpu.bus().read_range(address.clone());
if state.as_ref() != Some(&current_state) {
*state = Some(current_state.clone());
f(current_state);
}
}
Watch::Address { address, state, f } => {
let current_state = cpu.bus().read8(*address);
if *state != Some(current_state) {
*state = Some(current_state);
f(current_state);
}
}
}
}
}
pub fn dump_backtrace(&mut self) {
println!("...");
while let Some(entry) = self.backtrace.pop_front() {
Debugger::print_instruction(entry.pc, entry.opbyte, &entry.inst);
}
}
pub fn enable(&mut self) { // TODO: better API
self.dump_backtrace();
self.suspended = true;
}
fn suspend(&mut self, cpu: &Cpu, address: u16) {
self.suspended = true;
if !self.verbose {
// Print some instructions if we hit a break and we're not verbose already.
self.dump_backtrace();
}
println!("break at {:#06x}. step: <space>, cpu: <enter>, stack: <s>, continue: <c>, mute & continue: <m>", address);
self.user_input(cpu);
}
fn user_input(&mut self, cpu: &Cpu) {
let ch = self.stdin.getch().unwrap();
match ch {
0x20 => (), // Space, step
0x0a => { // Enter
println!("{:?}", cpu);
println!("{}", cpu);
self.user_input(cpu);
}
b'c' => {
println!("continuing...");
self.suspended = false;
}
b'm' => {
// TODO: Only mute current suspended address, not everything.
println!("continuing...");
self.suspended = false;
self.breakpoints.clear()
}
b's' => {
self.dump_stack(cpu);
self.user_input(cpu);
}
_ => {
println!("Unknown debugger command: {}", ch);
self.user_input(cpu);
}
}
}
fn dump_stack(&self, cpu: &Cpu) {
for a in Cpu::STACK_TOP..=Cpu::STACK_BOTTOM {
print!("{:#06x}: {:#04x}", a, cpu.bus().read8(a as u16));
if a as u8 == cpu[Reg::SP] {
print!(" <----");
}
println!();
}
}
fn print_instruction(pc: u16, opbyte: u8, inst: &Instruction) {
let mut pc_str = String::new();
write!(&mut pc_str, "{:#06x}", pc).unwrap();
let mut opbyte_str = String::new();
write!(&mut opbyte_str, "{:#04x}", opbyte).unwrap();
// TODO move to Into<String> for Instruction?
let mut operands_str = String::new();
for operand in inst.operands() {
write!(&mut operands_str, "{:#04x} ", operand).unwrap();
}
let mut mnemonic_str = String::new();
write!(&mut mnemonic_str, "{:?} {:?} {}", inst.opcode(), inst.mode(), operands_str).unwrap();
// let ascii_operand = if inst.mode() == AddressMode::Imm {
// String::from_utf8(vec![inst.operands()[0]]).unwrap_or_default()
// } else {
// String::new()
// };
// if ascii_operand.is_empty() {
println!("{:<10} {} {:<10} {}", pc_str, opbyte_str, operands_str, mnemonic_str);
// } else {
// println!("{:<10} {} {:<10} {} (b'{}')", pc_str, opbyte_str, operands_str, mnemonic_str, ascii_operand);
// }
}
}

447
mos6502/src/instructions.rs Normal file
View file

@ -0,0 +1,447 @@
use crate::{address_mode::AddressMode, cpu::Cpu};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[allow(clippy::upper_case_acronyms)]
pub enum Opcode {
ADC, // Add Memory to Accumulator with Carry
SBC, // Subtract Memory from Accumulator with Borrow
CLD, // Clear decimal mode
CLC, // Clear carry
CLI, // Clear interrupt
CLV, // Clear overflow
EOR, // Exclusive-OR Memory with Accumulator
AND, // AND Memory with Accumulator
ORA, // OR Memory with Accumulator
LDX,
LDA,
LDY,
TAX, // Transfer Accumulator to Index X
TAY, // Transfer Accumulator to Index Y
TSX, // Transfer Stack Pointer to Index X
TXA, // Transfer Index X to Accumulato
TXS,
TYA, // Transfer Index Y to Accumulator
STA, // Store Accumulator in Memory
STX, // Store Index X in Memory
STY, // Store Index Y in Memory
JMP,
JSR, // Jump to New Location Saving Return Address
RTS, // Return from Subroutine
BNE, // Branch on Result not Zero
BEQ, // Branch on Result Zero
BPL, // Branch on Result Plus
DEX, // Decrement Index X by One
DEY, // Decrement Index Y by One
DEC, // Decrement Memory by One
INC, // Increment Memory by One
INX,
INY,
BIT, // Test Bits in Memory with Accumulator
BCC, // Branch on Carry Clear
BCS, // Branch on Carry Set
BMI, // Branch on Result Minus
BVC, // Branch on Overflow Clear
BVS, // Branch on Overflow Set
SEC, // Set Carry Flag
SED, // Set Decimal Flag
SEI, // Set Interrupt Disable Status
NOP,
CPY, // Compare Memory and Index Y
CPX, // Compare Memory and Index X
LSR, // Shift One Bit Right (Memory or Accumulator)
ASL, // Shift Left One Bit (Memory or Accumulator)
ROL, // Rotate One Bit Left (Memory or Accumulator)
ROR, // Rotate One Bit Right (Memory or Accumulator)
CMP, // Compare Memory with Accumulator
PHA, // Push Accumulator on Stack
PLA, // Pull Accumulator from Stack,
PLP, // Pull Processor Status from Stack
PHP, // Push Processor Status on Stack,
JAM, // Halt, kill. Illegal, only used for own tests so far.
BRK, // Force Break, Software interrupt
RTI, // Return from Interrupt
// Illegal opcodes
LAX, // LDA oper + LDX oper
SAX, // A AND X -> M
USBC, // effectively same as normal SBC immediate, instr. E9.
DCP, // DEC oper + CMP oper
ISC, // INC oper + SBC oper
SLO, // ASL oper + ORA oper
RLA, // ROL oper + AND oper
SRE, // LSR oper + EOR oper
RRA, // ROR oper + ADC oper
PHX, // push x
PHY, // push y
PLX, // pull x
PLY, // pull y
ANC, // AND oper + set C as ASL
ANC2, // effectively the same as instr. 0B (ANC)
ALR, // AND oper + LSR
}
impl std::fmt::Display for Opcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Clone, Debug)]
pub struct Instruction {
opcode: Opcode,
mode: AddressMode,
cycles: usize,
operands: Vec<u8>,
size: u8,
}
impl Instruction {
fn implied(opcode: Opcode, cycles: usize) -> Instruction {
Instruction { opcode, cycles, mode: AddressMode::Impl, size: 1, operands: vec![] }
}
pub fn two(opcode: Opcode, cycles: usize, mode: AddressMode, operand: u8) -> Instruction {
Instruction { opcode, cycles, mode, size: 2, operands: vec![operand] }
}
fn thr(opcode: Opcode, cycles: usize, mode: AddressMode, operand1: u8, operand2: u8) -> Instruction {
Instruction { opcode, cycles, mode, size: 3, operands: vec![operand1, operand2] }
}
pub fn size(&self) -> u8 {
self.size
}
pub fn opcode(&self) -> Opcode {
self.opcode
}
pub fn mode(&self) -> AddressMode {
self.mode
}
pub fn cycles(&self) -> usize {
self.cycles
}
pub fn resolve_operand_value_and_address(&self, cpu: &mut Cpu) -> (u8, u16) {
let address = self.mode.resolve(cpu, self.operands(), self.num_extra_cycles());
let value = cpu.bus().read8(address);
(value, address)
}
pub fn resolve_operand_value(&self, cpu: &mut Cpu) -> u8 {
match self.mode {
AddressMode::Imm => self.operands[0],
_ => {
let address = self.mode.resolve(cpu, self.operands(), self.num_extra_cycles());
cpu.bus().read8(address)
}
}
}
pub fn resolve_operand_address(&self, cpu: &mut Cpu) -> u16 {
self.mode.resolve(cpu, self.operands(), self.num_extra_cycles())
}
pub fn operands(&self) -> &[u8] {
&self.operands[..]
}
fn num_extra_cycles(&self) -> usize {
match self.opcode {
// these instructions don't add a cycle when they cross page bounds
Opcode::DCP => 0,
Opcode::STA => 0,
Opcode::SLO => 0,
Opcode::RLA => 0,
Opcode::SRE => 0,
Opcode::RRA => 0,
// isc in indy apparently adds 4 cycles.. bc many instructions in one i guess
Opcode::ISC => {
match self.mode {
AddressMode::IndY => 4,
_ => 0,
}
}
_ => 1,
}
}
pub fn disassemble(opbyte: u8, operand1: u8, operand2: u8) -> Instruction {
match opbyte {
0x02 | 0x12 | 0x22 | 0x32 | 0x42 | 0x52 | 0x62 | 0x72 | 0x92 | 0xB2 | 0xD2 | 0xF2 => Instruction::implied(Opcode::JAM, 0),
0x00 => Instruction::implied(Opcode::BRK, 7),
0x40 => Instruction::implied(Opcode::RTI, 6),
0x38 => Instruction::implied(Opcode::SEC, 2),
0xf8 => Instruction::implied(Opcode::SED, 2),
0x78 => Instruction::implied(Opcode::SEI, 2),
0x18 => Instruction::implied(Opcode::CLC, 2),
0xd8 => Instruction::implied(Opcode::CLD, 2),
0x58 => Instruction::implied(Opcode::CLI, 2),
0xb8 => Instruction::implied(Opcode::CLV, 2),
0xaa => Instruction::implied(Opcode::TAX, 2),
0xa8 => Instruction::implied(Opcode::TAY, 2),
0xba => Instruction::implied(Opcode::TSX, 2),
0x8a => Instruction::implied(Opcode::TXA, 2),
0x9a => Instruction::implied(Opcode::TXS, 2),
0x98 => Instruction::implied(Opcode::TYA, 2),
0x24 => Instruction::two(Opcode::BIT, 3, AddressMode::Zero, operand1),
0x2c => Instruction::thr(Opcode::BIT, 4, AddressMode::Abs, operand1, operand2),
0x69 => Instruction::two(Opcode::ADC, 2, AddressMode::Imm, operand1),
0x65 => Instruction::two(Opcode::ADC, 3, AddressMode::Zero, operand1),
0x75 => Instruction::two(Opcode::ADC, 4, AddressMode::ZeroX, operand1),
0x6d => Instruction::thr(Opcode::ADC, 4, AddressMode::Abs, operand1, operand2),
0x7d => Instruction::thr(Opcode::ADC, 4, AddressMode::AbsX, operand1, operand2),
0x79 => Instruction::thr(Opcode::ADC, 4, AddressMode::AbsY, operand1, operand2),
0x61 => Instruction::two(Opcode::ADC, 6, AddressMode::IndX, operand1),
0x71 => Instruction::two(Opcode::ADC, 5, AddressMode::IndY, operand1),
0xe9 => Instruction::two(Opcode::SBC, 2, AddressMode::Imm, operand1),
0xe5 => Instruction::two(Opcode::SBC, 3, AddressMode::Zero, operand1),
0xf5 => Instruction::two(Opcode::SBC, 4, AddressMode::ZeroX, operand1),
0xed => Instruction::thr(Opcode::SBC, 4, AddressMode::Abs, operand1, operand2),
0xfd => Instruction::thr(Opcode::SBC, 4, AddressMode::AbsX, operand1, operand2),
0xf9 => Instruction::thr(Opcode::SBC, 4, AddressMode::AbsY, operand1, operand2),
0xe1 => Instruction::two(Opcode::SBC, 6, AddressMode::IndX, operand1),
0xf1 => Instruction::two(Opcode::SBC, 5, AddressMode::IndY, operand1),
0xa2 => Instruction::two(Opcode::LDX, 2, AddressMode::Imm, operand1),
0xa6 => Instruction::two(Opcode::LDX, 3, AddressMode::Zero, operand1),
0xb6 => Instruction::two(Opcode::LDX, 4, AddressMode::ZeroY, operand1),
0xae => Instruction::thr(Opcode::LDX, 4, AddressMode::Abs, operand1, operand2),
0xbe => Instruction::thr(Opcode::LDX, 4, AddressMode::AbsY, operand1, operand2),
0x49 => Instruction::two(Opcode::EOR, 2, AddressMode::Imm, operand1),
0x45 => Instruction::two(Opcode::EOR, 3, AddressMode::Zero, operand1),
0x55 => Instruction::two(Opcode::EOR, 4, AddressMode::ZeroX, operand1),
0x4d => Instruction::thr(Opcode::EOR, 4, AddressMode::Abs, operand1, operand2),
0x5d => Instruction::thr(Opcode::EOR, 4, AddressMode::AbsX, operand1, operand2),
0x59 => Instruction::thr(Opcode::EOR, 4, AddressMode::AbsY, operand1, operand2),
0x41 => Instruction::two(Opcode::EOR, 6, AddressMode::IndX, operand1),
0x51 => Instruction::two(Opcode::EOR, 5, AddressMode::IndY, operand1),
0x09 => Instruction::two(Opcode::ORA, 2, AddressMode::Imm, operand1),
0x05 => Instruction::two(Opcode::ORA, 3, AddressMode::Zero, operand1),
0x15 => Instruction::two(Opcode::ORA, 4, AddressMode::ZeroX, operand1),
0x0d => Instruction::thr(Opcode::ORA, 4, AddressMode::Abs, operand1, operand2),
0x1d => Instruction::thr(Opcode::ORA, 4, AddressMode::AbsX, operand1, operand2),
0x19 => Instruction::thr(Opcode::ORA, 4, AddressMode::AbsY, operand1, operand2),
0x01 => Instruction::two(Opcode::ORA, 6, AddressMode::IndX, operand1),
0x11 => Instruction::two(Opcode::ORA, 5, AddressMode::IndY, operand1),
0x29 => Instruction::two(Opcode::AND, 2, AddressMode::Imm, operand1),
0x25 => Instruction::two(Opcode::AND, 3, AddressMode::Zero, operand1),
0x35 => Instruction::two(Opcode::AND, 4, AddressMode::ZeroX, operand1),
0x2d => Instruction::thr(Opcode::AND, 4, AddressMode::Abs, operand1, operand2),
0x3d => Instruction::thr(Opcode::AND, 4, AddressMode::AbsX, operand1, operand2),
0x39 => Instruction::thr(Opcode::AND, 4, AddressMode::AbsY, operand1, operand2),
0x21 => Instruction::two(Opcode::AND, 6, AddressMode::IndX, operand1),
0x31 => Instruction::two(Opcode::AND, 5, AddressMode::IndY, operand1),
0xa0 => Instruction::two(Opcode::LDY, 2, AddressMode::Imm, operand1),
0xa4 => Instruction::two(Opcode::LDY, 3, AddressMode::Zero, operand1),
0xb4 => Instruction::two(Opcode::LDY, 4, AddressMode::ZeroX, operand1),
0xac => Instruction::thr(Opcode::LDY, 4, AddressMode::Abs, operand1, operand2),
0xbc => Instruction::thr(Opcode::LDY, 4, AddressMode::AbsX, operand1, operand2),
0xa9 => Instruction::two(Opcode::LDA, 2, AddressMode::Imm, operand1),
0xa5 => Instruction::two(Opcode::LDA, 3, AddressMode::Zero, operand1),
0xb5 => Instruction::two(Opcode::LDA, 4, AddressMode::ZeroX, operand1),
0xad => Instruction::thr(Opcode::LDA, 4, AddressMode::Abs, operand1, operand2),
0xbd => Instruction::thr(Opcode::LDA, 4, AddressMode::AbsX, operand1, operand2),
0xb9 => Instruction::thr(Opcode::LDA, 4, AddressMode::AbsY, operand1, operand2),
0xa1 => Instruction::two(Opcode::LDA, 6, AddressMode::IndX, operand1),
0xb1 => Instruction::two(Opcode::LDA, 5, AddressMode::IndY, operand1),
0x85 => Instruction::two(Opcode::STA, 3, AddressMode::Zero, operand1),
0x95 => Instruction::two(Opcode::STA, 4, AddressMode::ZeroX, operand1),
0x8d => Instruction::thr(Opcode::STA, 4, AddressMode::Abs, operand1, operand2),
0x9d => Instruction::thr(Opcode::STA, 5, AddressMode::AbsX, operand1, operand2),
0x99 => Instruction::thr(Opcode::STA, 5, AddressMode::AbsY, operand1, operand2),
0x81 => Instruction::two(Opcode::STA, 6, AddressMode::IndX, operand1),
0x91 => Instruction::two(Opcode::STA, 6, AddressMode::IndY, operand1),
0x86 => Instruction::two(Opcode::STX, 3, AddressMode::Zero, operand1),
0x96 => Instruction::two(Opcode::STX, 4, AddressMode::ZeroY, operand1),
0x8e => Instruction::thr(Opcode::STX, 4, AddressMode::Abs, operand1, operand2),
0x84 => Instruction::two(Opcode::STY, 3, AddressMode::Zero, operand1),
0x94 => Instruction::two(Opcode::STY, 4, AddressMode::ZeroX, operand1),
0x8c => Instruction::thr(Opcode::STY, 4, AddressMode::Abs, operand1, operand2),
0x4c => Instruction::thr(Opcode::JMP, 3, AddressMode::Abs, operand1, operand2),
0x6c => Instruction::thr(Opcode::JMP, 5, AddressMode::Ind, operand1, operand2),
0x20 => Instruction::thr(Opcode::JSR, 6, AddressMode::Abs, operand1, operand2),
0x60 => Instruction::implied(Opcode::RTS, 6),
0xd0 => Instruction::two(Opcode::BNE, 2, AddressMode::Rel, operand1),
0xf0 => Instruction::two(Opcode::BEQ, 2, AddressMode::Rel, operand1),
0x10 => Instruction::two(Opcode::BPL, 2, AddressMode::Rel, operand1),
0x90 => Instruction::two(Opcode::BCC, 2, AddressMode::Rel, operand1),
0xb0 => Instruction::two(Opcode::BCS, 2, AddressMode::Rel, operand1),
0x30 => Instruction::two(Opcode::BMI, 2, AddressMode::Rel, operand1),
0x50 => Instruction::two(Opcode::BVC, 2, AddressMode::Rel, operand1),
0x70 => Instruction::two(Opcode::BVS, 2, AddressMode::Rel, operand1),
0xca => Instruction::implied(Opcode::DEX, 2),
0x88 => Instruction::implied(Opcode::DEY, 2),
0xe8 => Instruction::implied(Opcode::INX, 2),
0xc8 => Instruction::implied(Opcode::INY, 2),
0xc6 => Instruction::two(Opcode::DEC, 5, AddressMode::Zero, operand1),
0xd6 => Instruction::two(Opcode::DEC, 6, AddressMode::ZeroX, operand1),
0xce => Instruction::thr(Opcode::DEC, 6, AddressMode::Abs, operand1, operand2),
0xde => Instruction::thr(Opcode::DEC, 7, AddressMode::AbsX, operand1, operand2),
0xe6 => Instruction::two(Opcode::INC, 5, AddressMode::Zero, operand1),
0xf6 => Instruction::two(Opcode::INC, 6, AddressMode::ZeroX, operand1),
0xee => Instruction::thr(Opcode::INC, 6, AddressMode::Abs, operand1, operand2),
0xfe => Instruction::thr(Opcode::INC, 7, AddressMode::AbsX, operand1, operand2),
// 1 byte NOPs
0xea | 0x1a | 0x3a => Instruction::implied(Opcode::NOP, 2),
// 2 byte NOPs 2 cycles
0x80 | 0x82 | 0x89 | 0xc2 | 0xe2 => Instruction::two(Opcode::NOP, 2, AddressMode::Nop, operand1),
// 2 byte NOPs 3 cycles
0x04 | 0x44 | 0x64 => Instruction::two(Opcode::NOP, 3, AddressMode::Nop, operand1),
// 2 byte NOPs 4 cycles
0x14 | 0x34 | 0x54 | 0x74 | 0xd4 | 0xf4 => Instruction::two(Opcode::NOP, 4, AddressMode::Nop, operand1),
// 3 byte nop
0x0c => Instruction::thr(Opcode::NOP, 4, AddressMode::Nop, operand1, operand2),
// 3 byte NOPs, absX for cycle
0x1c | 0x3c | 0x5c | 0x7c | 0xdc | 0xfc => Instruction::thr(Opcode::NOP, 4, AddressMode::AbsX, operand1, operand2),
0xc0 => Instruction::two(Opcode::CPY, 2, AddressMode::Imm, operand1),
0xc4 => Instruction::two(Opcode::CPY, 3, AddressMode::Zero, operand1),
0xcc => Instruction::thr(Opcode::CPY, 4, AddressMode::Abs, operand1, operand2),
0xe0 => Instruction::two(Opcode::CPX, 2, AddressMode::Imm, operand1),
0xe4 => Instruction::two(Opcode::CPX, 3, AddressMode::Zero, operand1),
0xec => Instruction::thr(Opcode::CPX, 4, AddressMode::Abs, operand1, operand2),
0xc9 => Instruction::two(Opcode::CMP, 2, AddressMode::Imm, operand1),
0xc5 => Instruction::two(Opcode::CMP, 3, AddressMode::Zero, operand1),
0xd5 => Instruction::two(Opcode::CMP, 4, AddressMode::ZeroX, operand1),
0xcd => Instruction::thr(Opcode::CMP, 4, AddressMode::Abs, operand1, operand2),
0xdd => Instruction::thr(Opcode::CMP, 4, AddressMode::AbsX, operand1, operand2),
0xd9 => Instruction::thr(Opcode::CMP, 4, AddressMode::AbsY, operand1, operand2),
0xc1 => Instruction::two(Opcode::CMP, 6, AddressMode::IndX, operand1),
0xd1 => Instruction::two(Opcode::CMP, 5, AddressMode::IndY, operand1),
0x4a => Instruction::implied(Opcode::LSR, 2),
0x46 => Instruction::two(Opcode::LSR, 5, AddressMode::Zero, operand1),
0x56 => Instruction::two(Opcode::LSR, 6, AddressMode::ZeroX, operand1),
0x4e => Instruction::thr(Opcode::LSR, 6, AddressMode::Abs, operand1, operand2),
0x5e => Instruction::thr(Opcode::LSR, 7, AddressMode::AbsX, operand1, operand2),
0x0a => Instruction::implied(Opcode::ASL, 2),
0x06 => Instruction::two(Opcode::ASL, 5, AddressMode::Zero, operand1),
0x16 => Instruction::two(Opcode::ASL, 6, AddressMode::ZeroX, operand1),
0x0e => Instruction::thr(Opcode::ASL, 6, AddressMode::Abs, operand1, operand2),
0x1e => Instruction::thr(Opcode::ASL, 7, AddressMode::AbsX, operand1, operand2),
0x2a => Instruction::implied(Opcode::ROL, 2),
0x26 => Instruction::two(Opcode::ROL, 5, AddressMode::Zero, operand1),
0x36 => Instruction::two(Opcode::ROL, 6, AddressMode::ZeroX, operand1),
0x2e => Instruction::thr(Opcode::ROL, 6, AddressMode::Abs, operand1, operand2),
0x3e => Instruction::thr(Opcode::ROL, 7, AddressMode::AbsX, operand1, operand2),
0x6a => Instruction::implied(Opcode::ROR, 2),
0x66 => Instruction::two(Opcode::ROR, 5, AddressMode::Zero, operand1),
0x76 => Instruction::two(Opcode::ROR, 6, AddressMode::ZeroX, operand1),
0x6e => Instruction::thr(Opcode::ROR, 6, AddressMode::Abs, operand1, operand2),
0x7e => Instruction::thr(Opcode::ROR, 7, AddressMode::AbsX, operand1, operand2),
0x48 => Instruction::implied(Opcode::PHA, 3),
0x68 => Instruction::implied(Opcode::PLA, 4),
0x08 => Instruction::implied(Opcode::PHP, 3),
0x28 => Instruction::implied(Opcode::PLP, 4),
0xa7 => Instruction::two(Opcode::LAX, 3, AddressMode::Zero, operand1),
0xb7 => Instruction::two(Opcode::LAX, 4, AddressMode::ZeroY, operand1),
0xaf => Instruction::thr(Opcode::LAX, 4, AddressMode::Abs, operand1, operand2),
0xbf => Instruction::thr(Opcode::LAX, 4, AddressMode::AbsY, operand1, operand2),
0xa3 => Instruction::two(Opcode::LAX, 6, AddressMode::IndX, operand1),
0xb3 => Instruction::two(Opcode::LAX, 5, AddressMode::IndY, operand1),
0x87 => Instruction::two(Opcode::SAX, 3, AddressMode::Zero, operand1),
0x97 => Instruction::two(Opcode::SAX, 4, AddressMode::ZeroY, operand1),
0x8f => Instruction::thr(Opcode::SAX, 4, AddressMode::Abs, operand1, operand2),
0x83 => Instruction::two(Opcode::SAX, 6, AddressMode::IndX, operand1),
0xeb => Instruction::two(Opcode::USBC, 2, AddressMode::Imm, operand1),
0xc7 => Instruction::two(Opcode::DCP, 5, AddressMode::Zero, operand1),
0xd7 => Instruction::two(Opcode::DCP, 6, AddressMode::ZeroX, operand1),
0xcf => Instruction::thr(Opcode::DCP, 6, AddressMode::Abs, operand1, operand2),
0xdf => Instruction::thr(Opcode::DCP, 7, AddressMode::AbsX, operand1, operand2),
0xdb => Instruction::thr(Opcode::DCP, 7, AddressMode::AbsY, operand1, operand2),
0xc3 => Instruction::two(Opcode::DCP, 8, AddressMode::IndX, operand1),
0xd3 => Instruction::two(Opcode::DCP, 8, AddressMode::IndY, operand1),
0xe7 => Instruction::two(Opcode::ISC, 5, AddressMode::Zero, operand1),
0xf7 => Instruction::two(Opcode::ISC, 6, AddressMode::ZeroX, operand1),
0xef => Instruction::thr(Opcode::ISC, 6, AddressMode::Abs, operand1, operand2),
0xff => Instruction::thr(Opcode::ISC, 7, AddressMode::AbsX, operand1, operand2),
0xfb => Instruction::thr(Opcode::ISC, 7, AddressMode::AbsY, operand1, operand2),
0xe3 => Instruction::two(Opcode::ISC, 8, AddressMode::IndX, operand1),
0xf3 => Instruction::two(Opcode::ISC, 4, AddressMode::IndY, operand1),
0x07 => Instruction::two(Opcode::SLO, 5, AddressMode::Zero, operand1),
0x17 => Instruction::two(Opcode::SLO, 6, AddressMode::ZeroX, operand1),
0x0f => Instruction::thr(Opcode::SLO, 6, AddressMode::Abs, operand1, operand2),
0x1f => Instruction::thr(Opcode::SLO, 7, AddressMode::AbsX, operand1, operand2),
0x1b => Instruction::thr(Opcode::SLO, 7, AddressMode::AbsY, operand1, operand2),
0x03 => Instruction::two(Opcode::SLO, 8, AddressMode::IndX, operand1),
0x13 => Instruction::two(Opcode::SLO, 8, AddressMode::IndY, operand1),
0x27 => Instruction::two(Opcode::RLA, 5, AddressMode::Zero, operand1),
0x37 => Instruction::two(Opcode::RLA, 6, AddressMode::ZeroX, operand1),
0x2f => Instruction::thr(Opcode::RLA, 6, AddressMode::Abs, operand1, operand2),
0x3f => Instruction::thr(Opcode::RLA, 7, AddressMode::AbsX, operand1, operand2),
0x3b => Instruction::thr(Opcode::RLA, 7, AddressMode::AbsY, operand1, operand2),
0x23 => Instruction::two(Opcode::RLA, 8, AddressMode::IndX, operand1),
0x33 => Instruction::two(Opcode::RLA, 8, AddressMode::IndY, operand1),
0x47 => Instruction::two(Opcode::SRE, 5, AddressMode::Zero, operand1),
0x57 => Instruction::two(Opcode::SRE, 6, AddressMode::ZeroX, operand1),
0x4f => Instruction::thr(Opcode::SRE, 6, AddressMode::Abs, operand1, operand2),
0x5f => Instruction::thr(Opcode::SRE, 7, AddressMode::AbsX, operand1, operand2),
0x5b => Instruction::thr(Opcode::SRE, 7, AddressMode::AbsY, operand1, operand2),
0x43 => Instruction::two(Opcode::SRE, 8, AddressMode::IndX, operand1),
0x53 => Instruction::two(Opcode::SRE, 8, AddressMode::IndY, operand1),
0x67 => Instruction::two(Opcode::RRA, 5, AddressMode::Zero, operand1),
0x77 => Instruction::two(Opcode::RRA, 6, AddressMode::ZeroX, operand1),
0x6f => Instruction::thr(Opcode::RRA, 6, AddressMode::Abs, operand1, operand2),
0x7f => Instruction::thr(Opcode::RRA, 7, AddressMode::AbsX, operand1, operand2),
0x7b => Instruction::thr(Opcode::RRA, 7, AddressMode::AbsY, operand1, operand2),
0x63 => Instruction::two(Opcode::RRA, 8, AddressMode::IndX, operand1),
0x73 => Instruction::two(Opcode::RRA, 8, AddressMode::IndY, operand1),
// TODO illegal, not sure if they should be NOPS or real
// 0xda => Instruction::implied(Opcode::PHX, 3),
// 0x5a => Instruction::implied(Opcode::PHY, 3),
// 0xfa => Instruction::implied(Opcode::PLX, 4),
// 0x7a => Instruction::implied(Opcode::PLY, 4),
0xda => Instruction::implied(Opcode::NOP, 2),
0x5a => Instruction::implied(Opcode::NOP, 2),
0xfa => Instruction::implied(Opcode::NOP, 2),
0x7a => Instruction::implied(Opcode::NOP, 2),
0x0b => Instruction::two(Opcode::ANC, 2, AddressMode::Imm, operand1),
0x2b => Instruction::two(Opcode::ANC2, 2, AddressMode::Imm, operand1),
0x4b => Instruction::two(Opcode::ALR, 2, AddressMode::Imm, operand1),
_ => panic!("Unknown opcode: {:#04x}", opbyte)
}
}
}

6
mos6502/src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
mod address_mode;
mod instructions;
pub mod cpu;
pub mod debugger;
pub mod mos6502;
pub mod memory;

34
mos6502/src/memory.rs Normal file
View file

@ -0,0 +1,34 @@
use std::ops::RangeInclusive;
const MEM_SIZE: usize = 0xffff + 1;
pub trait Bus {
fn read8(&self, address: u16) -> u8;
fn write8(&mut self, val: u8, address: u16);
fn read_range(&self, range: RangeInclusive<u16>) -> Vec<u8> {
range.map(|a| self.read8(a)).collect()
}
}
pub struct NonMappedMemory([u8; MEM_SIZE]);
impl NonMappedMemory {
pub fn load(program: &[u8], base: u16) -> Self {
let mut mem = [0x00; MEM_SIZE];
let base = base as usize;
let end = base + program.len() - 1;
mem[base..=end].copy_from_slice(program);
Self(mem)
}
}
impl Bus for NonMappedMemory {
fn read8(&self, address: u16) -> u8 {
self.0[address as usize]
}
fn write8(&mut self, val: u8, address: u16) {
self.0[address as usize] = val;
}
}

56
mos6502/src/mos6502.rs Normal file
View file

@ -0,0 +1,56 @@
use crate::{cpu::Cpu, memory::Bus, debugger::Debugger};
pub struct Mos6502 {
cpu: Cpu,
debugger: Debugger,
total_cycles: usize,
total_ticks: usize,
}
impl Mos6502 {
pub fn new(cpu: Cpu) -> Self {
let debugger = Debugger::new();
Self { cpu, debugger, total_cycles: 0, total_ticks: 0 }
}
pub fn cpu(&self) -> &Cpu {
&self.cpu
}
pub fn cpu_mut(&mut self) -> &mut Cpu {
&mut self.cpu
}
pub fn bus(&self) -> &Box<dyn Bus> {
self.cpu.bus()
}
pub fn debugger(&mut self) -> &mut Debugger {
&mut self.debugger
}
pub fn ticks(&self) -> usize {
self.total_ticks
}
pub fn cycles(&self) -> usize {
self.total_cycles
}
pub fn inc_cycles(&mut self, c: usize) {
self.total_cycles += c;
}
// The clock ticks Hzhzhzhz
pub fn tick(&mut self) -> usize {
let inst = self.cpu.fetch_next_instruction();
self.debugger.on_tick(&self.cpu, &inst);
let cycles = self.cpu.execute(&inst);
self.total_cycles += cycles;
self.total_ticks += 1;
cycles
}
}

View file

@ -0,0 +1,77 @@
use mos6502::{memory::NonMappedMemory, cpu::Cpu, mos6502::Mos6502};
fn run_test_rom(file: &str, load_base: u16, entry_point: u16, success_address: u16) -> (bool, usize) {
let path = format!("../test-roms/bin/{}", file);
let program = std::fs::read(path).expect("failed to load test rom");
let mem = NonMappedMemory::load(&program[..], load_base);
let mut cpu = Cpu::new(Box::new(mem));
cpu.set_pc(entry_point);
// debugger.add_breakpoint(Breakpoint::Opcode("DEX".into()));
let mut machine = Mos6502::new(cpu);
let mut last_pc: Option<u16> = None;
let mut ticks = 0usize;
loop {
ticks += 1;
machine.tick();
let pc = machine.cpu().pc();
// Panic if looping on PC, most likely functional_tests trap.
if Some(pc) == last_pc {
machine.debugger().dump_backtrace();
return (false, ticks)
}
// JMP start == catastrophic error
if last_pc.is_some() && pc == entry_point {
machine.debugger().dump_backtrace();
return (false, ticks)
}
if pc == success_address {
// println!("✅ TEST SUCCESSFUL! Hit success address at {:#06x}", pc);
// machine.debugger().dump_fun_stats();
return (true, ticks)
}
last_pc = Some(pc);
}
}
#[test]
fn functional_test_bcd_disabled() {
let expected_ticks = 26765879;
let res = run_test_rom("functional_test_bcd_disabled.bin", 0x000, 0x400, 0x336d);
assert!(res.0, "trapped");
assert_eq!(expected_ticks, res.1, "wrong tick count");
}
#[test]
fn ttl6502() {
let expected_ticks = 2738;
let res = run_test_rom("TTL6502.bin", 0xe000, 0xe000, 0xf5b6);
assert!(res.0, "trapped");
assert_eq!(expected_ticks, res.1, "wrong tick count");
}
#[test]
#[ignore = "BCD is not implemented yet"]
fn functional_test_full() {
let expected_ticks = 0;
let res = run_test_rom("functional_test_full.bin", 0x000, 0x400, 0x3469);
assert!(res.0, "trapped");
assert_eq!(expected_ticks, res.1, "wrong tick count");
}
#[test]
#[ignore = "BCD is not implemented yet"]
fn functional_test_extended_opcodes() {
let expected_ticks = 26765879;
let res = run_test_rom("extended_test.bin", 0x000, 0x400, 0x336d);
assert!(res.0, "trapped");
assert_eq!(expected_ticks, res.1, "wrong tick count");
}

16
nes-sdl/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "nes-sdl"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
#[[bin]]
#name = "nes-sdl"
#path = "src/nes-sdl.rs"
[dependencies]
structopt = "0.3.13"
sdl2 = "0.35.2"
common = { path = "../common" }
nes = { path = "../nes" }

49
nes-sdl/src/main.rs Normal file
View file

@ -0,0 +1,49 @@
use std::{path::PathBuf};
use nes::{cartridge::Cartridge, nes::Nes, mos6502::debugger::Breakpoint};
use structopt::StructOpt;
use common::utils;
mod sdl;
use crate::sdl::SdlHostSystem;
#[derive(StructOpt, Debug)]
struct Cli {
path: PathBuf,
#[structopt(short, long, parse(try_from_str = utils::parse_hex))]
breakpoint: Option<u16>,
#[structopt(short, long)]
opcode_breakpoint: Option<String>,
#[structopt(short, long)]
verbose: bool,
#[structopt(short, long)]
debug: bool
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Cli = Cli::from_args();
println!("Loading {:?}.", args.path);
let cart = Cartridge::blow_dust(args.path)?;
println!("Loaded! {}", cart);
let mut nes = Nes::insert(cart, SdlHostSystem::new());
let debugger = nes.debugger();
debugger.verbose(args.verbose);
if let Some(bp) = args.breakpoint {
debugger.add_breakpoint(Breakpoint::Address(bp));
}
if let Some(opbp) = args.opcode_breakpoint {
debugger.add_breakpoint(Breakpoint::Opcode(opbp));
}
if args.debug {
debugger.enable();
}
loop {
nes.tick();
}
}

99
nes-sdl/src/sdl.rs Normal file
View file

@ -0,0 +1,99 @@
use nes::{joypad::{Joypad, JoypadEvent, JoypadButton}, frame::RenderFrame, nes::HostSystem};
use sdl2::{pixels::PixelFormatEnum, event::Event, keyboard::Keycode, Sdl, render::{Texture, Canvas, TextureCreator}, video::{Window, WindowContext}};
pub struct SdlHostSystem<'a> {
context: Sdl,
canvas: Canvas<Window>,
texture: Texture<'a>,
_creator: TextureCreator<WindowContext>
}
impl SdlHostSystem<'_> {
const W: u32 = 256;
const H: u32 = 240;
pub fn new() -> Self {
// const scale: f32 = 1.;
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem.window("Potatis", Self::W * 4, Self::H * 4)
.position_centered()
.build()
.unwrap();
let canvas = window.into_canvas()
.present_vsync()
.build()
.unwrap();
// canvas.set_scale(4., scale).unwrap();
let mut creator = canvas.texture_creator();
let texture: Texture = unsafe {
let ptr = &mut creator as *mut TextureCreator<WindowContext>;
(*ptr)
.create_texture_target(PixelFormatEnum::RGB24, Self::W, Self::H)
.unwrap()
};
Self {
_creator: creator,
context: sdl_context,
canvas,
texture
}
}
}
impl HostSystem for SdlHostSystem<'_> {
fn render(&mut self, frame: &RenderFrame) {
self.texture.update(None, frame.pixels(), frame.pitch()).unwrap();
self.canvas.copy(&self.texture, None, None).unwrap();
self.canvas.present();
}
fn poll_events(&mut self, joypad: &mut Joypad) {
for event in self.context.event_pump().unwrap().poll_iter() {
if let Some(joypad_ev) = map_joypad(&event) {
joypad.on_event(joypad_ev);
continue;
}
match event {
Event::Quit {..} |
Event::KeyDown { keycode: Some(Keycode::Q), .. } |
Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
std::process::exit(1) // TODO: exit more gracefully
}
_ => ()
}
}
}
}
fn map_joypad(sdlev: &Event) -> Option<JoypadEvent> {
match sdlev {
Event::KeyDown { keycode: Some(keycode), .. } => {
map_button(keycode).map(JoypadEvent::Press)
}
Event::KeyUp { keycode: Some(keycode), .. } => {
map_button(keycode).map(JoypadEvent::Release)
}
_ => None
}
}
fn map_button(keycode: &Keycode) -> Option<JoypadButton> {
match keycode {
Keycode::Down => Some(JoypadButton::DOWN),
Keycode::Up => Some(JoypadButton::UP),
Keycode::Left => Some(JoypadButton::LEFT),
Keycode::Right => Some(JoypadButton::RIGHT),
Keycode::A => Some(JoypadButton::A),
Keycode::B => Some(JoypadButton::B),
Keycode::Return => Some(JoypadButton::START),
Keycode::Space => Some(JoypadButton::SELECT),
_ => None
}
}

14
nes/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "nes"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitflags = "1.3.2"
common = { path = "../common" }
mos6502 = { path = "../mos6502" }
[lib]
doctest = false

162
nes/src/cartridge.rs Normal file
View file

@ -0,0 +1,162 @@
use std::{fmt::Display, path::PathBuf};
use common::kilobytes;
use crate::error::PotatisError;
#[derive(Debug)]
#[repr(C)]
struct Header {
magic: [u8; 4],
prg_rom_size: u8,
chr_rom_size: u8,
flags6: u8,
flags7: u8,
flags8: u8,
flags9: u8,
flags10: u8,
padding: [u8; 5]
}
#[derive(Debug, PartialEq, Eq)]
enum Format { Nes2, Ines }
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Mapper {
Nrom = 0,
Mmc1 = 1,
Mapper3 = 3,
}
impl TryFrom<&Header> for Mapper {
type Error = PotatisError;
fn try_from(header: &Header) -> Result<Self, Self::Error> {
let id: u8 = (header.flags7 & 0xf0) | header.flags6 >> 4;
match id {
0 => Ok(Mapper::Nrom),
1 => Ok(Mapper::Mmc1),
3 => Ok(Mapper::Mapper3),
_ => Err(PotatisError::NotYetImplemented(format!("Mapper {}", id)))
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Mirroring {
Horizontal,
Vertical,
FourScreen
}
pub struct Cartridge {
mirroring: Mirroring,
prg_rom: Vec<u8>,
chr_rom: Vec<u8>,
mapper: Mapper,
}
impl Cartridge {
const MAGIC: [u8; 4] = [0x4e, 0x45, 0x53, 0x1a];
pub fn blow_dust(path: PathBuf) -> Result<Cartridge, PotatisError> {
let bin = std::fs::read(path)?;
if bin[0..4] != Self::MAGIC {
return Err(PotatisError::InvalidCartMagic);
}
// TODO: should probably use nom or something, or just read manually, it's 16bytes..
let header: Header = unsafe { std::ptr::read(bin.as_ptr() as *const _) };
let format = if (header.flags7 & 0x0c) == 0x08 { Format::Nes2 } else { Format::Ines };
if format == Format::Nes2 {
return Err(PotatisError::NotYetImplemented("NES 2.0".into()));
}
let mapper = Mapper::try_from(&header)?;
if header.magic != Self::MAGIC {
return Err(PotatisError::InvalidCartMagic);
}
let skip_trainer = header.flags6 & 0b100 != 0;
if skip_trainer || (header.flags6 & (1 << 3)) != 0 {
return Err(PotatisError::NotYetImplemented("Trainer".into()));
}
if header.flags6 & 0b10 != 0 {
return Err(PotatisError::NotYetImplemented("PRG RAM".into()));
}
if header.flags6 & 0b1000 != 0 {
return Err(PotatisError::NotYetImplemented("cartidge fiddles w VRAM address space..".into()));
}
let mut mirroring = match header.flags6 & 1 {
1 => Mirroring::Vertical,
_ => Mirroring::Horizontal
};
if header.flags6 & 0b1000 != 0 {
mirroring = Mirroring::FourScreen
}
let prg_size = (header.prg_rom_size as usize) * kilobytes::KB16;
let prg_start = 16usize; // sizeof header
let prg_end = prg_start + prg_size;
let prg_rom = bin[prg_start..prg_end].to_vec();
let chr_rom = if header.chr_rom_size == 0 {
vec![]
} else {
let chr_start = prg_end;
let chr_size = (header.chr_rom_size as usize) * kilobytes::KB8;
bin[chr_start..(chr_start + chr_size)].to_vec()
};
Ok(Cartridge {
prg_rom,
chr_rom,
mirroring,
mapper,
})
}
pub fn mirroring(&self) -> Mirroring {
self.mirroring
}
pub fn prg(&self) -> &[u8] {
&self.prg_rom
}
pub fn chr(&self) -> &[u8] {
&self.chr_rom
}
pub fn chr_mut(&mut self) -> &mut [u8] {
&mut self.chr_rom
}
pub fn mapper(&self) -> Mapper {
self.mapper
}
#[cfg(test)]
pub fn new_test(prg_rom: &[u8], chr_rom: &[u8]) -> Self {
Cartridge {
prg_rom: prg_rom.to_vec(),
chr_rom: chr_rom.to_vec(),
mirroring: Mirroring::Vertical,
mapper: Mapper::Nrom,
}
}
}
impl Display for Cartridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Mapper: {:?}, Mirroring: {:?}, CHR ROM: {}", self.mapper, self.mirroring, self.chr_rom.len())
}
}

36
nes/src/frame.rs Normal file
View file

@ -0,0 +1,36 @@
pub const W: usize = 256;
pub const H: usize = 240;
const BYTES_PER_PIXEL: usize = 3; // 0xRR0xGG0xBB
pub struct RenderFrame {
pixels: [u8; W * H * BYTES_PER_PIXEL],
}
impl RenderFrame {
pub fn new() -> RenderFrame {
RenderFrame { pixels: [0; W * H * BYTES_PER_PIXEL] }
}
pub fn set_pixel(&mut self, x: usize, y: usize, rgb: (u8, u8, u8)) {
let row = y * W * BYTES_PER_PIXEL;
let col = x * BYTES_PER_PIXEL;
let i = row + col;
if i + 3 < self.pixels.len() {
// let pixel = &mut self.pixels[start..start+3];
// pixel.copy_from_slice(&[rgb.0, rgb.1, rgb.2]);
self.pixels[i] = rgb.0;
self.pixels[i + 1] = rgb.1;
self.pixels[i + 2] = rgb.2;
}
}
pub fn pixels(&self) -> &[u8] {
&self.pixels[..]
}
pub fn pitch(&self) -> usize {
W * BYTES_PER_PIXEL
}
}

57
nes/src/joypad.rs Normal file
View file

@ -0,0 +1,57 @@
use bitflags::bitflags;
bitflags! {
#[derive(Default)]
pub struct JoypadButton: u8 {
const A = 0b00000001;
const B = 0b00000010;
const SELECT = 0b00000100;
const START = 0b00001000;
const UP = 0b00010000;
const DOWN = 0b00100000;
const LEFT = 0b01000000;
const RIGHT = 0b10000000;
}
}
#[derive(Debug)]
pub enum JoypadEvent {
Press(JoypadButton),
Release(JoypadButton)
}
#[derive(Default)]
pub struct Joypad {
state: JoypadButton,
out: u8
}
/*
bit 7 6 5 4 3 2 1 0
button A B Select Start Up Down Left Right
*/
impl Joypad {
pub fn new() -> Self {
Self::default()
}
pub fn read(&mut self) -> u8 {
// It reads 8 times, once per button
let val = self.out & 1;
self.out >>= 1;
val
}
pub fn strobe(&mut self, val: u8) {
if val & 1 == 1 { // Strobe is high
self.out = self.state.bits;
}
}
pub fn on_event(&mut self, event: JoypadEvent) {
match event {
JoypadEvent::Press(b) => self.state.set(b, true),
JoypadEvent::Release(b) => self.state.set(b, false),
}
}
}

33
nes/src/lib.rs Normal file
View file

@ -0,0 +1,33 @@
pub use mos6502;
mod mappers;
mod nesbus;
mod ppu;
pub mod frame;
pub mod cartridge;
pub mod nes;
pub mod joypad;
pub mod error {
#[derive(Debug)]
pub enum PotatisError {
IO(std::io::Error),
InvalidCartMagic,
NotYetImplemented(String),
}
impl std::error::Error for PotatisError {}
impl std::fmt::Display for PotatisError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<std::io::Error> for PotatisError {
fn from(e: std::io::Error) -> Self {
PotatisError::IO(e)
}
}
}

View file

@ -0,0 +1,91 @@
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::Cartridge;
const BANK_SIZE: usize = kilobytes::KB8;
// AKA CNROM
pub(crate) struct Mapper3 {
cart: Cartridge,
selected_bank: usize,
is_16kb: bool,
// this mapper does not have a ram, but tests put status codes in mem ranges outside of the
// documented memory map for this mapper. this ram is only used in tests. TODO make better
ram_for_integration_test: [u8; kilobytes::KB32]
}
impl Mapper3 {
pub fn new(cart: Cartridge) -> Self {
let is_16kb = match cart.prg().len() {
kilobytes::KB16 => true,
kilobytes::KB32 => false,
_ => panic!("invalid size for mapper 3 prg rom")
};
Self {
cart,
selected_bank: 0,
is_16kb,
ram_for_integration_test: [0; kilobytes::KB32]
}
}
}
impl Bus for Mapper3 {
fn read8(&self, address: u16) -> u8 {
match address {
0x0000..=0x1fff => self.cart.chr()[(self.selected_bank * BANK_SIZE) + address as usize],
0x8000..=0xffff => {
if self.is_16kb {
self.cart.prg()[address as usize - 0x8000 - kilobytes::KB16] // see tests
}
else {
self.cart.prg()[address as usize - 0x8000]
}
}
_ => {
self.ram_for_integration_test[address as usize]
}
}
}
fn write8(&mut self, val: u8, address: u16) {
match address {
0x0000..=0x1fff => self.cart.chr_mut()[(self.selected_bank * BANK_SIZE) + address as usize] = val,
0x8000..=0xffff => {
self.selected_bank = (val & 0b00000011) as usize;
// println!("mapper 3 selected bank: {}", self.selected_bank);
},
_ => self.ram_for_integration_test[address as usize] = val
}
}
}
#[cfg(test)]
mod tests {
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::Cartridge;
use super::Mapper3;
#[test]
fn test_vectors_at_end() {
let mut kb32 = [0; kilobytes::KB32];
kb32[kilobytes::KB32-2..kilobytes::KB32].copy_from_slice(&[0xde, 0xad]);
let cart = Cartridge::new_test(&kb32, &[]);
let mapper = Mapper3::new(cart);
assert_eq!(mapper.read8(0xfffe), 0xde);
assert_eq!(mapper.read8(0xffff), 0xad);
let mut kb16 = [0; kilobytes::KB16];
kb16[kilobytes::KB16-2..kilobytes::KB16].copy_from_slice(&[0xbe, 0xef]);
let cart = Cartridge::new_test(&kb16, &[]);
let mapper = Mapper3::new(cart);
assert_eq!(mapper.read8(0xfffe), 0xbe);
assert_eq!(mapper.read8(0xffff), 0xef);
}
}

232
nes/src/mappers/mmc1.rs Normal file
View file

@ -0,0 +1,232 @@
use core::panic;
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::Cartridge;
#[derive(Debug, PartialEq, Eq)]
enum PrgBankMode {
Switch32Kb,
FixFirstLowerSwitchUpper,
FixLastUpperSwitchLower,
}
impl From<u8> for PrgBankMode {
fn from(i: u8) -> Self {
match i {
0 | 1 => PrgBankMode::Switch32Kb,
2 => PrgBankMode::FixFirstLowerSwitchUpper,
3 => PrgBankMode::FixLastUpperSwitchLower,
_ => panic!()
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum ChrBankMode {
Switch8Kb,
SwitchTwo4KbBanks
}
pub struct MMC1 {
prg_ram: [u8; kilobytes::KB8],
prg_rom_banks: Vec<Vec<u8>>,
prg_rom_bank_mode: PrgBankMode,
selected_prg_bank: u8,
// chr_rom: Vec<Vec<u8>>,
chr_rom_banks: Vec<Vec<u8>>,
chr_rom_bank_mode: ChrBankMode,
selected_chr_bank: u8,
chr_ram_mode: bool,
num_shift_writes: u8,
shift_register: u8,
}
impl MMC1 {
pub fn new(cart: Cartridge) -> MMC1 {
let chunks = cart.prg().chunks_exact(kilobytes::KB16);
assert!(chunks.remainder().len() == 0);
let prg_rom_banks = chunks.map(|s| s.to_vec()).collect();
let chr_ram_mode: bool;
let chr_rom_banks = if cart.chr().len() == 0 {
// CHR RAM
chr_ram_mode = true;
vec![vec![0; kilobytes::KB4]; 2]
} else {
chr_ram_mode = false;
let chunks = cart.chr().chunks_exact(kilobytes::KB4);
assert!(chunks.remainder().len() == 0);
chunks.map(|s| s.to_vec()).collect()
};
MMC1 {
prg_ram: [0; kilobytes::KB8],
prg_rom_banks: prg_rom_banks,
prg_rom_bank_mode: PrgBankMode::FixFirstLowerSwitchUpper, // TODO don't understand the default yet..
chr_rom_banks: chr_rom_banks,
chr_rom_bank_mode: ChrBankMode::Switch8Kb, // TODO: default?
chr_ram_mode,
selected_chr_bank: 0,
shift_register: 0,
num_shift_writes: 0,
selected_prg_bank: 0,
}
}
fn reset_shift_register(&mut self) {
self.num_shift_writes = 0;
self.shift_register = 0;
}
fn write_to_shift_register(&mut self, val: u8, address: u16) {
if common::bits::is_signed(val) {
self.reset_shift_register();
return;
}
let bit_to_write = val & 1;
// shift in bit, lsb first. max width of shift reg is 5 bits, so we only shift to bit 4.
self.shift_register = (self.shift_register >> 1) | (bit_to_write << 4);
self.num_shift_writes += 1;
if self.num_shift_writes == 5 {
match address {
0x8000..=0x9fff => { // Control
self.update_control_register(self.shift_register);
}
0xa000..=0xbfff => { // CHR bank 0
self.switch_lower_chr_bank(self.shift_register)
}
0xc000..=0xdfff => { // CHR bank 1
self.switch_upper_chr_bank(self.shift_register)
}
0xe000..=0xffff => { // PRG bank
self.selected_prg_bank = self.shift_register & 0b01111;
// println!("selected_rom: {}", self.selected_prg_bank);
}
_ => panic!("unknown register")
}
self.reset_shift_register()
}
}
fn switch_lower_chr_bank(&mut self, selected_bank: u8) {
// https://www.nesdev.org/wiki/MMC1#iNES_Mapper_001
// println!("{:05b} to CHR bank 0", selected_bank);
match self.chr_rom_bank_mode {
ChrBankMode::Switch8Kb => {
if selected_bank != 0 {
todo!("handle bank switch")
}
self.selected_chr_bank = selected_bank;
},
ChrBankMode::SwitchTwo4KbBanks => todo!(),
}
}
fn switch_upper_chr_bank(&mut self, _selected_bank: u8) {
// https://www.nesdev.org/wiki/MMC1#iNES_Mapper_001
match self.chr_rom_bank_mode {
ChrBankMode::Switch8Kb => (), // (ignored in 8 KB mode)
ChrBankMode::SwitchTwo4KbBanks => todo!(),
}
}
fn update_control_register(&mut self, val: u8) {
// 01010
// println!("{:05b} control", val);
let _mirroring = val & 0b00011; // TODO, gfx stuff
let chr_rom_bank_mode = (val & 0b10000) >> 4;
self.chr_rom_bank_mode = match chr_rom_bank_mode {
0 => ChrBankMode::Switch8Kb,
_ => ChrBankMode::SwitchTwo4KbBanks
};
if self.chr_rom_bank_mode == ChrBankMode::SwitchTwo4KbBanks {
todo!("implement this bank mode")
}
let prg_rom_bank_mode = (val & 0b01100) >> 2;
self.prg_rom_bank_mode = prg_rom_bank_mode.into();
// println!("setting prg rom bank mode: {:?}", self.prg_rom_bank_mode);
}
fn lower_prg_bank(&self) -> &Vec<u8> {
match self.prg_rom_bank_mode {
PrgBankMode::Switch32Kb => todo!(),
PrgBankMode::FixFirstLowerSwitchUpper => &self.prg_rom_banks[0],
PrgBankMode::FixLastUpperSwitchLower => todo!(),
}
}
fn upper_prg_bank(&self) -> &Vec<u8> {
// &self.rom_banks[self.selected_rom as usize]
match self.prg_rom_bank_mode {
PrgBankMode::Switch32Kb => todo!(),
PrgBankMode::FixFirstLowerSwitchUpper => &self.prg_rom_banks[self.selected_prg_bank as usize],
PrgBankMode::FixLastUpperSwitchLower => todo!(),
}
}
fn lower_chr_bank(&self) -> &Vec<u8> {
match self.chr_rom_bank_mode {
ChrBankMode::Switch8Kb => &self.chr_rom_banks[0],
ChrBankMode::SwitchTwo4KbBanks => todo!(),
}
}
fn upper_chr_bank(&self) -> &Vec<u8> {
// &self.rom_banks[self.selected_rom as usize]
match self.chr_rom_bank_mode {
ChrBankMode::Switch8Kb => &self.chr_rom_banks[1],
ChrBankMode::SwitchTwo4KbBanks => todo!(),
}
}
fn write_chr_ram(&mut self, val: u8, address: u16) {
if !self.chr_ram_mode {
panic!("writing to CHR without CHR RAM enabled.. is this correct?")
}
// TODO: don't to this every write op
let mut ram: Vec<&mut u8> = self.chr_rom_banks.iter_mut().flatten().collect();
*ram[address as usize] = val;
}
}
impl Bus for MMC1 {
fn read8(&self, address: u16) -> u8 {
// TODO: In most mappers, banks past the end of PRG or CHR ROM show up as mirrors of earlier banks.
match address {
// PPU
0x0000..=0x0fff => self.lower_chr_bank()[address as usize],
0x1000..=0x1fff => self.upper_chr_bank()[address as usize - 0x1000],
// CPU
0x6000..=0x7fff => self.prg_ram[address as usize - 0x6000],
0x8000..=0xbfff => self.lower_prg_bank()[address as usize - 0x8000],
0xc000..=0xffff => self.upper_prg_bank()[address as usize - 0xc000],
_ => panic!("unknown mmc1 memory range") // TODO: In most mappers, banks past the end of PRG or CHR ROM show up as mirrors of earlier banks.
}
}
fn write8(&mut self, val: u8, address: u16) {
match address {
// PPU
0x0000..=0x1fff => self.write_chr_ram(val, address),
// 0x1000..=0x1fff => self.upper_chr_bank().borrow_mut()[address as usize - 0x1000] = val,
// CPU
0x6000..=0x7fff => self.prg_ram[address as usize - 0x6000] = val,
0x8000..=0xffff => self.write_to_shift_register(val, address),
_ => panic!("writing to rom")
}
}
}

17
nes/src/mappers/mod.rs Normal file
View file

@ -0,0 +1,17 @@
use std::{cell::RefCell, rc::Rc};
use mos6502::memory::Bus;
use crate::cartridge::Cartridge;
mod mmc1;
mod nrom;
mod mapper3;
pub(crate) fn for_cart(cart: Cartridge) -> Rc<RefCell<dyn Bus>> {
match cart.mapper() {
crate::cartridge::Mapper::Nrom => Rc::new(RefCell::new(nrom::NROM::new(cart))),
crate::cartridge::Mapper::Mmc1 => Rc::new(RefCell::new(mmc1::MMC1::new(cart))),
crate::cartridge::Mapper::Mapper3 => Rc::new(RefCell::new(mapper3::Mapper3::new(cart))),
}
}

60
nes/src/mappers/nrom.rs Normal file
View file

@ -0,0 +1,60 @@
use core::panic;
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::Cartridge;
pub struct NROM {
cart: Cartridge,
// prg_rom: &'a [u8],
// chr_rom: Vec<u8>,
prg_ram: [u8; kilobytes::KB8],
is_16kb: bool
}
impl NROM {
pub fn new(cart: Cartridge) -> Self {
let is_16kb = match cart.prg().len() {
kilobytes::KB16 => true,
kilobytes::KB32 => false,
_ => panic!("invalid size for NROM prg rom")
};
Self { cart, prg_ram: [0; kilobytes::KB8], is_16kb }
}
}
impl Bus for NROM {
fn read8(&self, address: u16) -> u8 {
match address {
0x0000..=0x1fff => self.cart.chr()[address as usize], // PPU
// TODO: Mirrored, Write protectable w external switch
0x6000..=0x7fff => self.prg_ram[address as usize - 0x6000],
0x8000..=0xbfff => self.cart.prg()[address as usize - 0x8000],
0xc000..=0xffff => {
if self.is_16kb {
// Mirror
self.cart.prg()[address as usize - 0xc000]
}
else {
// last 16kb of rom
self.cart.prg()[kilobytes::KB16 + (address as usize - 0xc000)]
}
}
_ => panic!("unknown NROM memory range: {:#06x}", address)
}
}
fn write8(&mut self, val: u8, address: u16) {
match address {
// TODO: Mirrored, Write protectable w external switch
0x0000..=0x1fff => (),//panic!("writing to chr rom?? {:#06x}", address), //self.chr_rom[address as usize] = val,
0x6000..=0x7fff => self.prg_ram[address as usize - 0x6000] = val,
_ => {
// println!("writing to {:#06x}", address);
panic!("writing to rom");
}
}
}
}

123
nes/src/nes.rs Normal file
View file

@ -0,0 +1,123 @@
use std::{rc::Rc, cell::RefCell};
use mos6502::{mos6502::Mos6502, memory::{Bus}, cpu::{Cpu, Reg}, debugger::Debugger};
use crate::{cartridge::Cartridge, nesbus::NesBus, ppu::ppu::PPU, joypad::Joypad, frame::RenderFrame};
pub trait HostSystem {
fn render(&mut self, frame: &RenderFrame);
fn poll_events(&mut self, joypad: &mut Joypad);
}
#[derive(Default)]
struct HeadlessHost;
impl HostSystem for HeadlessHost {
fn render(&mut self, _: &RenderFrame) {}
fn poll_events(&mut self, _: &mut Joypad) {}
}
pub struct Nes {
machine: Mos6502,
ppu: Rc<RefCell<PPU>>,
host: Box<dyn HostSystem>,
joypad: Rc<RefCell<Joypad>>,
}
impl Nes {
pub fn insert<H : HostSystem + 'static>(cartridge: Cartridge, host: H) -> Self {
let mirroring = cartridge.mirroring();
let rom_mapper = crate::mappers::for_cart(cartridge);
let ppu = Rc::new(RefCell::new(PPU::new(rom_mapper.clone(), mirroring)));
let joypad = Rc::new(RefCell::new(Joypad::default()));
let bus = NesBus::new(rom_mapper.clone(), ppu.clone(), joypad.clone());
let mut cpu = Cpu::new(Box::new(bus));
cpu.reset();
let mut machine = Mos6502::new(cpu);
machine.inc_cycles(7); // Startup cycles.. (not sure, from nestest)
Self {
machine,
ppu,
host: Box::new(host),
joypad
}
}
pub fn insert_headless_host(cartridge: Cartridge) -> Self {
Self::insert(cartridge, HeadlessHost::default())
}
pub fn debugger(&mut self) -> &mut Debugger {
self.machine.debugger()
}
pub fn cpu(&self) -> &Cpu {
self.machine.cpu()
}
pub fn cpu_mut(&mut self) -> &mut Cpu {
self.machine.cpu_mut()
}
pub fn bus(&self) -> &Box<dyn Bus> {
self.machine.bus()
}
pub fn cpu_ticks(&self) -> usize {
self.machine.ticks()
}
pub fn tick(&mut self) {
let cpu_cycles = self.machine.tick();
self.ppu.borrow_mut().tick(cpu_cycles * 3);
self.host.poll_events(&mut self.joypad.borrow_mut());
let mut ppu = self.ppu.borrow_mut();
if ppu.frame_ready_to_render() {
let frame = ppu.frame();
self.host.render(frame);
ppu.clear_frame_ready();
}
if ppu.is_nmi_pending() {
// println!("NMI");
self.machine.cpu_mut().interrupt_nmi();
ppu.clear_pending_nmi();
}
}
}
// mainly for nestest
impl std::fmt::Debug for Nes {
// A:00 X:00 Y:00 P:26 SP:FB PPU: 0,120 CYC:40
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let c = self.cpu();
let scanline = self.ppu.borrow_mut().scanline();
let ppu_cycle = self.ppu.borrow_mut().current_cycle();
// let ppuw = if scanline >= 10 { 3 } else { 3 };
let ppuw = 3;
if ppu_cycle < 100 {
write!(f,
"{:04X} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:ppuw$}, {:>2} CYC:{}",
c.pc(),
c[Reg::AC], c[Reg::X], c[Reg::Y], c.flags_as_byte(), c[Reg::SP],
scanline, ppu_cycle,
self.machine.cycles()
)
}
else {
write!(f,
"{:04X} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:ppuw$},{:>2} CYC:{}",
c.pc(),
c[Reg::AC], c[Reg::X], c[Reg::Y], c.flags_as_byte(), c[Reg::SP],
scanline, ppu_cycle,
self.machine.cycles()
)
}
}
}

158
nes/src/nesbus.rs Normal file
View file

@ -0,0 +1,158 @@
use std::{rc::Rc, cell::RefCell};
use common::kilobytes;
use mos6502::memory::Bus;
use crate::{ppu::ppu::PPU, joypad::Joypad};
pub struct NesBus {
ram: [u8; kilobytes::KB2],
rom: Rc<RefCell<dyn Bus>>,
ppu: Rc<RefCell<PPU>>,
joypad: Rc<RefCell<Joypad>>
}
#[derive(Debug, PartialEq, Eq)]
enum MappedDevice {
RAM,
PPU,
APU,
PPUOAMDMA,
JOYPAD,
CPUTEST,
CARTRIDGE,
}
impl NesBus {
pub fn new(rom: Rc<RefCell<dyn Bus>>, ppu: Rc<RefCell<PPU>>, joypad: Rc<RefCell<Joypad>>) -> Self {
Self {
rom: rom,
ram: [0; kilobytes::KB2],
ppu: ppu,
joypad
}
}
fn map(&self, address: u16) -> (MappedDevice, u16) {
match address {
0x0000..=0x07ff => (MappedDevice::RAM, address),
0x0800..=0x1fff => (MappedDevice::RAM, address & 0x07ff),
0x2000..=0x2007 => (MappedDevice::PPU, address - 0x2000),
0x2008..=0x3fff => (MappedDevice::PPU, address % 8),
0x4014 => (MappedDevice::PPUOAMDMA, address),
0x4000..=0x4015 => (MappedDevice::APU, address - 0x4000),
0x4016..=0x4017 => (MappedDevice::JOYPAD, address),
0x4018..=0x401f => (MappedDevice::CPUTEST, address - 0x4018),
0x4020..=0xffff => (MappedDevice::CARTRIDGE, address),
}
}
}
impl Bus for NesBus {
fn read8(&self, address: u16) -> u8 {
let (device, mapped_address) = self.map(address);
match device {
MappedDevice::RAM => self.ram[mapped_address as usize],
MappedDevice::PPU => self.ppu.borrow().cpu_read_register(mapped_address),
MappedDevice::APU => 0,
MappedDevice::PPUOAMDMA => 0,
MappedDevice::JOYPAD => {
match address {
0x4016 => self.joypad.borrow_mut().read(), // Joystick 1 data
0x4017 => 0, // Joystick 2 data
_ => unreachable!()
}
}
MappedDevice::CPUTEST => 0,
MappedDevice::CARTRIDGE => self.rom.borrow().read8(mapped_address),
}
}
fn write8(&mut self, val: u8, address: u16) {
let (device, mapped_address) = self.map(address);
match device {
MappedDevice::RAM => self.ram[mapped_address as usize] = val,
MappedDevice::PPU => self.ppu.borrow_mut().cpu_write_register(val, mapped_address),
MappedDevice::APU => (),
MappedDevice::PPUOAMDMA => {
// Dump CPU page XX00..XXFF to PPU OAM
let page_start = (val as u16) << 8;
let mem: Vec<u8> = (page_start..=page_start+0xff).map(|addr| self.read8(addr)).collect();
// println!("{:#04x} - dumping {:#06x}..{:#06x}", val, page_start, page_start+0xff);
self.ppu.borrow_mut().cpu_oam_dma(&mem[..]);
}
MappedDevice::JOYPAD => {
match address {
0x4016 => self.joypad.borrow_mut().strobe(val), // Joystick strobe
0x4017 => (), // APU Frame counter control
_ => unreachable!()
}
}
MappedDevice::CPUTEST => (), // TODO
MappedDevice::CARTRIDGE => self.rom.borrow_mut().write8(val, address),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestBus{}
impl Bus for TestBus {
fn read8(&self, _: u16) -> u8 {
todo!()
}
fn write8(&mut self, _: u8, _: u16) {
todo!()
}
}
fn sut() -> NesBus {
let bus = Rc::new(RefCell::new(TestBus{}));
let joypad = Joypad::default();
NesBus::new(
bus.clone(),
Rc::new(RefCell::new(PPU::new(bus, crate::cartridge::Mirroring::FourScreen))),
Rc::new(RefCell::new(joypad))
)
}
#[test]
fn test_map_ram_mirror() {
let bus = sut();
assert_eq!(bus.map(0x07ff), (MappedDevice::RAM, 0x07ff));
assert_eq!(bus.map(0x0800), (MappedDevice::RAM, 0x0000));
assert_eq!(bus.map(0x1fff), (MappedDevice::RAM, 0x07ff));
assert_eq!(bus.map(0x1001), (MappedDevice::RAM, 0x0001));
}
#[test]
fn test_map_ppu_mirror() {
let bus = sut();
assert_eq!(bus.map(0x2000), (MappedDevice::PPU, 0));
assert_eq!(bus.map(0x3456), (MappedDevice::PPU, 6));
assert_eq!(bus.map(0x2008), (MappedDevice::PPU, 0));
assert_eq!(bus.map(0x3fff), (MappedDevice::PPU, 7));
assert_eq!(bus.map(0x2022), (MappedDevice::PPU, 2));
for a in (0x2002..=0x3ffa).step_by(8) {
assert_eq!(bus.map(a), (MappedDevice::PPU, 2));
}
for a in (0x2007..=0x3fff).step_by(8) {
assert_eq!(bus.map(a), (MappedDevice::PPU, 7));
}
for a in (0x2000..=0x3fff).step_by(8) {
assert_eq!(bus.map(a), (MappedDevice::PPU, 0));
}
}
}

3
nes/src/ppu/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod registers;
mod palette;
pub(crate) mod ppu;

73
nes/src/ppu/palette.rs Normal file
View file

@ -0,0 +1,73 @@
// https://www.nesdev.org/wiki/PPU_palettes
pub fn palette_to_rgb(value: u8) -> (u8, u8, u8) {
match value {
0x00 => (101, 101, 101),
0x01 => (0 , 45, 105),
0x02 => (19, 31, 127),
0x03 => (69 , 19, 124),
0x04 => (96 , 11, 98),
0x05 => (115, 10, 55),
0x06 => (113, 15, 7),
0x07 => (90 , 26, 0),
0x08 => (52 , 40, 0),
0x09 => (11 , 52, 0),
0x0a => (0, 60, 0),
0x0b => (0, 61, 16),
0x0c => (0, 56, 64),
0x0d => (0, 0, 0),
0x0e => (0, 0, 0),
0x0f => (0, 0, 0),
0x10 => (174,174 ,174),
0x11 => (15 , 99,179),
0x12 => (64 , 81, 208),
0x13 => (120, 65, 204),
0x14 => (167, 54, 169),
0x15 => (192, 52, 112),
0x16 => (189, 60, 48),
0x17 => (159, 74, 0),
0x18 => (109, 92, 0),
0x19 => (54 , 109 , 0),
0x1a => (7 , 119 , 4),
0x1b => (0 , 121 , 61),
0x1c => (0, 114 ,125),
0x1d => (0, 0, 0),
0x1e => (0, 0, 0),
0x1f => (0, 0, 0),
0x20 => (254, 254, 255),
0x21 => (93, 179, 255),
0x22 => (143, 161, 255),
0x23 => (200, 144, 255),
0x24 => (247, 133, 250),
0x25 => (255, 131, 192),
0x26 => (255, 139, 127),
0x27 => (239, 154, 73),
0x28 => (189, 172, 44),
0x29 => (133, 188, 47),
0x2a => (85, 199, 83),
0x2b => (60, 201, 140),
0x2c => (62, 194, 205),
0x2d => (78, 78, 78),
0x2e => (0, 0, 0),
0x2f => (0, 0, 0),
0x30 => (254, 254, 255),
0x31 => (188, 223, 255),
0x32 => (209, 216, 255),
0x33 => (232, 209, 255),
0x34 => (251, 205, 253),
0x35 => (255, 204, 229),
0x36 => (255, 207, 202),
0x37 => (248, 213, 180),
0x38 => (228, 220, 168),
0x39 => (204, 227, 169),
0x3a => (185, 232, 184),
0x3b => (174, 232, 208),
0x3c => (175, 229, 234),
0x3d => (182, 182, 182),
0x3e => (0, 0, 0),
0x3f => (0, 0, 0),
_ => {
println!("eh");
(0, 0, 0)
}
}
}

463
nes/src/ppu/ppu.rs Normal file
View file

@ -0,0 +1,463 @@
use core::panic;
use std::{cell::{RefCell, Cell}, rc::Rc};
use common::kilobytes;
use mos6502::memory::Bus;
use crate::{cartridge::Mirroring, frame::RenderFrame};
use super::{registers::{ControlRegister, StatusRegister, OpenBus, MaskRegister}, palette};
const PALETTE_SIZE: usize = 32;
#[derive(Debug)]
#[repr(u16)]
#[allow(dead_code)]
enum Register {
PPUCTRL = 0, // ... + base 0x2000
PPUMASK = 1,
PPUSTATUS = 2,
OAMADDR = 3,
OAMDATA = 4,
PPUSCROLL = 5,
PPUADDR = 6,
PPUDATA = 7,
OAMDMA = 8
}
impl From<u16> for Register {
fn from(n: u16) -> Register {
unsafe { std::mem::transmute(n) } // hehe
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
PreRender,
Render,
PostRender,
VBlank
}
#[allow(dead_code)]
pub struct PPU {
vram: [u8; kilobytes::KB2], // AKA CIRAM, AKA nametables
vram_mirroring: Mirroring,
rom_mapper: Rc<RefCell<dyn Bus>>,
ctrl: ControlRegister,
status: StatusRegister,
mask: MaskRegister,
openbus: OpenBus,
palettes: [u8; PALETTE_SIZE],
oam: [u8; 256],
oam_address: u8,
cycle: usize,
scanline: usize,
frame: RenderFrame,
nmi_pending: bool,
first_write: Cell<bool>,
v: Cell<u16>,
t: Cell<u16>,
data_buffer: Cell<u8>,
frame_ready: bool,
}
#[allow(dead_code)]
impl PPU {
const BLARRG_PALETTE: [u8; PALETTE_SIZE] = [
0x09,0x01,0x00,0x01,
0x00,0x02,0x02,0x0D,
0x08,0x10,0x08,0x24,
0x00,0x00,0x04,0x2C,
0x09,0x01,0x34,0x03,
0x00,0x04,0x00,0x14,
0x08,0x3A,0x00,0x02,
0x00,0x20,0x2C,0x08
];
pub fn new(mapper: Rc<RefCell<dyn Bus>>, mirroring: Mirroring) -> PPU {
PPU {
rom_mapper: mapper,
vram: [0; kilobytes::KB2],
vram_mirroring: mirroring,
cycle: 21, // from nestest. TODO: will this fuck stuff up?
scanline: 0,
ctrl: ControlRegister::new(),
status: StatusRegister::new(),
mask: MaskRegister::default(),
palettes: Self::BLARRG_PALETTE,
frame: RenderFrame::new(),
openbus: OpenBus::default(),
nmi_pending: false,
oam: [0; 256],
oam_address: 0,
first_write: Cell::new(true),
v: Cell::new(0),
t: Cell::new(0),
data_buffer: Cell::new(0),
frame_ready: false,
}
}
fn inc_vram(&self) {
self.v.set(self.v.get() + self.ctrl.vram_inc());
}
pub fn cpu_read_register(&self, address: u16) -> u8 {
let ppu_reg: Register = address.into();
match ppu_reg {
Register::PPUSTATUS => {
self.first_write.set(true);
self.status.read(&self.openbus)
}
Register::PPUDATA => {
let value = self.internal_read(self.v.get());
let return_value = match self.v.get() {
0..=0x3eff => self.data_buffer.get(),
_ => (value & 0b00111111) | (self.openbus.read() & 0b11000000) // palette, high 2 bits should be from decay
};
self.data_buffer.set(value);
self.inc_vram();
self.openbus.write(return_value);
return_value
}
Register::OAMDATA => self.oam[self.oam_address as usize],
_ => self.openbus.read()
}
}
pub fn cpu_write_register(&mut self, val: u8, address: u16) {
// println!("write {:#06x}", address);
self.openbus.write(val);
let ppu_reg: Register = address.into();
match ppu_reg {
Register::PPUCTRL => {
self.nmi_pending = self.ctrl.write(&self.status, val);
}
Register::PPUSCROLL => {
self.first_write.set(!self.first_write.get());
}
Register::PPUADDR => {
let t = self.t.get_mut();
let v = self.v.get_mut();
if self.first_write.get() {
*t = (val as u16) << 8 | (*t & 0x00ff);
}
else {
*t = (*t & 0xFF00) | val as u16;
*v = *t;
}
self.first_write.set(!self.first_write.get());
}
Register::PPUDATA => {
let vram_address = self.v.get();
self.internal_write(val, vram_address);
self.inc_vram();
}
Register::PPUMASK => self.mask.write(val),
Register::OAMADDR => self.oam_address = val,
Register::OAMDATA => {
self.oam[self.oam_address as usize] = val;
self.oam_address = self.oam_address.wrapping_add(1);
}
_ => ()
}
}
pub fn tick(&mut self, ppu_cycles_to_tick: usize) {
for _ in 0..ppu_cycles_to_tick {
self.openbus.tick_for_decay();
if self.mask.show_background() || self.mask.show_background_left() {
self.render_background_pixel();
}
if self.mask.show_sprites() || self.mask.show_sprites_left() {
self.render_sprite_pixel();
}
if self.cycle == 1 {
if self.scanline == 241 {
self.status.set_vblank(true);
if self.ctrl.generate_nmi_at_vblank_interval() {
self.nmi_pending = true;
}
} else if self.scanline == 261 {
self.frame_ready = true;
self.status.set_vblank(false);
}
}
self.cycle += 1;
if self.cycle > 340 {
self.cycle = 0;
self.scanline += 1;
if self.scanline > 261 {
self.scanline = 0;
}
}
}
}
fn render_background_pixel(&mut self) {
let x = self.cycle as u16;
let y = self.scanline as u16;
if x > 256 || y > 240 {
return
}
let nametable_base = 0x2000;
let yoffset = (y / 8) * 32;
let xoffset = x / 8;
let address = nametable_base + xoffset + yoffset;
let bg_offset = if self.ctrl.background_table_address() != 0 { 256 } else { 0 };
let tile = self.internal_read(address) as u16 + bg_offset;
let attr = self.lookup_attribute_table(address);
let row = y % 8;
let plane1 = self.internal_read(tile * 16 + row);
let plane2 = self.internal_read(tile * 16 + row + 8);
let col = x % 8;
let a = if (plane1 & (1 << col)) != 0 { 1 } else { 0 };
let b = if (plane2 & (1 << col)) != 0 { 2 } else { 0 };
let palette_index = a + b;
let mut color_index = self.palettes[(attr * 4 + palette_index) as usize];
if palette_index == 0 {
color_index = self.palettes[0]; // TODO?
}
let pixel = palette::palette_to_rgb(color_index);
let reverse_x = (x - col) + (7 - col);
self.frame.set_pixel(reverse_x as usize, y as usize, pixel);
}
fn render_sprite_pixel(&mut self) {
if self.cycle > 256 || self.scanline > 240 {
return
}
for oam_index in (0..64).step_by(4) { // TODO
let y = self.oam[oam_index];
let sprite_index = self.oam[oam_index + 1];
let attrs = self.oam[oam_index + 2];
let x = self.oam[oam_index + 3];
// is it visible?
if x >= 249 || y >= 239 {
continue;
}
let offset = if self.ctrl.sprite_table_address() != 0 { 256 } else { 0 };
let tile: u16 = sprite_index as u16 + offset;
let flip_x = attrs & 0b01000000;
let flip_y = attrs & 0b10000000;
let row = (self.scanline % 8) as u16;
let plane1 = self.internal_read(tile * 16 + row);
let plane2 = self.internal_read(tile * 16 + row + 8);
let col = self.cycle % 8;
let a = if (plane1 & (1 << col)) != 0 { 1 } else { 0 };
let b = if (plane2 & (1 << col)) != 0 { 2 } else { 0 };
let palette_index = a + b;
let color_index = self.palettes[(0x10 + (attrs & 0x03) * 4 + palette_index) as usize];
if palette_index == 0 { // transparent??
continue;
}
let rgb = palette::palette_to_rgb(color_index);
let x_offset = if flip_x == 0 { 7 - col as usize } else { col as usize };
let y_offset = if flip_y == 0 { row as usize } else { 7 - row as usize };
self.frame.set_pixel(x as usize + x_offset, y as usize + y_offset, rgb);
}
}
fn lookup_attribute_table(&mut self, vram_address: u16) -> u8 {
// 32x32 attr table address
let row = ((vram_address & 0x3e0) >> 5) / 4;
let col = (vram_address & 0x1f) / 4;
// 16x16 metatile??
let a = if (vram_address & 0b01000000) != 0 { 4 } else { 0 };
let b = if (vram_address & 0b00000010) != 0 { 2 } else { 0 };
let shift = a + b;
// attr table offset
let offset = (vram_address & 0xc00) + 0x400 - 64 + (row * 8 + col);
(self.vram[offset as usize] & (0b0000011 << shift)) >> shift
}
pub fn cpu_oam_dma(&mut self, mem: &[u8]) {
assert!(mem.len() == 256);
for byte in mem {
self.oam[self.oam_address as usize] = *byte;
self.oam_address = self.oam_address.wrapping_add(1);
}
}
pub fn frame(&self) -> &RenderFrame {
&self.frame
}
pub fn scanline(&self) -> usize {
self.scanline
}
pub fn current_cycle(&self) -> usize {
self.cycle
}
pub fn is_nmi_pending(&self) -> bool {
self.nmi_pending
}
pub fn clear_pending_nmi(&mut self) {
self.nmi_pending = false;
}
pub fn frame_ready_to_render(&self) -> bool {
self.frame_ready
}
pub fn clear_frame_ready(&mut self) {
self.frame_ready = false;
}
fn mirror_vram(mode: &Mirroring, vram_address: u16) -> u16 {
// https://www.nesdev.org/wiki/Mirroring#Nametable_Mirroring
// https://www.nesdev.org/wiki/PPU_nametables
// substract the 0x2000 base for vram, divide by nametable size (1kb) to get the table index.
let name_table = (vram_address - 0x2000) as usize / common::kilobytes::KB1;
let mapped_address = match (&mode, name_table) {
(Mirroring::Vertical, 0) => vram_address,
(Mirroring::Vertical, 1) => vram_address,
(Mirroring::Vertical, 2) => vram_address - common::kilobytes::KB2 as u16,
(Mirroring::Vertical, 3) => vram_address - common::kilobytes::KB2 as u16,
(Mirroring::Horizontal, 0) => vram_address,
(Mirroring::Horizontal, 1) => vram_address - common::kilobytes::KB1 as u16,
(Mirroring::Horizontal, 2) => vram_address - common::kilobytes::KB1 as u16,
(Mirroring::Horizontal, 3) => vram_address - common::kilobytes::KB2 as u16,
_ => panic!("nametable mirroring? {}", name_table) //vram_address,
};
// substract vram base because the bus is gonna index the 2kb array directly.
mapped_address - 0x2000
}
fn internal_read(&self, address: u16) -> u8 {
match address {
0x0000..=0x1fff => self.rom_mapper.borrow().read8(address), // CHR
0x2000..=0x2fff => self.vram[Self::mirror_vram(&self.vram_mirroring, address) as usize],
0x3000..=0x3eff => self.vram[Self::mirror_vram(&self.vram_mirroring, address - 0x1000) as usize], // -0x1000 because mirror_vram expects base 0x2000
0x3f00..=0x3fff => {
// Palette incl mirrors
let i = address as usize % PALETTE_SIZE;
self.palettes[i]
},
_ => 0
}
}
fn internal_write(&mut self, val: u8, address: u16) {
match address {
0x0000..=0x1fff => self.rom_mapper.borrow_mut().write8(val, address), // CHR RAM
0x2000..=0x2fff => self.vram[Self::mirror_vram(&self.vram_mirroring, address) as usize] = val,
0x3000..=0x3eff => self.vram[Self::mirror_vram(&self.vram_mirroring, address - 0x1000) as usize] = val, // -0x1000 because mirror_vram expects base 0x2000
0x3f00..=0x3fff => {
// Palette incl mirrors
let i = address as usize % PALETTE_SIZE;
self.palettes[i] = val;
},
_ => ()
}
}
}
#[cfg(test)]
mod tests {
use crate::cartridge::Mirroring;
use super::PPU;
#[test]
fn vram_mirror() {
let nametable1_base = 0; // 0x2000
let nametable2_base = 0x400; // 0x2800
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2400) == nametable1_base);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2401) == nametable1_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2000) == nametable1_base);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2001) == nametable1_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x24ff) == nametable1_base + 0xff);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2800) == nametable2_base);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2801) == nametable2_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x28ff) == nametable2_base + 0xff);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2c00) == nametable2_base);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2c01) == nametable2_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Horizontal, 0x2cff) == nametable2_base + 0xff);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2000) == nametable1_base);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2800) == nametable1_base);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2801) == nametable1_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x28ff) == nametable1_base + 0xff);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2001) == nametable1_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x24ff) == nametable2_base + 0xff);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2c00) == nametable2_base);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2c01) == nametable2_base + 1);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2cff) == nametable2_base + 0xff);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2400) == nametable2_base);
assert!(PPU::mirror_vram(&Mirroring::Vertical, 0x2401) == nametable2_base + 1);
}
fn map(x: u8, y: u8) -> u16 {
let yoffset = (y / 8) * 32;
let xoffset = x / 8;
0x2000 + xoffset as u16 + yoffset as u16
}
fn map_real(x: u8, y: u8) -> u16 {
let yoffset = (y / 8) * 32;
let xoffset = x / 8;
0x2000 + xoffset as u16 + yoffset as u16
}
#[test]
fn ixymap() {
assert!(map(0, 0) == 0x2000);
assert!(map(8, 0) == 0x2001);
assert!(map(248, 0) == 0x201f);
assert!(map(0, 8) == 0x2020);
assert!(map(8, 8) == 0x2021);
}
#[test]
fn ixymap_real() {
assert!(map_real(7, 0) == 0x2000);
assert!(map_real(7, 1) == 0x2000);
assert!(map_real(0, 7) == 0x2000);
assert!(map_real(15, 0) == 0x2001);
assert!(map_real(9, 3) == 0x2001);
}
}

305
nes/src/ppu/registers.rs Normal file
View file

@ -0,0 +1,305 @@
use bitflags::bitflags;
use std::cell::{RefCell, Cell};
// https://www.nesdev.org/wiki/PPU_programmer_reference#Controller_($2000)_%3E_write
#[derive(Default)]
pub struct OpenBus { // AKA data bus, decay register
data: Cell<u8>,
cycles: usize
}
impl OpenBus {
const CPU_CLOCK_HZ: usize = 1_790_000;
pub fn read(&self) -> u8 {
self.data.get()
}
pub fn write(&self, data: u8) {
self.data.set(data);
}
pub fn tick_for_decay(&mut self) {
self.cycles += 1;
if self.cycles >= Self::CPU_CLOCK_HZ {
self.data.set(0);
self.cycles = 0;
}
}
}
/*
7 bit 0
---- ----
VPHB SINN
|||| ||||
|||| ||++- Base nametable address
|||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
|||| |+--- VRAM address increment per CPU read/write of PPUDATA
|||| | (0: add 1, going across; 1: add 32, going down)
|||| +---- Sprite pattern table address for 8x8 sprites
|||| (0: $0000; 1: $1000; ignored in 8x16 mode)
|||+------ Background pattern table address (0: $0000; 1: $1000)
||+------- Sprite size (0: 8x8 pixels; 1: 8x16 pixels see PPU OAM#Byte 1)
|+-------- PPU master/slave select
| (0: read backdrop from EXT pins; 1: output color on EXT pins)
+--------- Generate an NMI at the start of the
vertical blanking interval (0: off; 1: on)
*/
bitflags! {
pub struct ControlRegister: u8 {
const NAMETABLE1 = 1;
const NAMETABLE2 = 1 << 1;
const VRAM_ADDR_INC_PER_PPUDATA_ACCESS = 1 << 2;
const SPRITE_TABLE_ADDRESS = 1 << 3;
const BACKGROUND_TABLE_ADDRESS = 1 << 4;
const SPRITE_SIZE = 1 << 5;
const MASTER_SLAVE_SELECT = 1 << 6;
const GENERATE_NMI_AT_VBI = 1 << 7;
}
}
impl ControlRegister {
pub fn new() -> ControlRegister {
ControlRegister::empty() // TODO: startup state?
}
// TODO: After power/reset, writes to this register are ignored for about 30,000 cycles.
pub fn write(&mut self, status: &StatusRegister, v: u8) -> bool {
let was_nmi_on = self.generate_nmi_at_vblank_interval();
self.bits = v;
if self.intersects(Self::SPRITE_SIZE | Self::MASTER_SLAVE_SELECT) {
todo!("not yet implemented PPU control flag: {:?}", self)
}
if self.base_table_address() != 0x2000 {
println!("WARNING: BASE NAMETABLE SELECTED: {:#06x}", self.base_table_address());
}
// If the PPU is currently in vertical blank, and the PPUSTATUS ($2002) vblank flag is still set (1),
// changing the NMI flag in bit 7 of $2000 from 0 to 1 will immediately generate an NMI.
let trigger_nmi = status.in_vblank() && !was_nmi_on && self.generate_nmi_at_vblank_interval();
trigger_nmi
}
pub fn generate_nmi_at_vblank_interval(&self) -> bool {
self.contains(ControlRegister::GENERATE_NMI_AT_VBI)
}
pub fn vram_inc(&self) -> u16 {
match self.contains(ControlRegister::VRAM_ADDR_INC_PER_PPUDATA_ACCESS) {
false => 1,
true => 32,
}
}
pub fn base_table_address(&self) -> u16 {
match self.bits & 0b00000011 {
// (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
0 => 0x2000,
1 => 0x2400,
2 => 0x2800,
3 => 0x2c00,
_ => unreachable!()
}
}
pub fn background_table_address(&self) -> u16 {
if self.contains(ControlRegister::BACKGROUND_TABLE_ADDRESS) {
0x1000
} else {
0x0000
}
}
pub fn sprite_table_address(&self) -> u16 {
if self.contains(ControlRegister::SPRITE_TABLE_ADDRESS) {
0x1000
} else {
0x0000
}
}
}
/*
7 bit 0
---- ----
BGRs bMmG
|||| ||||
|||| |||+- Greyscale (0: normal color, 1: produce a greyscale display)
|||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide
|||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide
|||| +---- 1: Show background
|||+------ 1: Show sprites
||+------- Emphasize red (green on PAL/Dendy)
|+-------- Emphasize green (red on PAL/Dendy)
+--------- Emphasize blue
*/
bitflags! {
#[derive(Default)]
pub struct MaskRegister: u8 {
const GRAYSCALE = 1;
const SHOW_BACKGROUND_LEFT = 1 << 1;
const SHOW_SPRITES_LEFT = 1 << 2;
const SHOW_BACKGROUND = 1 << 3;
const SHOW_SPRITES = 1 << 4;
const MORE_RED = 1 << 5;
const MORE_GREEN = 1 << 6;
const MORE_BLUE = 1 << 7;
}
}
#[allow(dead_code)]
impl MaskRegister {
pub fn show_background(self) -> bool {
self.contains(MaskRegister::SHOW_BACKGROUND)
}
pub fn show_background_left(self) -> bool {
self.contains(MaskRegister::SHOW_BACKGROUND_LEFT)
}
pub fn show_sprites(self) -> bool {
self.contains(MaskRegister::SHOW_SPRITES)
}
pub fn show_sprites_left(self) -> bool {
self.contains(MaskRegister::SHOW_SPRITES_LEFT)
}
pub fn write(&mut self, v: u8) {
self.bits = v;
}
}
/*
7 bit 0
---- ----
VSO. ....
|||| ||||
|||+-++++- Least significant bits previously written into a PPU register
||| (due to register not being updated for this address)
||+------- Sprite overflow. The intent was for this flag to be set
|| whenever more than eight sprites appear on a scanline, but a
|| hardware bug causes the actual behavior to be more complicated
|| and generate false positives as well as false negatives; see
|| PPU sprite evaluation. This flag is set during sprite
|| evaluation and cleared at dot 1 (the second dot) of the
|| pre-render line.
|+-------- Sprite 0 Hit. Set when a nonzero pixel of sprite 0 overlaps
| a nonzero background pixel; cleared at dot 1 of the pre-render
| line. Used for raster timing.
+--------- Vertical blank has started (0: not in vblank; 1: in vblank).
Set at dot 1 of line 241 (the line *after* the post-render
line); cleared after reading $2002 and at dot 1 of the
pre-render line.
*/
bitflags! {
struct StatusFlags: u8 {
const SPRITE_OVERFLOW = 0b00100000;
const SPRITE_ZERO_HIT = 0b01000000;
const IN_VLBANK = 0b10000000;
}
}
pub struct StatusRegister {
inner: RefCell<StatusFlags>
}
impl StatusRegister {
pub fn new() -> StatusRegister {
StatusRegister {
inner: RefCell::new(StatusFlags::empty()) // TODO: startup state?
}
}
pub fn set_vblank(&self, in_vblank: bool) {
self.inner.borrow_mut().set(StatusFlags::IN_VLBANK, in_vblank);
}
pub fn in_vblank(&self) -> bool {
self.inner.borrow().contains(StatusFlags::IN_VLBANK)
}
pub fn read(&self, openbus: &OpenBus) -> u8 {
let status = self.inner.borrow().bits;
// Reading 2002 should clear vblank
self.set_vblank(false);
// Low 5 bits of $2002 should be from decay value.
// see tests/ppu_open_bus/readme
let busdata = openbus.read();
let status_bus_combined = (status & 0b11100000) | (busdata & 0b00011111);
openbus.write(status_bus_combined);
status_bus_combined
}
}
#[cfg(test)]
mod tests {
use crate::ppu::registers::{StatusRegister, OpenBus, ControlRegister};
#[test]
fn read_status_decay_lower_5() {
let openbus = OpenBus::default();
let status = StatusRegister::new();
status.set_vblank(true);
assert!(status.read(&openbus) == 0x80);
// first read should clear
assert!(status.read(&openbus) == 0x00);
status.set_vblank(true);
openbus.write(0b00101010);
assert!(status.read(&openbus) == 0b10001010);
// vbl cleared
assert!(status.read(&openbus) == 0b00001010);
// bus does not mess with vbl
status.set_vblank(true);
openbus.write(0);
assert!(status.in_vblank() == true);
assert!(status.read(&openbus) == 0x80);
// also updates the bus
status.set_vblank(true);
openbus.write(0);
status.read(&openbus);
assert!(openbus.read() == 0x80);
}
/*
|||| ||++- Base nametable address
|||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
*/
#[test]
fn ctrl_nametable_base() {
let mut reg = ControlRegister::new();
reg.bits = 0;
assert_eq!(reg.base_table_address(), 0x2000);
reg.bits = 0b00000001;
assert_eq!(reg.base_table_address(), 0x2400);
reg.bits = 0b00000010;
assert_eq!(reg.base_table_address(), 0x2800);
reg.bits = 0b10000011;
assert_eq!(reg.base_table_address(), 0x2c00);
reg.bits = 0b00000111;
assert_eq!(reg.base_table_address(), 0x2c00);
}
}

95
nes/tests/blargg.rs Normal file
View file

@ -0,0 +1,95 @@
use nes::nes::Nes;
mod common;
const STATUS_RUNNING: u8 = 0x80;
const STATUS_NEEDS_RESET: u8 = 0x81;
const STATUS_SUCCESS: u8 = 0x00;
const VALID_MAGIC: [u8; 3] = [0xde, 0xb0, 0x61];
#[test]
fn instr_test_v5_official_mmc1() {
run_blargg_test("instr_test-v5/official_only.nes", "All 16 tests passed", STATUS_SUCCESS)
}
#[test]
fn instr_test_v5_basic_nrom() {
run_blargg_test("instr_test-v5/rom_singles/01-basics.nes", "01-basics\n\nPassed", STATUS_SUCCESS);
}
#[test]
fn instr_misc() {
// The last steps tests dummy reads on PPU. Not yet implemented.
let success = "Test requires $2002 mirroring every 8 bytes to $3FFA\n\n03-dummy_reads\n\nFailed #2\n\nWhile running test";
run_blargg_test("instr_misc/instr_misc.nes", success, 1);
}
#[test]
fn ppu_vbl_nmi() {
let success = "VBL period is too long with BG off\n\n01-vbl_basics\n\nFailed #8";
// let success = "$2002 should be mirrored at $200A\n\n01-vbl_basics\n\nFailed #5";
run_blargg_test("ppu_vbl_nmi/rom_singles/01-vbl_basics.nes", success, 0x08);
}
#[test]
fn ppu_open_bus() {
let success = "Bits 2-4 of sprite attributes should always be clear when read\n\nppu_open_bus\n\nFailed #10";
run_blargg_test("ppu_open_bus/ppu_open_bus.nes", success, 0x0a);
}
#[test]
#[ignore = "impl ppu, mapper 3"]
fn ppu_read_buffer() {
run_blargg_test("ppu_read_buffer/test_ppu_read_buffer.nes", "dunno", STATUS_SUCCESS);
}
fn run_blargg_test(test: &str, success_string: &str, success_status: u8) {
let path = format!("../test-roms/nes-test-roms/{}", test);
let mut nes = common::setup(path.into(), std::env::var("VERBOSE").is_ok());
let result: String;
let mut status: Option<u8> = None;
// nes.debugger().enable();
nes.debugger().watch_memory_range(0x6004..=0x6004+100, |mem| {
println!("{}", read_null_terminated_string(&mem));
});
loop {
nes.tick();
if check_and_update_status(&nes, &mut status) {
match status {
Some(STATUS_RUNNING) => (),
Some(STATUS_NEEDS_RESET) => panic!("needs reset.."),
Some(0x00..=0x7F) => { // Completed, status is the result code.
let mem_view = nes.bus().read_range(0x6004..=0x6004+100);
result = read_null_terminated_string(&mem_view);
break
},
_ => panic!("unknown status")
}
}
}
println!("status code: {:#04x}", status.unwrap());
assert_eq!(success_string, result.trim());
assert_eq!(Some(success_status), status);
}
fn read_null_terminated_string(range: &[u8]) -> String {
let string: Vec<u8> = range.iter().take_while(|&b| *b != 0x00).cloned().collect();
String::from_utf8(string).unwrap()
}
fn check_and_update_status(nes: &Nes, current_status: &mut Option<u8>) -> bool {
let mem = nes.cpu().bus();
if mem.read_range(0x6001..=0x6003) == VALID_MAGIC {
let new_status = mem.read8(0x6000);
if Some(new_status) != *current_status {
*current_status = Some(new_status);
return true;
}
}
false
}

10
nes/tests/common.rs Normal file
View file

@ -0,0 +1,10 @@
use std::path::PathBuf;
use nes::{cartridge::Cartridge, nes::Nes};
pub fn setup(path: PathBuf, verbose: bool) -> Nes {
let cartridge = Cartridge::blow_dust(path).expect("failed to map rom");
let mut nes = Nes::insert_headless_host(cartridge);
nes.debugger().verbose(verbose);
nes
}

63
nes/tests/nestest.rs Normal file
View file

@ -0,0 +1,63 @@
use mos6502::cpu::{Flag, Reg};
use std::{fmt::Write, fs::File, io::{BufReader, BufRead}};
mod common;
// cat nes/roms/nestest.log | awk '{print substr(,49)}' > nes/roms/nestest_cycles.log
// cat nes/roms/nestest.log | awk '{printf "%s ",substr(,0,4); print substr(,49)}; ' > nes/roms/nestest_cycles.log
// http://www.qmtpro.com/~nes/misc/nestest.txt
// This test program, when run on "automation", (i.e. set your program counter
// to 0c000h) will perform all tests in sequence and shove the results of
// the tests into locations 02h and 03h.
const NESTEST_ENTRY_POINT: u16 = 0xc000;
const NESTEST_SUCCESS: u16 = 0xc68b; // Here it starts writing to APU, which is not yet implemented.
const NESTEST_RES_BYTE2: u16 = 0x0002;
const NESTEST_RES_BYTE3: u16 = 0x0003;
const ENABLE_TEST_CYCLES: bool = true;
#[test]
fn nestest() {
let mut nes = common::setup("../test-roms/nestest/nestest.nes".into(), false);
let logf = File::open("../test-roms/nestest/nestest_cycles.log").expect("failed to read test log");
let log: Vec<String> = BufReader::new(logf).lines().map(|s| s.unwrap()).collect();
// nes.machine().debugger().enable();
// nestest startup state
// reset vector points to 0xc004 - but that's for graphic mode, we want automation at 0xc000
nes.cpu_mut().set_pc(NESTEST_ENTRY_POINT);
// nestest startups with these flags... Maybe the CPU should as well? or only for this weird test?
nes.cpu_mut()[Flag::B] = 0;
nes.cpu_mut()[Flag::UNUSED] = 1;
nes.cpu_mut()[Flag::I] = 1;
nes.cpu_mut()[Reg::SP] = 0xfd;
nes.debugger().watch_memory_range(NESTEST_RES_BYTE2..=NESTEST_RES_BYTE3, |result| {
assert_eq!(result[0], 0x00, "nestest reports error code on byte 2.. check README");
assert_eq!(result[1], 0x00, "nestest reports error code on byte 3.. check README");
});
let mut i = 0;
loop {
let mut sts = String::new();
write!(&mut sts, "{:?}", nes).unwrap();
nes.tick();
if log[i] != sts && ENABLE_TEST_CYCLES {
nes.debugger().dump_backtrace();
panic!("nestest cycle test mismatch!\n\nExpected:\t{}\nActual:\t\t{}\n", log[i], sts);
}
i += 1;
if nes.cpu().pc() == NESTEST_SUCCESS {
break;
}
}
let expected_ticks = 8980;
assert_eq!(expected_ticks, nes.cpu_ticks());
}

BIN
screenshots/dk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

BIN
screenshots/nestest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 KiB

BIN
screenshots/pm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

@ -0,0 +1 @@
Subproject commit 966b1a35049f9d8be44ad092ec6d43d5ba1831b3

BIN
test-roms/bin/TTL6502.bin Normal file

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
Modules list:
-------------
extended_test.o:
CODE Offs=000000 Size=002487 Align=00001 Fill=0000
DATA Offs=000000 Size=000063 Align=00001 Fill=0000
ZEROPAGE Offs=000000 Size=00005A Align=00001 Fill=0000
VECTORS Offs=000000 Size=000006 Align=00001 Fill=0000
Segment list:
-------------
Name Start End Size Align
----------------------------------------------------
ZEROPAGE 000000 000059 00005A 00001
DATA 000200 000262 000063 00001
CODE 000400 002886 002487 00001
VECTORS 00FFFA 00FFFF 000006 00001
Exports list by name:
---------------------
Exports list by value:
----------------------
Imports list:
-------------

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
Modules list:
-------------
functional_test_bcd_disabled.o:
CODE Offs=000000 Size=003205 Align=00001 Fill=0000
DATA Offs=000000 Size=00007B Align=00001 Fill=0000
ZEROPAGE Offs=000000 Size=00005C Align=00001 Fill=0000
VECTORS Offs=000000 Size=000006 Align=00001 Fill=0000
Segment list:
-------------
Name Start End Size Align
----------------------------------------------------
ZEROPAGE 000000 00005B 00005C 00001
DATA 000200 00027A 00007B 00001
CODE 000400 003604 003205 00001
VECTORS 00FFFA 00FFFF 000006 00001
Exports list by name:
---------------------
Exports list by value:
----------------------
Imports list:
-------------

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
Modules list:
-------------
functional_test_full.o:
CODE Offs=000000 Size=003435 Align=00001 Fill=0000
DATA Offs=000000 Size=00007B Align=00001 Fill=0000
ZEROPAGE Offs=000000 Size=00005C Align=00001 Fill=0000
VECTORS Offs=000000 Size=000006 Align=00001 Fill=0000
Segment list:
-------------
Name Start End Size Align
----------------------------------------------------
ZEROPAGE 000000 00005B 00005C 00001
DATA 000200 00027A 00007B 00001
CODE 000400 003834 003435 00001
VECTORS 00FFFA 00FFFF 000006 00001
Exports list by name:
---------------------
Exports list by value:
----------------------
Imports list:
-------------

Binary file not shown.

33
test-roms/build-tests.sh Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
git submodule sync 6502_65C02_functional_tests
git submodule update 6502_65C02_functional_tests
SRC_DIR='./6502_65C02_functional_tests/ca65'
CONFIG="$SRC_DIR/example.cfg"
OUT_DIR='./bin'
FULL_TEST='6502_functional_test'
FULL_TEST_OUT='functional_test_full'
echo "$SRC_DIR/$FULL_TEST -> $OUT_DIR/$FULL_TEST_OUT"
ca65 -l "$OUT_DIR/$FULL_TEST_OUT.lst" "$SRC_DIR/$FULL_TEST.ca65" -o "$OUT_DIR/$FULL_TEST_OUT.o"
ld65 "$OUT_DIR/$FULL_TEST_OUT.o" -o "$OUT_DIR/$FULL_TEST_OUT.bin" -m "$OUT_DIR/$FULL_TEST_OUT.map" -C $CONFIG
echo "done"
NO_BCD_TEST_OUT='functional_test_bcd_disabled'
TMP='./tmp.ca65'
sed 's/disable_decimal = 0/disable_decimal = 1/' "$SRC_DIR/$FULL_TEST.ca65" > $TMP
echo "$SRC_DIR/$FULL_TEST (bcd disabled) -> $OUT_DIR/$NO_BCD_TEST_OUT"
ca65 -l "$OUT_DIR/$NO_BCD_TEST_OUT.lst" $TMP -o "$OUT_DIR/$NO_BCD_TEST_OUT.o"
ld65 "$OUT_DIR/$NO_BCD_TEST_OUT.o" -o "$OUT_DIR/$NO_BCD_TEST_OUT.bin" -m "$OUT_DIR/$NO_BCD_TEST_OUT.map" -C $CONFIG
echo "done"
EXTENDED_TEST='65C02_extended_opcodes_test'
EXTENDED_TEST_OUT='extended_test'
echo "$SRC_DIR/$EXTENDED_TEST -> $OUT_DIR/$EXTENDED_TEST_OUT"
ca65 -l "$OUT_DIR/$EXTENDED_TEST_OUT.lst" "$SRC_DIR/$EXTENDED_TEST.ca65" -o "$OUT_DIR/$EXTENDED_TEST_OUT.o"
ld65 "$OUT_DIR/$EXTENDED_TEST_OUT.o" -o "$OUT_DIR/$EXTENDED_TEST_OUT.bin" -m "$OUT_DIR/$EXTENDED_TEST_OUT.map" -C $CONFIG
echo "done"
rm $TMP

@ -0,0 +1 @@
Subproject commit 95d8f621ae55cee0d09b91519a8989ae0e64753b

View file

@ -0,0 +1,7 @@
From https://www.nesdev.org/wiki/Emulator_tests
nestest.nes:
fairly thoroughly tests CPU operation. This is the best test to start with when getting a CPU emulator working for the first time. Start execution at $C000 and compare execution with a known good log (created using Nintendulator, an emulator chosen by the test's author because its CPU was verified to function correctly, aside from some minor details of the power-up state).
instr_test-v5:
Tests official and unofficial CPU instructions and lists which ones failed. It will work even if emulator has no PPU and only supports NROM, writing a copy of output to $6000 (see readme). This more thoroughly tests instructions, but can't help you figure out what's wrong beyond what instruction(s) are failing, so it's better for testing mature CPU emulators.

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,724 @@
The ultimate NES CPU test ROM
-----------------------------
V1.00 - 09/06/04
By: Kevin Horton
---
What it is:
This here is a pretty much all inclusive test suite for a NES CPU.
It was designed to test almost every combination of flags, instructions,
and registers. Some of these tests are very difficult, and so far,
Nesten and Nesticle failed it. Nintendulator passes, as does a real
NES (naturally). I haven't tested it with any more emualtors yet.
I attempted to check the states of all flags after most instructions.
For example, CPY and CMP shouldn't affect the overflow flag, while SBC
and ADC should. Likewise, all forms of wrapping ARE tested for- zeropage
wrapping being the tests most emulators fail.
i.e.
LDA #001h
LDA 0ffh,X ;indexed zeropage read
should read the byte from 000h... NOT 0100h. This is because zeropage
instructions cannot cross a page boundary.
---
How to work it good:
Simply run the .NES ROM on your emulator of choice. You can select a single
test to run, or you can run ALL tests in sequence by selecting the
appropriate option.
Pressing Select will change pages and allow testing "invalid" opcodes.
Be aware that these will crash alot of emulators <cough>Nesten<cough>.
Once a test completes, the result will be "OK" if the test passes, or a
2 digit hex number which indicates a failure of some kind. A list is
provided below for the failure and its cause. For a more detailed reason
for the failure, you should check out the .ASM file included with this
document.
If the entire page of tests succeeds, "OK" will be displayed next to the
first entry on the page. If one or more tests fails, "Er" will be displayed
instead.
---
NSF player testing:
This ROM is set up to be usable inside an NSF player. It outputs the
results of the test audially. <to be finished>
---
Emulator authors:
This test program, when run on "automation", (i.e. set your program counter
to 0c000h) will perform all tests in sequence and shove the results of
the tests into locations 02h and 03h.
---
Final notes:
The hex numbers shown on the screen (or stored in the above mentioned
memory locations) are of the LAST test that failed in the group tested.
This means, there could be multiple failures in one or more groups. This
wasn't the best solution, but since there are close to 400 tests performed,
any other way wouldn't have had acceptable memory usage. So long as your
emulator bugs are fixed and the numbers are getting smaller, you're doing
good :-)
----------------------------------------
Test failure codes and what they mean:
(byte 02h only)
000h - tests completed successfully
branch tests
------------
001h - BCS failed to branch
002h - BCS branched when it shouldn't have
003h - BCC branched when it shouldn't have
004h - BCC failed to branch
005h - BEQ failed to branch
006h - BEQ branched when it shouldn't have
007h - BNE failed to branch
008h - BNE branched when it shouldn't have
009h - BVS failed to branch
00Ah - BVC branched when it shouldn't have
00Bh - BVC failed to branch
00Ch - BVS branched when it shouldn't have
00Dh - BPL failed to branch
00Eh - BPL branched when it shouldn't have
00Fh - BMI failed to branch
010h - BMI branched when it shouldn't have
flag tests
----------
011h - PHP/flags failure (bits set)
012h - PHP/flags failure (bits clear)
013h - PHP/flags failure (misc bit states)
014h - PLP/flags failure (misc bit states)
015h - PLP/flags failure (misc bit states)
016h - PHA/PLA failure (PLA didn't affect Z and N properly)
017h - PHA/PLA failure (PLA didn't affect Z and N properly)
immediate instruction tests
---------------------------
018h - ORA # failure
019h - ORA # failure
01Ah - AND # failure
01Bh - AND # failure
01Ch - EOR # failure
01Dh - EOR # failure
01Eh - ADC # failure (overflow/carry problems)
01Fh - ADC # failure (decimal mode was turned on)
020h - ADC # failure
021h - ADC # failure
022h - ADC # failure
023h - LDA # failure (didn't set N and Z correctly)
024h - LDA # failure (didn't set N and Z correctly)
025h - CMP # failure (messed up flags)
026h - CMP # failure (messed up flags)
027h - CMP # failure (messed up flags)
028h - CMP # failure (messed up flags)
029h - CMP # failure (messed up flags)
02Ah - CMP # failure (messed up flags)
02Bh - CPY # failure (messed up flags)
02Ch - CPY # failure (messed up flags)
02Dh - CPY # failure (messed up flags)
02Eh - CPY # failure (messed up flags)
02Fh - CPY # failure (messed up flags)
030h - CPY # failure (messed up flags)
031h - CPY # failure (messed up flags)
032h - CPX # failure (messed up flags)
033h - CPX # failure (messed up flags)
034h - CPX # failure (messed up flags)
035h - CPX # failure (messed up flags)
036h - CPX # failure (messed up flags)
037h - CPX # failure (messed up flags)
038h - CPX # failure (messed up flags)
039h - LDX # failure (didn't set N and Z correctly)
03Ah - LDX # failure (didn't set N and Z correctly)
03Bh - LDY # failure (didn't set N and Z correctly)
03Ch - LDY # failure (didn't set N and Z correctly)
03Dh - compare(s) stored the result in a register (whoops!)
071h - SBC # failure
072h - SBC # failure
073h - SBC # failure
074h - SBC # failure
075h - SBC # failure
implied instruction tests
-------------------------
03Eh - INX/DEX/INY/DEY did something bad
03Fh - INY/DEY messed up overflow or carry
040h - INX/DEX messed up overflow or carry
041h - TAY did something bad (changed wrong regs, messed up flags)
042h - TAX did something bad (changed wrong regs, messed up flags)
043h - TYA did something bad (changed wrong regs, messed up flags)
044h - TXA did something bad (changed wrong regs, messed up flags)
045h - TXS didn't set flags right, or TSX touched flags and it shouldn't have
stack tests
-----------
046h - wrong data popped, or data not in right location on stack
047h - JSR didn't work as expected
048h - RTS/JSR shouldn't have affected flags
049h - RTI/RTS didn't work right when return addys/data were manually pushed
accumulator tests
-----------------
04Ah - LSR A failed
04Bh - ASL A failed
04Ch - ROR A failed
04Dh - ROL A failed
(indirect,x) tests
------------------
058h - LDA didn't load the data it expected to load
059h - STA didn't store the data where it was supposed to
05Ah - ORA failure
05Bh - ORA failure
05Ch - AND failure
05Dh - AND failure
05Eh - EOR failure
05Fh - EOR failure
060h - ADC failure
061h - ADC failure
062h - ADC failure
063h - ADC failure
064h - ADC failure
065h - CMP failure
066h - CMP failure
067h - CMP failure
068h - CMP failure
069h - CMP failure
06Ah - CMP failure
06Bh - CMP failure
06Ch - SBC failure
06Dh - SBC failure
06Eh - SBC failure
06Fh - SBC failure
070h - SBC failure
zeropage tests
--------------
076h - LDA didn't set the flags properly
077h - STA affected flags it shouldn't
078h - LDY didn't set the flags properly
079h - STY affected flags it shouldn't
07Ah - LDX didn't set the flags properly
07Bh - STX affected flags it shouldn't
07Ch - BIT failure
07Dh - BIT failure
07Eh - ORA failure
07Fh - ORA failure
080h - AND failure
081h - AND failure
082h - EOR failure
083h - EOR failure
084h - ADC failure
085h - ADC failure
086h - ADC failure
087h - ADC failure
088h - ADC failure
089h - CMP failure
08Ah - CMP failure
08Bh - CMP failure
08Ch - CMP failure
08Dh - CMP failure
08Eh - CMP failure
08Fh - CMP failure
090h - SBC failure
091h - SBC failure
092h - SBC failure
093h - SBC failure
094h - SBC failure
095h - CPX failure
096h - CPX failure
097h - CPX failure
098h - CPX failure
099h - CPX failure
09Ah - CPX failure
09Bh - CPX failure
09Ch - CPY failure
09Dh - CPY failure
09Eh - CPY failure
09Fh - CPY failure
0A0h - CPY failure
0A1h - CPY failure
0A2h - CPY failure
0A3h - LSR failure
0A4h - LSR failure
0A5h - ASL failure
0A6h - ASL failure
0A7h - ROL failure
0A8h - ROL failure
0A9h - ROR failure
0AAh - ROR failure
0ABh - INC failure
0ACh - INC failure
0ADh - DEC failure
0AEh - DEC failure
0AFh - DEC failure
Absolute tests
--------------
0B0h - LDA didn't set the flags properly
0B1h - STA affected flags it shouldn't
0B2h - LDY didn't set the flags properly
0B3h - STY affected flags it shouldn't
0B4h - LDX didn't set the flags properly
0B5h - STX affected flags it shouldn't
0B6h - BIT failure
0B7h - BIT failure
0B8h - ORA failure
0B9h - ORA failure
0BAh - AND failure
0BBh - AND failure
0BCh - EOR failure
0BDh - EOR failure
0BEh - ADC failure
0BFh - ADC failure
0C0h - ADC failure
0C1h - ADC failure
0C2h - ADC failure
0C3h - CMP failure
0C4h - CMP failure
0C5h - CMP failure
0C6h - CMP failure
0C7h - CMP failure
0C8h - CMP failure
0C9h - CMP failure
0CAh - SBC failure
0CBh - SBC failure
0CCh - SBC failure
0CDh - SBC failure
0CEh - SBC failure
0CFh - CPX failure
0D0h - CPX failure
0D1h - CPX failure
0D2h - CPX failure
0D3h - CPX failure
0D4h - CPX failure
0D5h - CPX failure
0D6h - CPY failure
0D7h - CPY failure
0D8h - CPY failure
0D9h - CPY failure
0DAh - CPY failure
0DBh - CPY failure
0DCh - CPY failure
0DDh - LSR failure
0DEh - LSR failure
0DFh - ASL failure
0E0h - ASL failure
0E1h - ROR failure
0E2h - ROR failure
0E3h - ROL failure
0E4h - ROL failure
0E5h - INC failure
0E6h - INC failure
0E7h - DEC failure
0E8h - DEC failure
0E9h - DEC failure
(indirect),y tests
------------------
0EAh - LDA didn't load what it was supposed to
0EBh - read location should've wrapped around ffffh to 0000h
0ECh - should've wrapped zeropage address
0EDh - ORA failure
0EEh - ORA failure
0EFh - AND failure
0F0h - AND failure
0F1h - EOR failure
0F2h - EOR failure
0F3h - ADC failure
0F4h - ADC failure
0F5h - ADC failure
0F6h - ADC failure
0F7h - ADC failure
0F8h - CMP failure
0F9h - CMP failure
0FAh - CMP failure
0FBh - CMP failure
0FCh - CMP failure
0FDh - CMP failure
0FEh - CMP failure
(error byte location 03h starts here)
000h - no error, all tests pass
001h - SBC failure
002h - SBC failure
003h - SBC failure
004h - SBC failure
005h - SBC failure
006h - STA failure
007h - JMP () data reading didn't wrap properly (this fails on a 65C02)
zeropage,x tests
----------------
008h - LDY,X failure
009h - LDY,X failure
00Ah - STY,X failure
00Bh - ORA failure
00Ch - ORA failure
00Dh - AND failure
00Eh - AND failure
00Fh - EOR failure
010h - EOR failure
011h - ADC failure
012h - ADC failure
013h - ADC failure
014h - ADC failure
015h - ADC failure
016h - CMP failure
017h - CMP failure
018h - CMP failure
019h - CMP failure
01Ah - CMP failure
01Bh - CMP failure
01Ch - CMP failure
01Dh - SBC failure
01Eh - SBC failure
01Fh - SBC failure
020h - SBC failure
021h - SBC failure
022h - LDA failure
023h - LDA failure
024h - STA failure
025h - LSR failure
026h - LSR failure
027h - ASL failure
028h - ASL failure
029h - ROR failure
02Ah - ROR failure
02Bh - ROL failure
02Ch - ROL failure
02Dh - INC failure
02Eh - INC failure
02Fh - DEC failure
030h - DEC failure
031h - DEC failure
032h - LDX,Y failure
033h - LDX,Y failure
034h - STX,Y failure
035h - STX,Y failure
absolute,y tests
----------------
036h - LDA failure
037h - LDA failure to wrap properly from ffffh to 0000h
038h - LDA failure, page cross
039h - ORA failure
03Ah - ORA failure
03Bh - AND failure
03Ch - AND failure
03Dh - EOR failure
03Eh - EOR failure
03Fh - ADC failure
040h - ADC failure
041h - ADC failure
042h - ADC failure
043h - ADC failure
044h - CMP failure
045h - CMP failure
046h - CMP failure
047h - CMP failure
048h - CMP failure
049h - CMP failure
04Ah - CMP failure
04Bh - SBC failure
04Ch - SBC failure
04Dh - SBC failure
04Eh - SBC failure
04Fh - SBC failure
050h - STA failure
absolute,x tests
----------------
051h - LDY,X failure
052h - LDY,X failure (didn't page cross)
053h - ORA failure
054h - ORA failure
055h - AND failure
056h - AND failure
057h - EOR failure
058h - EOR failure
059h - ADC failure
05Ah - ADC failure
05Bh - ADC failure
05Ch - ADC failure
05Dh - ADC failure
05Eh - CMP failure
05Fh - CMP failure
060h - CMP failure
061h - CMP failure
062h - CMP failure
063h - CMP failure
064h - CMP failure
065h - SBC failure
066h - SBC failure
067h - SBC failure
068h - SBC failure
069h - SBC failure
06Ah - LDA failure
06Bh - LDA failure (didn't page cross)
06Ch - STA failure
06Dh - LSR failure
06Eh - LSR failure
06Fh - ASL failure
070h - ASL failure
071h - ROR failure
072h - ROR failure
073h - ROL failure
074h - ROL failure
075h - INC failure
076h - INC failure
077h - DEC failure
078h - DEC failure
079h - DEC failure
07Ah - LDX,Y failure
07Bh - LDX,Y failure
------------------------------------
Invalid opcode tests... all errors are reported in byte 03h unless
specified.
NOP - "invalid" opcode tests (error byte 02h)
---------------------------------------------
04Eh - absolute,X NOPs less than 3 bytes long
04Fh - implied NOPs affects regs/flags
050h - ZP,X NOPs less than 2 bytes long
051h - absolute NOP less than 3 bytes long
052h - ZP NOPs less than 2 bytes long
053h - absolute,X NOPs less than 3 bytes long
054h - implied NOPs affects regs/flags
055h - ZP,X NOPs less than 2 bytes long
056h - absolute NOP less than 3 bytes long
057h - ZP NOPs less than 2 bytes long
LAX - "invalid" opcode tests
----------------------------
07Ch - LAX (indr,x) failure
07Dh - LAX (indr,x) failure
07Eh - LAX zeropage failure
07Fh - LAX zeropage failure
080h - LAX absolute failure
081h - LAX absolute failure
082h - LAX (indr),y failure
083h - LAX (indr),y failure
084h - LAX zp,y failure
085h - LAX zp,y failure
086h - LAX abs,y failure
087h - LAX abs,y failure
SAX - "invalid" opcode tests
----------------------------
088h - SAX (indr,x) failure
089h - SAX (indr,x) failure
08Ah - SAX zeropage failure
08Bh - SAX zeropage failure
08Ch - SAX absolute failure
08Dh - SAX absolute failure
08Eh - SAX zp,y failure
08Fh - SAX zp,y failure
SBC - "invalid" opcode test
---------------------------
090h - SBC failure
091h - SBC failure
092h - SBC failure
093h - SBC failure
094h - SBC failure
DCP - "invalid" opcode tests
----------------------------
095h - DCP (indr,x) failure
096h - DCP (indr,x) failure
097h - DCP (indr,x) failure
098h - DCP zeropage failure
099h - DCP zeropage failure
09Ah - DCP zeropage failure
09Bh - DCP absolute failure
09Ch - DCP absolute failure
09Dh - DCP absolute failure
09Eh - DCP (indr),y failure
09Fh - DCP (indr),y failure
0A0h - DCP (indr),y failure
0A1h - DCP zp,x failure
0A2h - DCP zp,x failure
0A3h - DCP zp,x failure
0A4h - DCP abs,y failure
0A5h - DCP abs,y failure
0A6h - DCP abs,y failure
0A7h - DCP abs,x failure
0A8h - DCP abs,x failure
0A9h - DCP abs,x failure
ISB - "invalid" opcode tests
----------------------------
0AAh - DCP (indr,x) failure
0ABh - DCP (indr,x) failure
0ACh - DCP (indr,x) failure
0ADh - DCP zeropage failure
0AEh - DCP zeropage failure
0AFh - DCP zeropage failure
0B0h - DCP absolute failure
0B1h - DCP absolute failure
0B2h - DCP absolute failure
0B3h - DCP (indr),y failure
0B4h - DCP (indr),y failure
0B5h - DCP (indr),y failure
0B6h - DCP zp,x failure
0B7h - DCP zp,x failure
0B8h - DCP zp,x failure
0B9h - DCP abs,y failure
0BAh - DCP abs,y failure
0BBh - DCP abs,y failure
0BCh - DCP abs,x failure
0BDh - DCP abs,x failure
0BEh - DCP abs,x failure
SLO - "invalid" opcode tests
----------------------------
0BFh - SLO (indr,x) failure
0C0h - SLO (indr,x) failure
0C1h - SLO (indr,x) failure
0C2h - SLO zeropage failure
0C3h - SLO zeropage failure
0C4h - SLO zeropage failure
0C5h - SLO absolute failure
0C6h - SLO absolute failure
0C7h - SLO absolute failure
0C8h - SLO (indr),y failure
0C9h - SLO (indr),y failure
0CAh - SLO (indr),y failure
0CBh - SLO zp,x failure
0CCh - SLO zp,x failure
0CDh - SLO zp,x failure
0CEh - SLO abs,y failure
0CFh - SLO abs,y failure
0D0h - SLO abs,y failure
0D1h - SLO abs,x failure
0D2h - SLO abs,x failure
0D3h - SLO abs,x failure
RLA - "invalid" opcode tests
----------------------------
0D4h - RLA (indr,x) failure
0D5h - RLA (indr,x) failure
0D6h - RLA (indr,x) failure
0D7h - RLA zeropage failure
0D8h - RLA zeropage failure
0D9h - RLA zeropage failure
0DAh - RLA absolute failure
0DBh - RLA absolute failure
0DCh - RLA absolute failure
0DDh - RLA (indr),y failure
0DEh - RLA (indr),y failure
0DFh - RLA (indr),y failure
0E0h - RLA zp,x failure
0E1h - RLA zp,x failure
0E2h - RLA zp,x failure
0E3h - RLA abs,y failure
0E4h - RLA abs,y failure
0E5h - RLA abs,y failure
0E6h - RLA abs,x failure
0E7h - RLA abs,x failure
0E8h - RLA abs,x failure
SRE - "invalid" opcode tests
----------------------------
0E8h - SRE (indr,x) failure
0EAh - SRE (indr,x) failure
0EBh - SRE (indr,x) failure
0ECh - SRE zeropage failure
0EDh - SRE zeropage failure
0EEh - SRE zeropage failure
0EFh - SRE absolute failure
0F0h - SRE absolute failure
0F1h - SRE absolute failure
0F2h - SRE (indr),y failure
0F3h - SRE (indr),y failure
0F4h - SRE (indr),y failure
0F5h - SRE zp,x failure
0F6h - SRE zp,x failure
0F7h - SRE zp,x failure
0F8h - SRE abs,y failure
0F9h - SRE abs,y failure
0FAh - SRE abs,y failure
0FBh - SRE abs,x failure
0FCh - SRE abs,x failure
0FDh - SRE abs,x failure
RRA - "invalid" opcode tests
----------------------------
001h - RRA (indr,x) failure
002h - RRA (indr,x) failure
003h - RRA (indr,x) failure
004h - RRA zeropage failure
005h - RRA zeropage failure
006h - RRA zeropage failure
007h - RRA absolute failure
008h - RRA absolute failure
009h - RRA absolute failure
00Ah - RRA (indr),y failure
00Bh - RRA (indr),y failure
00Ch - RRA (indr),y failure
00Dh - RRA zp,x failure
00Eh - RRA zp,x failure
00Fh - RRA zp,x failure
010h - RRA abs,y failure
011h - RRA abs,y failure
012h - RRA abs,y failure
013h - RRA abs,x failure
014h - RRA abs,x failure
015h - RRA abs,x failure
001h -
002h -
003h -
004h -
005h -
006h -
007h -
008h -
009h -
00Ah -
00Bh -
00Ch -
00Dh -
00Eh -
00Fh -
010h -
Todo: check to see if decimal mode is missing on CPU

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff