mirror of
https://github.com/henrikpersson/potatis.git
synced 2024-05-20 05:10:17 -04:00
"First" commit
This commit is contained in:
commit
61f57a85d9
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
.DS_Store
|
||||
/legal-roms
|
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal 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
303
Cargo.lock
generated
Normal 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
5
Cargo.toml
Normal 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
92
README.md
Normal 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
11
common/Cargo.toml
Normal 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
40
common/src/lib.rs
Normal 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
13
mos6502/Cargo.toml
Normal 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
|
94
mos6502/src/address_mode.rs
Normal file
94
mos6502/src/address_mode.rs
Normal 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
853
mos6502/src/cpu.rs
Normal 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
241
mos6502/src/debugger.rs
Normal 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(¤t_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
447
mos6502/src/instructions.rs
Normal 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
6
mos6502/src/lib.rs
Normal 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
34
mos6502/src/memory.rs
Normal 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
56
mos6502/src/mos6502.rs
Normal 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
|
||||
}
|
||||
}
|
77
mos6502/tests/integration_test.rs
Normal file
77
mos6502/tests/integration_test.rs
Normal 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
16
nes-sdl/Cargo.toml
Normal 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
49
nes-sdl/src/main.rs
Normal 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
99
nes-sdl/src/sdl.rs
Normal 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
14
nes/Cargo.toml
Normal 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
162
nes/src/cartridge.rs
Normal 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
36
nes/src/frame.rs
Normal 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
57
nes/src/joypad.rs
Normal 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
33
nes/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
91
nes/src/mappers/mapper3.rs
Normal file
91
nes/src/mappers/mapper3.rs
Normal 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
232
nes/src/mappers/mmc1.rs
Normal 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
17
nes/src/mappers/mod.rs
Normal 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
60
nes/src/mappers/nrom.rs
Normal 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
123
nes/src/nes.rs
Normal 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
158
nes/src/nesbus.rs
Normal 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
3
nes/src/ppu/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod registers;
|
||||
mod palette;
|
||||
pub(crate) mod ppu;
|
73
nes/src/ppu/palette.rs
Normal file
73
nes/src/ppu/palette.rs
Normal 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
463
nes/src/ppu/ppu.rs
Normal 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
305
nes/src/ppu/registers.rs
Normal 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
95
nes/tests/blargg.rs
Normal 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
10
nes/tests/common.rs
Normal 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
63
nes/tests/nestest.rs
Normal 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
BIN
screenshots/dk.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 719 KiB |
BIN
screenshots/nestest.png
Normal file
BIN
screenshots/nestest.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 683 KiB |
BIN
screenshots/pm.png
Normal file
BIN
screenshots/pm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 689 KiB |
1
test-roms/6502_65C02_functional_tests
Submodule
1
test-roms/6502_65C02_functional_tests
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 966b1a35049f9d8be44ad092ec6d43d5ba1831b3
|
BIN
test-roms/bin/TTL6502.bin
Normal file
BIN
test-roms/bin/TTL6502.bin
Normal file
Binary file not shown.
BIN
test-roms/bin/extended_test.bin
Normal file
BIN
test-roms/bin/extended_test.bin
Normal file
Binary file not shown.
3448
test-roms/bin/extended_test.lst
Normal file
3448
test-roms/bin/extended_test.lst
Normal file
File diff suppressed because it is too large
Load diff
32
test-roms/bin/extended_test.map
Normal file
32
test-roms/bin/extended_test.map
Normal 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:
|
||||
-------------
|
||||
|
BIN
test-roms/bin/extended_test.o
Normal file
BIN
test-roms/bin/extended_test.o
Normal file
Binary file not shown.
BIN
test-roms/bin/functional_test_bcd_disabled.bin
Normal file
BIN
test-roms/bin/functional_test_bcd_disabled.bin
Normal file
Binary file not shown.
7008
test-roms/bin/functional_test_bcd_disabled.lst
Normal file
7008
test-roms/bin/functional_test_bcd_disabled.lst
Normal file
File diff suppressed because it is too large
Load diff
32
test-roms/bin/functional_test_bcd_disabled.map
Normal file
32
test-roms/bin/functional_test_bcd_disabled.map
Normal 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:
|
||||
-------------
|
||||
|
BIN
test-roms/bin/functional_test_bcd_disabled.o
Normal file
BIN
test-roms/bin/functional_test_bcd_disabled.o
Normal file
Binary file not shown.
BIN
test-roms/bin/functional_test_full.bin
Normal file
BIN
test-roms/bin/functional_test_full.bin
Normal file
Binary file not shown.
7012
test-roms/bin/functional_test_full.lst
Normal file
7012
test-roms/bin/functional_test_full.lst
Normal file
File diff suppressed because it is too large
Load diff
32
test-roms/bin/functional_test_full.map
Normal file
32
test-roms/bin/functional_test_full.map
Normal 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:
|
||||
-------------
|
||||
|
BIN
test-roms/bin/functional_test_full.o
Normal file
BIN
test-roms/bin/functional_test_full.o
Normal file
Binary file not shown.
33
test-roms/build-tests.sh
Executable file
33
test-roms/build-tests.sh
Executable 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
|
1
test-roms/nes-test-roms
Submodule
1
test-roms/nes-test-roms
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 95d8f621ae55cee0d09b91519a8989ae0e64753b
|
7
test-roms/nestest/README.md
Normal file
7
test-roms/nestest/README.md
Normal 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.
|
8991
test-roms/nestest/nestest.log
Normal file
8991
test-roms/nestest/nestest.log
Normal file
File diff suppressed because it is too large
Load diff
BIN
test-roms/nestest/nestest.nes
Normal file
BIN
test-roms/nestest/nestest.nes
Normal file
Binary file not shown.
724
test-roms/nestest/nestest.txt
Normal file
724
test-roms/nestest/nestest.txt
Normal 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
|
8991
test-roms/nestest/nestest_cpu.log
Normal file
8991
test-roms/nestest/nestest_cpu.log
Normal file
File diff suppressed because it is too large
Load diff
8991
test-roms/nestest/nestest_cycles.log
Normal file
8991
test-roms/nestest/nestest_cycles.log
Normal file
File diff suppressed because it is too large
Load diff
8991
test-roms/nestest/nestest_no_cycle.log
Normal file
8991
test-roms/nestest/nestest_no_cycle.log
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue