diff options
Diffstat (limited to 'lib/nes')
| -rw-r--r-- | lib/nes/CMakeLists.txt | 20 | ||||
| -rw-r--r-- | lib/nes/cartridge.cc | 70 | ||||
| -rw-r--r-- | lib/nes/cartridge.hh | 41 | ||||
| -rw-r--r-- | lib/nes/controller.cc | 18 | ||||
| -rw-r--r-- | lib/nes/controller.hh | 30 | ||||
| -rw-r--r-- | lib/nes/cpu.cc | 1358 | ||||
| -rw-r--r-- | lib/nes/cpu.hh | 294 | ||||
| -rw-r--r-- | lib/nes/display.cc | 16 | ||||
| -rw-r--r-- | lib/nes/display.hh | 33 | ||||
| -rw-r--r-- | lib/nes/mapper.cc | 134 | ||||
| -rw-r--r-- | lib/nes/mapper.hh | 64 | ||||
| -rw-r--r-- | lib/nes/nes.cc | 74 | ||||
| -rw-r--r-- | lib/nes/nes.hh | 46 | ||||
| -rw-r--r-- | lib/nes/ppu.cc | 737 | ||||
| -rw-r--r-- | lib/nes/ppu.hh | 208 | ||||
| -rw-r--r-- | lib/nes/ref/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | lib/nes/ref/test.cc | 3 | ||||
| -rw-r--r-- | lib/nes/util/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | lib/nes/util/address.hh | 73 | ||||
| -rw-r--r-- | lib/nes/util/button-mask.hh | 43 | ||||
| -rw-r--r-- | lib/nes/util/cycle-count.hh | 87 | ||||
| -rw-r--r-- | lib/nes/util/debug.hh | 15 | ||||
| -rw-r--r-- | lib/nes/util/rgb.hh | 33 | ||||
| -rw-r--r-- | lib/nes/util/snapshot.hh | 63 |
24 files changed, 3473 insertions, 0 deletions
diff --git a/lib/nes/CMakeLists.txt b/lib/nes/CMakeLists.txt new file mode 100644 index 0000000..78b7d04 --- /dev/null +++ b/lib/nes/CMakeLists.txt @@ -0,0 +1,20 @@ +target_sources( + nes + PRIVATE + nes.hh + nes.cc + cartridge.hh + cartridge.cc + display.hh + display.cc + controller.hh + controller.cc + cpu.hh + cpu.cc + ppu.hh + ppu.cc + mapper.hh + mapper.cc) + +add_subdirectory(util) +add_subdirectory(ref)
\ No newline at end of file diff --git a/lib/nes/cartridge.cc b/lib/nes/cartridge.cc new file mode 100644 index 0000000..861dc13 --- /dev/null +++ b/lib/nes/cartridge.cc @@ -0,0 +1,70 @@ +#include "nes/cartridge.hh" +#include <fstream> + +namespace nes +{ + namespace + { + constexpr auto prg_rom_bank_size = std::size_t{ 16 * 1024 }; + constexpr auto chr_rom_bank_size = std::size_t{ 8 * 1024 }; + constexpr auto ram_bank_size = std::size_t{ 8 * 1024 }; + } // namespace + + auto cartridge::from_data(void const* data, std::size_t const length) -> cartridge + { + // See: https://www.nesdev.org/wiki/INES + + auto const bytes = static_cast<std::uint8_t const*>(data); + auto res = cartridge{}; + res.is_valid_ = true; + + // Parse header. + if (length < 16) { return cartridge{}; } + if (bytes[0] != 'N' || bytes[1] != 'E' || bytes[2] != 'S' || bytes[3] != 0x1A) { return cartridge{}; } + auto const prg_rom_banks = bytes[4]; + auto const chr_rom_banks = bytes[5]; + auto const control_1 = bytes[6]; + auto const control_2 = bytes[7]; + auto const ram_banks = (bytes[8] != 0) ? bytes[8] : 1; + auto const has_trainer = (control_1 & 0b00000100) != 0; + + res.mapper_number_ = + (control_1 & 0b11110000) >> 4 | + (control_2 & 0b11110000) >> 0; + res.name_table_arrangement_ = (control_1 & 0b00000001) != 0 + ? name_table_arrangement::vertical + : name_table_arrangement::horizontal; + + // Load program data. + auto offset = std::size_t{ 16 }; + if (has_trainer) { offset += 512; } + + auto const prg_rom_size = prg_rom_banks * prg_rom_bank_size; + if (length < offset + prg_rom_size) { return cartridge{}; } + res.prg_rom_ = std::vector(&bytes[offset], &bytes[offset + prg_rom_size]); + + offset += prg_rom_size; + + auto const chr_rom_size = chr_rom_banks * chr_rom_bank_size; + if (length < offset + chr_rom_size) { return cartridge{}; } + res.chr_rom_ = std::vector(&bytes[offset], &bytes[offset + chr_rom_size]); + + res.ram_ = std::vector(ram_banks * ram_bank_size, std::uint8_t{ 0 }); + + return res; + } + + auto cartridge::from_file(std::string_view const path) -> cartridge + { + auto stream = std::ifstream{ path, std::ios::binary }; + if (!stream.good()) { return cartridge{}; } + + stream.seekg(0, std::ios::end); + auto const length = stream.tellg(); + stream.seekg(0, std::ios::beg); + auto data = std::vector<std::uint8_t>(length); + stream.read(reinterpret_cast<char*>(data.data()), data.size()); + + return from_data(data.data(), data.size()); + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/cartridge.hh b/lib/nes/cartridge.hh new file mode 100644 index 0000000..3ce2a16 --- /dev/null +++ b/lib/nes/cartridge.hh @@ -0,0 +1,41 @@ +#pragma once + +#include <cstddef> +#include <vector> +#include <string_view> + +namespace nes +{ + enum class name_table_arrangement + { + horizontal, + vertical, + }; + + class cartridge + { + bool is_valid_{ false }; + std::vector<std::uint8_t> prg_rom_; + std::vector<std::uint8_t> chr_rom_; + std::vector<std::uint8_t> ram_; + std::uint8_t mapper_number_{ 0 }; + name_table_arrangement name_table_arrangement_{}; + + public: + explicit cartridge() = default; + + static auto from_data(void const* data, std::size_t length) -> cartridge; + static auto from_file(std::string_view path) -> cartridge; + + auto is_valid() const -> bool { return is_valid_; } + auto get_prg_rom() const -> std::uint8_t const* { return prg_rom_.data(); } + auto get_prg_rom_length() const -> std::size_t { return prg_rom_.size(); } + auto get_chr_rom() const -> std::uint8_t const* { return chr_rom_.data(); } + auto get_chr_rom_length() const -> std::size_t { return chr_rom_.size(); } + auto get_ram() const -> std::uint8_t const* { return ram_.data(); } + auto get_ram_mut() -> std::uint8_t* { return ram_.data(); } + auto get_ram_length() const -> std::size_t { return ram_.size(); } + auto get_mapper_number() const -> std::uint8_t { return mapper_number_; } + auto get_name_table_arrangement() const -> name_table_arrangement { return name_table_arrangement_; } + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/controller.cc b/lib/nes/controller.cc new file mode 100644 index 0000000..d9d6946 --- /dev/null +++ b/lib/nes/controller.cc @@ -0,0 +1,18 @@ +#include "nes/controller.hh" + +namespace nes +{ + auto controller::read() -> std::uint8_t + { + auto const res = index_ < 8 ? (pressed_.get_raw_value() >> index_) & 1 : 0; + index_ += 1; + if (strobing_) { index_ = 0; } + return static_cast<std::uint8_t>(res); + } + + auto controller::write(std::uint8_t const value) -> void + { + strobing_ = value & 1; + if (strobing_) { index_ = 0; } + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/controller.hh b/lib/nes/controller.hh new file mode 100644 index 0000000..fa16859 --- /dev/null +++ b/lib/nes/controller.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "nes/util/button-mask.hh" + +namespace nes +{ + class controller + { + button_mask pressed_; + unsigned index_{ 0 }; + bool strobing_{ false }; + + public: + explicit controller() = default; + + controller(controller const&) = delete; + controller(controller&&) = delete; + auto operator=(controller const&) -> controller& = delete; + auto operator=(controller&&) -> controller& = delete; + + auto get_pressed() const -> button_mask { return pressed_;} + auto get_pressed() -> button_mask& { return pressed_; } + auto set_pressed(button_mask const value) -> void { pressed_ = value; } + + // IO register + + auto read() -> std::uint8_t; + auto write(std::uint8_t) -> void; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/cpu.cc b/lib/nes/cpu.cc new file mode 100644 index 0000000..51507b0 --- /dev/null +++ b/lib/nes/cpu.cc @@ -0,0 +1,1358 @@ +#include "nes/cpu.hh" +#include "nes/ppu.hh" +#include "nes/controller.hh" +#include "nes/mapper.hh" +#include "nes/util/snapshot.hh" + +namespace nes +{ + cpu::cpu(ppu& ppu, mapper& mapper, controller& controller_1, controller& controller_2) + : ppu_{ ppu } + , mapper_{ mapper } + , controller_1_{ controller_1 } + , controller_2_{ controller_2 } + { + registers_.pc = read16(address{ 0xFFFC }); + } + + auto cpu::build_snapshot(snapshot& snapshot) -> void + { + snapshot.cpu_cycle = current_cycles_; + snapshot.ram = std::vector(std::begin(ram_), std::end(ram_)); + snapshot.registers.pc = registers_.pc; + snapshot.registers.sp = registers_.sp; + snapshot.registers.a = registers_.a; + snapshot.registers.x = registers_.x; + snapshot.registers.y = registers_.y; + snapshot.registers.p = registers_.p; + snapshot.registers.c = registers_.c; + snapshot.registers.z = registers_.z; + snapshot.registers.i = registers_.i; + snapshot.registers.d = registers_.d; + snapshot.registers.b = registers_.b; + snapshot.registers.v = registers_.v; + snapshot.registers.n = registers_.n; + } + + auto cpu::stall_cycles(cycle_count const count) -> void + { + current_cycles_ += count; + } + + auto cpu::trigger_nmi() -> void + { + nmi_pending_ = true; + } + + auto cpu::step() -> void + { + // See https://www.nesdev.org/wiki/CPU_unofficial_opcodes + +#ifdef NES_ENABLE_PPU + if (nmi_pending_) + { + execute_interrupt(address{ 0xFFFA }); + nmi_pending_ = false; + } +#endif + + auto const opcode = advance_pc8(); + switch (opcode) + { + case 0x00: run_brk<addressing_mode::immediate>(); break; + case 0x01: run_ora<addressing_mode::indexed_indirect>(); break; + case 0x02: run_stp(); break; + case 0x03: run_slo<addressing_mode::indexed_indirect>(); break; + case 0x04: run_nop<addressing_mode::zero_page>(); break; + case 0x05: run_ora<addressing_mode::zero_page>(); break; + case 0x06: run_asl<addressing_mode::zero_page>(); break; + case 0x07: run_slo<addressing_mode::zero_page>(); break; + case 0x08: run_php(); break; + case 0x09: run_ora<addressing_mode::immediate>(); break; + case 0x0A: run_asl<addressing_mode::accumulator>(); break; + case 0x0B: run_anc<addressing_mode::immediate>(); break; + case 0x0C: run_nop<addressing_mode::absolute>(); break; + case 0x0D: run_ora<addressing_mode::absolute>(); break; + case 0x0E: run_asl<addressing_mode::absolute>(); break; + case 0x0F: run_slo<addressing_mode::absolute>(); break; + case 0x10: run_bpl<addressing_mode::relative>(); break; + case 0x11: run_ora<addressing_mode::indirect_indexed>(); break; + case 0x12: run_stp(); break; + case 0x13: run_slo<addressing_mode::indirect_indexed>(); break; + case 0x14: run_nop<addressing_mode::zero_page_indexed_x>(); break; + case 0x15: run_ora<addressing_mode::zero_page_indexed_x>(); break; + case 0x16: run_asl<addressing_mode::zero_page_indexed_x>(); break; + case 0x17: run_slo<addressing_mode::zero_page_indexed_x>(); break; + case 0x18: run_clc(); break; + case 0x19: run_ora<addressing_mode::absolute_indexed_y>(); break; + case 0x1A: run_nop(); break; + case 0x1B: run_slo<addressing_mode::absolute_indexed_y>(); break; + case 0x1C: run_nop<addressing_mode::absolute_indexed_x>(); break; + case 0x1D: run_ora<addressing_mode::absolute_indexed_x>(); break; + case 0x1E: run_asl<addressing_mode::absolute_indexed_x>(); break; + case 0x1F: run_slo<addressing_mode::absolute_indexed_x>(); break; + case 0x20: run_jsr<addressing_mode::absolute>(); break; + case 0x21: run_and<addressing_mode::indexed_indirect>(); break; + case 0x22: run_stp(); break; + case 0x23: run_rla<addressing_mode::indexed_indirect>(); break; + case 0x24: run_bit<addressing_mode::zero_page>(); break; + case 0x25: run_and<addressing_mode::zero_page>(); break; + case 0x26: run_rol<addressing_mode::zero_page>(); break; + case 0x27: run_rla<addressing_mode::zero_page>(); break; + case 0x28: run_plp(); break; + case 0x29: run_and<addressing_mode::immediate>(); break; + case 0x2A: run_rol<addressing_mode::accumulator>(); break; + case 0x2B: run_anc<addressing_mode::immediate>(); break; + case 0x2C: run_bit<addressing_mode::absolute>(); break; + case 0x2D: run_and<addressing_mode::absolute>(); break; + case 0x2E: run_rol<addressing_mode::absolute>(); break; + case 0x2F: run_rla<addressing_mode::absolute>(); break; + case 0x30: run_bmi<addressing_mode::relative>(); break; + case 0x31: run_and<addressing_mode::indirect_indexed>(); break; + case 0x32: run_stp(); break; + case 0x33: run_rla<addressing_mode::indirect_indexed>(); break; + case 0x34: run_nop<addressing_mode::zero_page_indexed_x>(); break; + case 0x35: run_and<addressing_mode::zero_page_indexed_x>(); break; + case 0x36: run_rol<addressing_mode::zero_page_indexed_x>(); break; + case 0x37: run_rla<addressing_mode::zero_page_indexed_x>(); break; + case 0x38: run_sec(); break; + case 0x39: run_and<addressing_mode::absolute_indexed_y>(); break; + case 0x3A: run_nop(); break; + case 0x3B: run_rla<addressing_mode::absolute_indexed_y>(); break; + case 0x3C: run_nop<addressing_mode::absolute_indexed_x>(); break; + case 0x3D: run_and<addressing_mode::absolute_indexed_x>(); break; + case 0x3E: run_rol<addressing_mode::absolute_indexed_x>(); break; + case 0x3F: run_rla<addressing_mode::absolute_indexed_x>(); break; + case 0x40: run_rti(); break; + case 0x41: run_eor<addressing_mode::indexed_indirect>(); break; + case 0x42: run_stp(); break; + case 0x43: run_sre<addressing_mode::indexed_indirect>(); break; + case 0x44: run_nop<addressing_mode::zero_page>(); break; + case 0x45: run_eor<addressing_mode::zero_page>(); break; + case 0x46: run_lsr<addressing_mode::zero_page>(); break; + case 0x47: run_sre<addressing_mode::zero_page>(); break; + case 0x48: run_pha(); break; + case 0x49: run_eor<addressing_mode::immediate>(); break; + case 0x4A: run_lsr<addressing_mode::accumulator>(); break; + case 0x4B: run_alr<addressing_mode::immediate>(); break; + case 0x4C: run_jmp<addressing_mode::absolute>(); break; + case 0x4D: run_eor<addressing_mode::absolute>(); break; + case 0x4E: run_lsr<addressing_mode::absolute>(); break; + case 0x4F: run_sre<addressing_mode::absolute>(); break; + case 0x50: run_bvc<addressing_mode::relative>(); break; + case 0x51: run_eor<addressing_mode::indirect_indexed>(); break; + case 0x52: run_stp(); break; + case 0x53: run_sre<addressing_mode::indirect_indexed>(); break; + case 0x54: run_nop<addressing_mode::zero_page_indexed_x>(); break; + case 0x55: run_eor<addressing_mode::zero_page_indexed_x>(); break; + case 0x56: run_lsr<addressing_mode::zero_page_indexed_x>(); break; + case 0x57: run_sre<addressing_mode::zero_page_indexed_x>(); break; + case 0x58: run_cli(); break; + case 0x59: run_eor<addressing_mode::absolute_indexed_y>(); break; + case 0x5A: run_nop(); break; + case 0x5B: run_sre<addressing_mode::absolute_indexed_y>(); break; + case 0x5C: run_nop<addressing_mode::absolute_indexed_x>(); break; + case 0x5D: run_eor<addressing_mode::absolute_indexed_x>(); break; + case 0x5E: run_lsr<addressing_mode::absolute_indexed_x>(); break; + case 0x5F: run_sre<addressing_mode::absolute_indexed_x>(); break; + case 0x60: run_rts(); break; + case 0x61: run_adc<addressing_mode::indexed_indirect>(); break; + case 0x62: run_stp(); break; + case 0x63: run_rra<addressing_mode::indexed_indirect>(); break; + case 0x64: run_nop<addressing_mode::zero_page>(); break; + case 0x65: run_adc<addressing_mode::zero_page>(); break; + case 0x66: run_ror<addressing_mode::zero_page>(); break; + case 0x67: run_rra<addressing_mode::zero_page>(); break; + case 0x68: run_pla(); break; + case 0x69: run_adc<addressing_mode::immediate>(); break; + case 0x6A: run_ror<addressing_mode::accumulator>(); break; + case 0x6B: run_arr<addressing_mode::immediate>(); break; + case 0x6C: run_jmp<addressing_mode::indirect>(); break; + case 0x6D: run_adc<addressing_mode::absolute>(); break; + case 0x6E: run_ror<addressing_mode::absolute>(); break; + case 0x6F: run_rra<addressing_mode::absolute>(); break; + case 0x70: run_bvs<addressing_mode::relative>(); break; + case 0x71: run_adc<addressing_mode::indirect_indexed>(); break; + case 0x72: run_stp(); break; + case 0x73: run_rra<addressing_mode::indirect_indexed>(); break; + case 0x74: run_nop<addressing_mode::zero_page_indexed_x>(); break; + case 0x75: run_adc<addressing_mode::zero_page_indexed_x>(); break; + case 0x76: run_ror<addressing_mode::zero_page_indexed_x>(); break; + case 0x77: run_rra<addressing_mode::zero_page_indexed_x>(); break; + case 0x78: run_sei(); break; + case 0x79: run_adc<addressing_mode::absolute_indexed_y>(); break; + case 0x7A: run_nop(); break; + case 0x7B: run_rra<addressing_mode::absolute_indexed_y>(); break; + case 0x7C: run_nop<addressing_mode::absolute_indexed_x>(); break; + case 0x7D: run_adc<addressing_mode::absolute_indexed_x>(); break; + case 0x7E: run_ror<addressing_mode::absolute_indexed_x>(); break; + case 0x7F: run_rra<addressing_mode::absolute_indexed_x>(); break; + case 0x80: run_nop<addressing_mode::immediate>(); break; + case 0x81: run_sta<addressing_mode::indexed_indirect>(); break; + case 0x82: run_nop<addressing_mode::immediate>(); break; + case 0x83: run_sax<addressing_mode::indexed_indirect>(); break; + case 0x84: run_sty<addressing_mode::zero_page>(); break; + case 0x85: run_sta<addressing_mode::zero_page>(); break; + case 0x86: run_stx<addressing_mode::zero_page>(); break; + case 0x87: run_sax<addressing_mode::zero_page>(); break; + case 0x88: run_dey(); break; + case 0x89: run_nop<addressing_mode::immediate>(); break; + case 0x8A: run_txa(); break; + case 0x8B: run_xaa<addressing_mode::immediate>(); break; + case 0x8C: run_sty<addressing_mode::absolute>(); break; + case 0x8D: run_sta<addressing_mode::absolute>(); break; + case 0x8E: run_stx<addressing_mode::absolute>(); break; + case 0x8F: run_sax<addressing_mode::absolute>(); break; + case 0x90: run_bcc<addressing_mode::relative>(); break; + case 0x91: run_sta<addressing_mode::indirect_indexed>(); break; + case 0x92: run_stp(); break; + case 0x93: run_ahx<addressing_mode::indirect_indexed>(); break; + case 0x94: run_sty<addressing_mode::zero_page_indexed_x>(); break; + case 0x95: run_sta<addressing_mode::zero_page_indexed_x>(); break; + case 0x96: run_stx<addressing_mode::zero_page_indexed_y>(); break; + case 0x97: run_sax<addressing_mode::zero_page_indexed_y>(); break; + case 0x98: run_tya(); break; + case 0x99: run_sta<addressing_mode::absolute_indexed_y>(); break; + case 0x9A: run_txs(); break; + case 0x9B: run_tas<addressing_mode::absolute_indexed_y>(); break; + case 0x9C: run_shy<addressing_mode::absolute_indexed_x>(); break; + case 0x9D: run_sta<addressing_mode::absolute_indexed_x>(); break; + case 0x9E: run_shx<addressing_mode::absolute_indexed_y>(); break; + case 0x9F: run_ahx<addressing_mode::absolute_indexed_y>(); break; + case 0xA0: run_ldy<addressing_mode::immediate>(); break; + case 0xA1: run_lda<addressing_mode::indexed_indirect>(); break; + case 0xA2: run_ldx<addressing_mode::immediate>(); break; + case 0xA3: run_lax<addressing_mode::indexed_indirect>(); break; + case 0xA4: run_ldy<addressing_mode::zero_page>(); break; + case 0xA5: run_lda<addressing_mode::zero_page>(); break; + case 0xA6: run_ldx<addressing_mode::zero_page>(); break; + case 0xA7: run_lax<addressing_mode::zero_page>(); break; + case 0xA8: run_tay(); break; + case 0xA9: run_lda<addressing_mode::immediate>(); break; + case 0xAA: run_tax(); break; + case 0xAB: run_lax<addressing_mode::immediate>(); break; + case 0xAC: run_ldy<addressing_mode::absolute>(); break; + case 0xAD: run_lda<addressing_mode::absolute>(); break; + case 0xAE: run_ldx<addressing_mode::absolute>(); break; + case 0xAF: run_lax<addressing_mode::absolute>(); break; + case 0xB0: run_bcs<addressing_mode::relative>(); break; + case 0xB1: run_lda<addressing_mode::indirect_indexed>(); break; + case 0xB2: run_stp(); break; + case 0xB3: run_lax<addressing_mode::indirect_indexed>(); break; + case 0xB4: run_ldy<addressing_mode::zero_page_indexed_x>(); break; + case 0xB5: run_lda<addressing_mode::zero_page_indexed_x>(); break; + case 0xB6: run_ldx<addressing_mode::zero_page_indexed_y>(); break; + case 0xB7: run_lax<addressing_mode::zero_page_indexed_y>(); break; + case 0xB8: run_clv(); break; + case 0xB9: run_lda<addressing_mode::absolute_indexed_y>(); break; + case 0xBA: run_tsx(); break; + case 0xBB: run_las<addressing_mode::absolute_indexed_y>(); break; + case 0xBC: run_ldy<addressing_mode::absolute_indexed_x>(); break; + case 0xBD: run_lda<addressing_mode::absolute_indexed_x>(); break; + case 0xBE: run_ldx<addressing_mode::absolute_indexed_y>(); break; + case 0xBF: run_lax<addressing_mode::absolute_indexed_y>(); break; + case 0xC0: run_cpy<addressing_mode::immediate>(); break; + case 0xC1: run_cmp<addressing_mode::indexed_indirect>(); break; + case 0xC2: run_nop<addressing_mode::immediate>(); break; + case 0xC3: run_dcp<addressing_mode::indexed_indirect>(); break; + case 0xC4: run_cpy<addressing_mode::zero_page>(); break; + case 0xC5: run_cmp<addressing_mode::zero_page>(); break; + case 0xC6: run_dec<addressing_mode::zero_page>(); break; + case 0xC7: run_dcp<addressing_mode::zero_page>(); break; + case 0xC8: run_iny(); break; + case 0xC9: run_cmp<addressing_mode::immediate>(); break; + case 0xCA: run_dex(); break; + case 0xCB: run_axs<addressing_mode::immediate>(); break; + case 0xCC: run_cpy<addressing_mode::absolute>(); break; + case 0xCD: run_cmp<addressing_mode::absolute>(); break; + case 0xCE: run_dec<addressing_mode::absolute>(); break; + case 0xCF: run_dcp<addressing_mode::absolute>(); break; + case 0xD0: run_bne<addressing_mode::relative>(); break; + case 0xD1: run_cmp<addressing_mode::indirect_indexed>(); break; + case 0xD2: run_stp(); break; + case 0xD3: run_dcp<addressing_mode::indirect_indexed>(); break; + case 0xD4: run_nop<addressing_mode::zero_page_indexed_x>(); break; + case 0xD5: run_cmp<addressing_mode::zero_page_indexed_x>(); break; + case 0xD6: run_dec<addressing_mode::zero_page_indexed_x>(); break; + case 0xD7: run_dcp<addressing_mode::zero_page_indexed_x>(); break; + case 0xD8: run_cld(); break; + case 0xD9: run_cmp<addressing_mode::absolute_indexed_y>(); break; + case 0xDA: run_nop(); break; + case 0xDB: run_dcp<addressing_mode::absolute_indexed_y>(); break; + case 0xDC: run_nop<addressing_mode::absolute_indexed_x>(); break; + case 0xDD: run_cmp<addressing_mode::absolute_indexed_x>(); break; + case 0xDE: run_dec<addressing_mode::absolute_indexed_x>(); break; + case 0xDF: run_dcp<addressing_mode::absolute_indexed_x>(); break; + case 0xE0: run_cpx<addressing_mode::immediate>(); break; + case 0xE1: run_sbc<addressing_mode::indexed_indirect>(); break; + case 0xE2: run_nop<addressing_mode::immediate>(); break; + case 0xE3: run_isc<addressing_mode::indexed_indirect>(); break; + case 0xE4: run_cpx<addressing_mode::zero_page>(); break; + case 0xE5: run_sbc<addressing_mode::zero_page>(); break; + case 0xE6: run_inc<addressing_mode::zero_page>(); break; + case 0xE7: run_isc<addressing_mode::zero_page>(); break; + case 0xE8: run_inx(); break; + case 0xE9: run_sbc<addressing_mode::immediate>(); break; + case 0xEA: run_nop(); break; + case 0xEB: run_sbc<addressing_mode::immediate>(); break; + case 0xEC: run_cpx<addressing_mode::absolute>(); break; + case 0xED: run_sbc<addressing_mode::absolute>(); break; + case 0xEE: run_inc<addressing_mode::absolute>(); break; + case 0xEF: run_isc<addressing_mode::absolute>(); break; + case 0xF0: run_beq<addressing_mode::relative>(); break; + case 0xF1: run_sbc<addressing_mode::indirect_indexed>(); break; + case 0xF2: run_stp(); break; + case 0xF3: run_isc<addressing_mode::indirect_indexed>(); break; + case 0xF4: run_nop<addressing_mode::zero_page_indexed_x>(); break; + case 0xF5: run_sbc<addressing_mode::zero_page_indexed_x>(); break; + case 0xF6: run_inc<addressing_mode::zero_page_indexed_x>(); break; + case 0xF7: run_isc<addressing_mode::zero_page_indexed_x>(); break; + case 0xF8: run_sed(); break; + case 0xF9: run_sbc<addressing_mode::absolute_indexed_y>(); break; + case 0xFA: run_nop(); break; + case 0xFB: run_isc<addressing_mode::absolute_indexed_y>(); break; + case 0xFC: run_nop<addressing_mode::absolute_indexed_x>(); break; + case 0xFD: run_sbc<addressing_mode::absolute_indexed_x>(); break; + case 0xFE: run_inc<addressing_mode::absolute_indexed_x>(); break; + case 0xFF: run_isc<addressing_mode::absolute_indexed_x>(); break; + default: std::abort(); + } + } + + // ----------------------------------------------------------------------------------------------------------------- + // Instructions + // ----------------------------------------------------------------------------------------------------------------- + + template<cpu::addressing_mode Mode> + auto cpu::run_brk() -> void + { + // BRK: Break + fetch_operand<Mode>(); + execute_interrupt(address{ 0xFFFE }); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ora() -> void + { + // ORA: Logical Inclusive OR + auto operand = fetch_operand<Mode>(); + eval_ora(operand.read()); + current_cycles_ += operand.get_cycles(); + } + + auto cpu::run_stp() -> void + { + // TODO: Proper STP handling + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_slo() -> void + { + // SLO: ASL + ORA + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + auto const tmp = eval_asl(operand.read()); + operand.write(tmp); + eval_ora(tmp); + current_cycles_ += operand.get_cycles() + cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_nop() -> void + { + // NOP: NOP + auto operand = fetch_operand<Mode>(); + current_cycles_ += operand.get_cycles(); + } + + auto cpu::run_nop() -> void + { + // NOP: NOP + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_php() -> void + { + // PHP: Push Processor Status + eval_php(); + current_cycles_ += cycle_count::from_cpu(3); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_asl() -> void + { + // ASL: Arithmetic Shift Left + auto operand = fetch_operand<Mode>(); + operand.write(eval_asl(operand.read())); + current_cycles_ += shift_cycle_count<Mode>(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_anc() -> void + { + // ANC: AND #i + copy N to C + auto operand = fetch_operand<Mode>(); + eval_and(operand.read()); + registers_.c = registers_.n; + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bpl() -> void + { + // BPL: Break If Positive + branch<Mode>(!registers_.n); + } + + auto cpu::run_clc() -> void + { + // CLC: Clear Carry Flag + registers_.c = false; + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_jsr() -> void + { + // JSR: Jump to Subroutine + static_assert(Mode == addressing_mode::absolute, "JSR only supports absolute addressing."); + + push_stack16(registers_.pc + 1); + + auto operand = fetch_operand<Mode>(); + registers_.pc = operand.get_address().get_absolute(); + current_cycles_ += cycle_count::from_cpu(6); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_and() -> void + { + // AND: Logical AND + auto operand = fetch_operand<Mode>(); + eval_and(operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_rla() -> void + { + // RLA: ROL + AND + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + auto const tmp = eval_rol(operand.read()); + operand.write(tmp); + eval_and(tmp); + current_cycles_ += operand.get_cycles() + cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bit() -> void + { + // BIT: Bit Test + auto operand = fetch_operand<Mode>(); + auto const arg = operand.read(); + auto const res = (registers_.a & arg) != 0; + registers_.z = res == 0; + registers_.v = (arg & 0x40) != 0; + registers_.n = (arg & 0x80) != 0; + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_rol() -> void + { + // ROR: Rotate Left + auto operand = fetch_operand<Mode>(); + auto const result = eval_rol(operand.read()); + operand.write(result); + current_cycles_ += shift_cycle_count<Mode>(); + } + + auto cpu::run_plp() -> void + { + // PLP: Pull Processor Status + eval_plp(); + current_cycles_ += cycle_count::from_cpu(4); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bmi() -> void + { + branch<Mode>(registers_.n); + } + + auto cpu::run_sec() -> void + { + // SEC: Set Carry Flag + registers_.c = true; + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_rti() -> void + { + // RTI: Return From Interrupt + eval_plp(); + registers_.pc = pop_stack16(); + current_cycles_ += cycle_count::from_cpu(6); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_eor() -> void + { + // EOR: Exclusive OR + auto operand = fetch_operand<Mode>(); + eval_eor(operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sre() -> void + { + // SRE: LSR + EOR + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + auto const tmp = eval_lsr(operand.read()); + operand.write(tmp); + eval_eor(tmp); + current_cycles_ += operand.get_cycles() + cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_lsr() -> void + { + // LSR: Logical Shift Right + auto operand = fetch_operand<Mode>(); + auto const old_val = operand.read(); + auto const new_val = static_cast<std::uint8_t>(old_val >> 1); + operand.write(new_val); + registers_.c = (old_val & 0x1) != 0; + update_zn(new_val); + current_cycles_ += shift_cycle_count<Mode>(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_alr() -> void + { + // ALR: AND #i + LSR A + auto operand = fetch_operand<Mode>(); + eval_and(operand.read()); + registers_.a = eval_lsr(registers_.a); + current_cycles_ += operand.get_cycles(); + } + + auto cpu::run_pha() -> void + { + // PHA: Push Accumulator + push_stack8(registers_.a); + current_cycles_ += cycle_count::from_cpu(3); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_jmp() -> void + { + // JMP: Jump + auto operand = fetch_operand<Mode>(); + registers_.pc = operand.get_address().get_absolute(); + current_cycles_ += jump_cycle_count<Mode>(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bvc() -> void + { + // BVC: Branch If Overflow Clear + branch<Mode>(!registers_.v); + } + + auto cpu::run_cli() -> void + { + // CLI: Clear Interrupt Disable + registers_.i = false; + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_rts() -> void + { + // RTS: Return from Subroutine + auto const addr = address{ pop_stack16() } + 1; + registers_.pc = addr.get_absolute(); + current_cycles_ += cycle_count::from_cpu(6); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_adc() -> void + { + // ADC: Add With Carry + auto operand = fetch_operand<Mode>(); + eval_adc(operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_rra() -> void + { + // RRA: ROR + ADC + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + auto const tmp = eval_ror(operand.read()); + operand.write(tmp); + eval_adc(tmp); + current_cycles_ += operand.get_cycles() + cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ror() -> void + { + // ROR: Rotate Right + auto operand = fetch_operand<Mode>(); + auto const result = eval_ror(operand.read()); + operand.write(result); + current_cycles_ += shift_cycle_count<Mode>(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_arr() -> void + { + // ARR: AND #i + ROR A but with different flags + // TODO + std::abort(); + } + + auto cpu::run_pla() -> void + { + // PLA: Pull Accumulator + registers_.a = pop_stack8(); + update_zn(registers_.a); + current_cycles_ += cycle_count::from_cpu(4); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bvs() -> void + { + // BVS: Branch If Overflow Set + branch<Mode>(registers_.v); + } + + auto cpu::run_sei() -> void + { + // SEI: Set Interrupt Disable + registers_.i = true; + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sta() -> void + { + // STA: Store Accumulator + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + operand.write(registers_.a); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ahx() -> void + { + // AHX: ??? + // TODO + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sax() -> void + { + // SAX: AND A X + auto operand = fetch_operand<Mode>(); + auto const result = static_cast<std::uint8_t>(registers_.a & registers_.x); + operand.write(result); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sty() -> void + { + // STX: Store Y + auto operand = fetch_operand<Mode>(); + operand.write(registers_.y); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_stx() -> void + { + // STX: Store X + auto operand = fetch_operand<Mode>(); + operand.write(registers_.x); + current_cycles_ += operand.get_cycles(); + } + + auto cpu::run_dey() -> void + { + // DEY: Decrement Y Register + registers_.y -= 1; + update_zn(registers_.y); + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_txa() -> void + { + // TXA: Transfer X to Accumulator + registers_.a = registers_.x; + update_zn(registers_.a); + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_xaa() -> void + { + // XAA: Depends on analog behavior. + run_stp(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bcc() -> void + { + // BCC: Branch If Carry Clear + branch<Mode>(!registers_.c); + } + + auto cpu::run_tya() -> void + { + // TYA: Transfer Y to Accumulator + registers_.a = registers_.y; + update_zn(registers_.a); + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_txs() -> void + { + // TXS: Transfer X to Stack Pointer + registers_.sp = registers_.x; + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_tas() -> void + { + // TAS: ??? + // TODO + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_shy() -> void + { + // SHY: ??? + // TODO + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_shx() -> void + { + // SHY: ??? + // TODO + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ldy() -> void + { + // LDY: Load Y Register + auto operand = fetch_operand<Mode>(); + registers_.y = operand.read(); + update_zn(registers_.y); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_lda() -> void + { + // LDA: Load Accumulator + auto operand = fetch_operand<Mode>(); + registers_.a = operand.read(); + update_zn(registers_.a); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ldx() -> void + { + // LDX: Load X Register + auto operand = fetch_operand<Mode>(); + registers_.x = operand.read(); + update_zn(registers_.x); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_lax() -> void + { + // LAX: LDA + TAX + auto operand = fetch_operand<Mode>(); + registers_.x = registers_.a = operand.read(); + update_zn(registers_.a); + current_cycles_ += operand.get_cycles(); + } + + auto cpu::run_tay() -> void + { + // TAY: Transfer Accumulator to Y + registers_.y = registers_.a; + update_zn(registers_.y); + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_tax() -> void + { + // TAX: Transfer Accumulator to X + registers_.x = registers_.a; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bcs() -> void + { + // BCS: Branch If Carry Set + branch<Mode>(registers_.c); + } + + auto cpu::run_clv() -> void + { + // CLV: Clear Overflow Flag + registers_.v = false; + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_tsx() -> void + { + // TSX: Transfer Stack Pointer to X + registers_.x = registers_.sp; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_las() -> void + { + // LAS: ??? + // TODO + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_cpy() -> void + { + // CPY: Compare Y Register + auto operand = fetch_operand<Mode>(); + eval_cmp(registers_.y, operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_axs() -> void + { + // AXS: Set X to ((A AND X) - #value without borrow), updates NZC + // TODO + std::abort(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_cmp() -> void + { + // CMP: Compare + auto operand = fetch_operand<Mode>(); + eval_cmp(registers_.a, operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_dcp() -> void + { + // DCP: DEC + CMP + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + auto const tmp = static_cast<std::uint8_t>(operand.read() - 1); + operand.write(tmp); + eval_cmp(registers_.a, tmp); + current_cycles_ += operand.get_cycles() + cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_dec() -> void + { + // DEC: Decrement Memory + auto operand = fetch_operand<Mode>(); + auto const old_val = read8(operand.get_address()); + auto const new_val = static_cast<std::uint8_t>(old_val - 1); + write8(operand.get_address(), new_val); + update_zn(new_val); + current_cycles_ += inc_dec_cycle_count<Mode>(); + } + + auto cpu::run_iny() -> void + { + // INY: Increment Y + registers_.y += 1; + update_zn(registers_.y); + current_cycles_ += cycle_count::from_cpu(2); + } + + auto cpu::run_dex() -> void + { + // DEX: Decrement X Register + registers_.x -= 1; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bne() -> void + { + // BNE: Branch If Not Equal + branch<Mode>(!registers_.z); + } + + auto cpu::run_cld() -> void + { + // CLD: Clear Decimal Mode + registers_.d = false; + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_cpx() -> void + { + // CPX: Compare X Register + auto operand = fetch_operand<Mode>(); + eval_cmp(registers_.x, operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sbc() -> void + { + // SBC: Subtract With Carry + auto operand = fetch_operand<Mode>(); + eval_adc(~operand.read()); + current_cycles_ += operand.get_cycles(); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_isc() -> void + { + // ISC: INC + SBC + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + auto const tmp = static_cast<std::uint8_t>(operand.read() + 1); + operand.write(tmp); + eval_adc(~tmp); + current_cycles_ += operand.get_cycles() + cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_inc() -> void + { + // INC: Increment Memory + auto operand = fetch_operand<Mode>(); + auto const old_val = read8(operand.get_address()); + auto const new_val = static_cast<std::uint8_t>(old_val + 1); + write8(operand.get_address(), new_val); + update_zn(new_val); + current_cycles_ += inc_dec_cycle_count<Mode>(); + } + + auto cpu::run_inx() -> void + { + // INX: Increment X + registers_.x += 1; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + } + + template<cpu::addressing_mode Mode> + auto cpu::run_beq() -> void + { + // BEQ: Branch If Equal + branch<Mode>(registers_.z); + } + + auto cpu::run_sed() -> void + { + // SED: Set Decimal Flag + registers_.d = true; + current_cycles_ += cycle_count::from_cpu(2); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------------------------------------------------- + + auto cpu::advance_pc8() -> std::uint8_t + { + auto const val = read8(address{ registers_.pc }); + registers_.pc += 1; + return val; + } + + auto cpu::advance_pc16() -> std::uint16_t + { + auto const val = read16(address{ registers_.pc }); + registers_.pc += 2; + return val; + } + + auto cpu::push_stack8(std::uint8_t const value) -> void + { + write8(stack_offset + registers_.sp, value); + registers_.sp -= 1; + } + + auto cpu::push_stack16(std::uint16_t const value) -> void + { + auto const low = static_cast<std::uint8_t>(value >> 0 & 0xFF); + auto const high = static_cast<std::uint8_t>(value >> 8 & 0xFF); + push_stack8(high); + push_stack8(low); + } + + auto cpu::pop_stack8() -> std::uint8_t + { + registers_.sp += 1; + return read8(stack_offset + registers_.sp); + } + + auto cpu::pop_stack16() -> std::uint16_t + { + auto const low = pop_stack8(); + auto const high = pop_stack8(); + return static_cast<std::uint16_t>((high << 8) | (low << 0)); + } + + auto cpu::update_zn(std::uint8_t const value) -> void + { + registers_.z = value == 0; + registers_.n = (value & 0x80) != 0; + } + + template<cpu::addressing_mode Mode> + auto cpu::branch(bool const condition) -> void + { + static_assert(Mode == addressing_mode::relative, "Branch should only be used with relative operands."); + + auto operand = fetch_operand<Mode>(); + if (condition) + { + registers_.pc = operand.get_address().get_absolute(); + current_cycles_ += operand.get_cycles(); + } + else + { + current_cycles_ += cycle_count::from_cpu(2); + } + } + + auto cpu::execute_interrupt(address const vector) -> void + { + push_stack16(registers_.pc); + eval_php(); + registers_.pc = read16(vector); + registers_.i = true; + current_cycles_ += cycle_count::from_cpu(7); + } + + auto cpu::eval_ror(std::uint8_t const arg) -> std::uint8_t + { + auto const res = static_cast<std::uint8_t>((registers_.c << 7) | (arg >> 1)); + registers_.c = (arg & 0x1) != 0; + update_zn(res); + return res; + } + + auto cpu::eval_rol(std::uint8_t const arg) -> std::uint8_t + { + auto const res = static_cast<std::uint8_t>((arg << 1) | (registers_.c ? 1 : 0)); + registers_.c = (arg & 0x80) != 0; + update_zn(res); + return res; + } + + auto cpu::eval_asl(std::uint8_t const arg) -> std::uint8_t + { + auto const new_val = static_cast<std::uint8_t>(arg << 1); + registers_.c = (arg & 0x80) != 0; + update_zn(new_val); + return new_val; + } + + auto cpu::eval_lsr(std::uint8_t const arg) -> std::uint8_t + { + auto const new_val = static_cast<std::uint8_t>(arg >> 1); + registers_.c = (arg & 0x01) != 0; + update_zn(new_val); + return new_val; + } + + auto cpu::eval_adc(std::uint8_t const arg) -> void + { + auto const old_val = registers_.a; + auto const tmp = old_val + arg + (registers_.c ? 1 : 0); + auto const new_val = static_cast<std::uint8_t>(tmp); + + registers_.a = new_val; + registers_.c = tmp > 0xFF; + registers_.v = ((old_val ^ arg) & 0x80) == 0 && ((old_val ^ new_val) & 0x80) != 0; + update_zn(registers_.a); + } + + auto cpu::eval_and(std::uint8_t const arg) -> void + { + registers_.a &= arg; + update_zn(registers_.a); + } + + auto cpu::eval_ora(std::uint8_t const arg) -> void + { + registers_.a |= arg; + update_zn(registers_.a); + } + + auto cpu::eval_eor(std::uint8_t const arg) -> void + { + registers_.a ^= arg; + update_zn(registers_.a); + } + + auto cpu::eval_cmp(std::uint8_t const a, std::uint8_t const b) -> void + { + update_zn(a - b); + registers_.c = a >= b; + } + + auto cpu::eval_plp() -> void + { + // Ignore bits 4 and 5 + registers_.p = + (pop_stack8() & 0b11001111) | + (registers_.p & 0b00110000); + } + + auto cpu::eval_php() -> void + { + // Always set bits 4 and 5. + push_stack8(registers_.p | 0b00110000); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Operands + // ----------------------------------------------------------------------------------------------------------------- + + // + // See: https://www.nesdev.org/wiki/CPU_addressing_modes + // + + template<cpu::addressing_mode Mode> + auto cpu::fetch_operand(force_page_crossing const force_page_crossing) -> operand<Mode> + { + auto addr = address{}; + auto cycles = cycle_count{}; + + switch (Mode) + { + case addressing_mode::immediate: static_assert(Mode != addressing_mode::immediate); + case addressing_mode::accumulator: static_assert(Mode != addressing_mode::accumulator); + case addressing_mode::zero_page: + { + addr = address{ advance_pc8() }; + cycles = cycle_count::from_cpu(3); + break; + } + case addressing_mode::absolute: + { + addr = address{ advance_pc16() }; + cycles = cycle_count::from_cpu(4); + break; + } + case addressing_mode::relative: + { + auto const offset = static_cast<std::int8_t>(advance_pc8()); + auto const base = address{ registers_.pc }; + addr = address{ static_cast<std::uint16_t>(registers_.pc + offset) }; + auto const page_crossing = + base.get_page() != addr.get_page() || force_page_crossing == force_page_crossing::yes; + cycles = cycle_count::from_cpu(page_crossing ? 4 : 3); + break; + } + case addressing_mode::indirect: + { + auto const arg = address{ advance_pc16() }; + // Account for buggy indirect read: https://www.nesdev.org/wiki/Errata + addr = arg.get_offset() == 0xFF + ? address{ read8(address{ arg.get_page(), 0x00 }), read8(arg) } + : address{ read16(arg) }; + cycles = cycle_count::from_cpu(6); + break; + } + case addressing_mode::zero_page_indexed_x: + { + // PEEK((arg + X) % 256) + addr = address{ 0x00, static_cast<std::uint8_t>(advance_pc8() + registers_.x) }; + cycles = cycle_count::from_cpu(4); + break; + } + case addressing_mode::zero_page_indexed_y: + { + // PEEK((arg + Y) % 256) + addr = address{ 0x00, static_cast<std::uint8_t>(advance_pc8() + registers_.y) }; + cycles = cycle_count::from_cpu(4); + break; + } + case addressing_mode::absolute_indexed_x: + { + // PEEK(arg + X) + auto const arg = address{ advance_pc16() }; + addr = arg + registers_.x; + auto const page_crossing = + addr.get_page() != arg.get_page() || force_page_crossing == force_page_crossing::yes; + cycles = cycle_count::from_cpu(page_crossing ? 5 : 4); + break; + } + case addressing_mode::absolute_indexed_y: + { + // PEEK(arg + Y) + auto const arg = address{ advance_pc16() }; + addr = arg + registers_.y; + auto const page_crossing = + addr.get_page() != arg.get_page() || force_page_crossing == force_page_crossing::yes; + cycles = cycle_count::from_cpu(page_crossing ? 5 : 4); + break; + } + case addressing_mode::indexed_indirect: + { + // PEEK(PEEK((arg + X) % 256) + PEEK((arg + X + 1) % 256) * 256) + auto const arg = advance_pc8(); + auto const page = read8(address{ 0x00, static_cast<std::uint8_t>(arg + registers_.x + 1) }); + auto const offset = read8(address{ 0x00, static_cast<std::uint8_t>(arg + registers_.x) }); + addr = address{ page, offset }; + cycles = cycle_count::from_cpu(6); + break; + } + case addressing_mode::indirect_indexed: + { + // PEEK(PEEK(arg) + PEEK((arg + 1) % 256) * 256 + Y) + auto const arg = advance_pc8(); + auto const page = read8(address{ 0x00, static_cast<std::uint8_t>(arg + 1) }); + auto const offset = read8(address{ 0x00, static_cast<std::uint8_t>(arg) }); + auto const base = address{ page, offset }; + addr = base + registers_.y; + auto const page_crossing = + addr.get_page() != base.get_page() || force_page_crossing == force_page_crossing::yes; + cycles = cycle_count::from_cpu(page_crossing ? 6 : 5); + break; + } + } + + return operand<Mode>{ *this, addr, cycles }; + } + + template<> + auto cpu::fetch_operand<cpu::addressing_mode::accumulator>(force_page_crossing) + -> operand<addressing_mode::accumulator> + { + return operand<addressing_mode::accumulator>{ *this }; + } + + template<> + auto cpu::fetch_operand<cpu::addressing_mode::immediate>(force_page_crossing) + -> operand<addressing_mode::immediate> + { + return operand<addressing_mode::immediate>{ advance_pc8() }; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Memory Access + // ----------------------------------------------------------------------------------------------------------------- + + // + // See: https://www.nesdev.org/wiki/CPU_memory_map + // + + auto cpu::read8(address const addr) -> std::uint8_t + { + if (addr <= address{ 0x1FFF }) { return ram_[addr.get_absolute() % ram_size]; } + if (addr <= address{ 0x3FFF }) + { +#ifdef NES_ENABLE_PPU + switch (addr.get_absolute() % 8) + { + case 0: return ppu_.read_latch(); + case 1: return ppu_.read_latch(); + case 2: return ppu_.read_ppustatus(); + case 3: return ppu_.read_latch(); + case 4: return ppu_.read_oamdata(); + case 5: return ppu_.read_latch(); + case 6: return ppu_.read_latch(); + case 7: return ppu_.read_ppudata(); + default: return 0x0; + } +#else + return 0x0; +#endif + } + if (addr <= address{ 0x4013 }) { return 0x0; } // TODO: APU registers +#ifdef NES_ENABLE_PPU + if (addr == address{ 0x4014 }) { return ppu_.read_latch(); } +#else + if (addr == address{ 0x4014 }) { return 0x0; } +#endif + if (addr == address{ 0x4015 }) { return 0x0; } // TODO: APU registers + if (addr == address{ 0x4016 }) { return controller_1_.read(); } + if (addr == address{ 0x4017 }) { return controller_2_.read(); } + if (addr <= address{ 0x401F }) { return 0x0; } + return mapper_.read_cpu(addr); + } + + auto cpu::read16(address const addr) -> std::uint16_t + { + auto const low = read8(addr + 0); + auto const high = read8(addr + 1); + return static_cast<std::uint16_t>((high << 8) | (low << 0)); + } + + auto cpu::write8(address const addr, std::uint8_t const value) -> void + { + if (addr <= address{ 0x1FFF }) { ram_[addr.get_absolute() % ram_size] = value; return; } + if (addr <= address{ 0x3FFF }) + { +#ifdef NES_ENABLE_PPU + switch (addr.get_absolute() % 8) + { + case 0: ppu_.write_ppuctrl(value); return; + case 1: ppu_.write_ppumask(value); return; + case 2: ppu_.write_latch(value); return; + case 3: ppu_.write_oamaddr(value); return; + case 4: ppu_.write_oamdata(value); return; + case 5: ppu_.write_ppuscroll(value); return; + case 6: ppu_.write_ppuaddr(value); return; + case 7: ppu_.write_ppudata(value); return; + default: return; + } +#else + return; +#endif + } + if (addr <= address{ 0x4013 }) { return; } // TODO: APU registers +#ifdef NES_ENABLE_PPU + if (addr == address{ 0x4014 }) { ppu_.write_oamdma(value); return; } +#else + if (addr == address{ 0x4014 }) { return; } +#endif + if (addr == address{ 0x4015 }) { return; } // TODO: APU registers + if (addr == address{ 0x4016 }) { controller_1_.write(value); return; } + if (addr == address{ 0x4017 }) { controller_2_.write(value); return; } + if (addr <= address{ 0x401F }) { return; } + mapper_.write_cpu(addr, value); + } + + auto cpu::write16(address const addr, std::uint16_t const value) -> void + { + auto const low = static_cast<std::uint8_t>(value >> 0 & 0xFF); + auto const high = static_cast<std::uint8_t>(value >> 8 & 0xFF); + write8(addr + 0, low); + write8(addr + 1, high); + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/cpu.hh b/lib/nes/cpu.hh new file mode 100644 index 0000000..9511c09 --- /dev/null +++ b/lib/nes/cpu.hh @@ -0,0 +1,294 @@ +#pragma once + +#include "nes/util/cycle-count.hh" +#include "nes/util/address.hh" + +namespace nes +{ + class ppu; + class controller; + class mapper; + struct snapshot; + + class cpu + { + static constexpr auto ram_size = std::size_t{ 0x800 }; + static constexpr auto stack_offset = address{ 0x100 }; + + cycle_count current_cycles_; + ppu& ppu_; + mapper& mapper_; + controller& controller_1_; + controller& controller_2_; + std::uint8_t ram_[ram_size]{}; + bool nmi_pending_{ false }; + + // Registers + struct + { + std::uint16_t pc{ 0 }; + std::uint8_t sp{ 0xFD }; + std::uint8_t a{ 0 }; + std::uint8_t x{ 0 }; + std::uint8_t y{ 0 }; + union + { + struct + { + bool c : 1; // carry + bool z : 1; // zero + bool i : 1; // interrupt inhibit + bool d : 1; // decimal + bool b : 1; // break + bool : 1; // (unused, always true) + bool v : 1; // overflow + bool n : 1; // negative + }; + std::uint8_t p{ 0b00100100 }; // initially, set i = true + }; + } registers_{}; + + enum class addressing_mode + { + accumulator, // A + immediate, // #v + zero_page, // d + absolute, // a + relative, // *+d + indirect, // (a) + zero_page_indexed_x, // d,x + zero_page_indexed_y, // d,y + absolute_indexed_x, // a,x + absolute_indexed_y, // a,y + indexed_indirect, // (d,x) + indirect_indexed, // (d),y + }; + + enum class force_page_crossing + { + no, + yes, + }; + + template<addressing_mode> + class operand + { + cpu& cpu_; + address address_; + cycle_count cycles_; + + public: + explicit operand(cpu& cpu, address const address, cycle_count const cycles) + : cpu_{ cpu } + , address_{ address } + , cycles_{ cycles } + { + } + + auto read() -> std::uint8_t { return cpu_.read8(address_); } + auto write(std::uint8_t const value) -> void { cpu_.write8(address_, value); } + auto get_address() -> address { return address_; } + auto get_cycles() -> cycle_count { return cycles_; } + }; + + template<> + class operand<addressing_mode::accumulator> + { + cpu& cpu_; + + public: + explicit operand(cpu& cpu) + : cpu_{ cpu } + { + } + + auto read() -> std::uint8_t { return cpu_.registers_.a; } + auto write(std::uint8_t const value) -> void { cpu_.registers_.a = value; } + auto get_cycles() -> cycle_count { return cycle_count::from_cpu(2); } + }; + + template<> + class operand<addressing_mode::immediate> + { + std::uint8_t value_{ 0 }; + + public: + explicit operand(std::uint8_t const value) + : value_{ value } + { + } + + auto read() -> std::uint8_t { return value_; } + auto get_cycles() -> cycle_count { return cycle_count::from_cpu(2); } + }; + + public: + explicit cpu(ppu&, mapper&, controller& controller_1, controller& controller_2); + + cpu(cpu const&) = delete; + cpu(cpu&&) = delete; + auto operator=(cpu const&) -> cpu& = delete; + auto operator=(cpu&&) -> cpu& = delete; + + auto get_cycles() const -> cycle_count { return current_cycles_; } + auto build_snapshot(snapshot&) -> void; + auto stall_cycles(cycle_count) -> void; + auto trigger_nmi() -> void; + auto step() -> void; + auto is_nmi_pending() const -> bool { return nmi_pending_; } // XXX: Debugging + + // Memory access + + auto read8(address) -> std::uint8_t; + auto read16(address) -> std::uint16_t; + auto write8(address, std::uint8_t) -> void; + auto write16(address, std::uint16_t) -> void; + + private: + // Instructions + +#define DEFINE_SIMPLE_INSTRUCTION(name) \ + auto run_##name() -> void; +#define DEFINE_OPERAND_INSTRUCTION(name) \ + template<addressing_mode Mode> auto run_##name() -> void; + + DEFINE_OPERAND_INSTRUCTION(brk) + DEFINE_OPERAND_INSTRUCTION(ora) + DEFINE_SIMPLE_INSTRUCTION(stp) + DEFINE_OPERAND_INSTRUCTION(slo) + DEFINE_OPERAND_INSTRUCTION(nop) + DEFINE_SIMPLE_INSTRUCTION(nop) + DEFINE_SIMPLE_INSTRUCTION(php) + DEFINE_OPERAND_INSTRUCTION(asl) + DEFINE_OPERAND_INSTRUCTION(anc) + DEFINE_OPERAND_INSTRUCTION(bpl) + DEFINE_SIMPLE_INSTRUCTION(clc) + DEFINE_OPERAND_INSTRUCTION(jsr) + DEFINE_OPERAND_INSTRUCTION(and) + DEFINE_OPERAND_INSTRUCTION(rla) + DEFINE_OPERAND_INSTRUCTION(bit) + DEFINE_OPERAND_INSTRUCTION(rol) + DEFINE_SIMPLE_INSTRUCTION(plp) + DEFINE_OPERAND_INSTRUCTION(bmi) + DEFINE_SIMPLE_INSTRUCTION(sec) + DEFINE_SIMPLE_INSTRUCTION(rti) + DEFINE_OPERAND_INSTRUCTION(eor) + DEFINE_OPERAND_INSTRUCTION(sre) + DEFINE_OPERAND_INSTRUCTION(lsr) + DEFINE_OPERAND_INSTRUCTION(alr) + DEFINE_SIMPLE_INSTRUCTION(pha) + DEFINE_OPERAND_INSTRUCTION(jmp) + DEFINE_OPERAND_INSTRUCTION(bvc) + DEFINE_SIMPLE_INSTRUCTION(cli) + DEFINE_SIMPLE_INSTRUCTION(rts) + DEFINE_OPERAND_INSTRUCTION(adc) + DEFINE_OPERAND_INSTRUCTION(rra) + DEFINE_OPERAND_INSTRUCTION(ror) + DEFINE_OPERAND_INSTRUCTION(arr) + DEFINE_SIMPLE_INSTRUCTION(pla) + DEFINE_OPERAND_INSTRUCTION(bvs) + DEFINE_SIMPLE_INSTRUCTION(sei) + DEFINE_OPERAND_INSTRUCTION(sta) + DEFINE_OPERAND_INSTRUCTION(ahx) + DEFINE_OPERAND_INSTRUCTION(sax) + DEFINE_OPERAND_INSTRUCTION(sty) + DEFINE_OPERAND_INSTRUCTION(stx) + DEFINE_SIMPLE_INSTRUCTION(dey) + DEFINE_SIMPLE_INSTRUCTION(txa) + DEFINE_OPERAND_INSTRUCTION(xaa) + DEFINE_OPERAND_INSTRUCTION(bcc) + DEFINE_SIMPLE_INSTRUCTION(tya) + DEFINE_SIMPLE_INSTRUCTION(txs) + DEFINE_OPERAND_INSTRUCTION(tas) + DEFINE_OPERAND_INSTRUCTION(shy) + DEFINE_OPERAND_INSTRUCTION(shx) + DEFINE_OPERAND_INSTRUCTION(ldy) + DEFINE_OPERAND_INSTRUCTION(lda) + DEFINE_OPERAND_INSTRUCTION(ldx) + DEFINE_OPERAND_INSTRUCTION(lax) + DEFINE_SIMPLE_INSTRUCTION(tay) + DEFINE_SIMPLE_INSTRUCTION(tax) + DEFINE_OPERAND_INSTRUCTION(bcs) + DEFINE_SIMPLE_INSTRUCTION(clv) + DEFINE_SIMPLE_INSTRUCTION(tsx) + DEFINE_OPERAND_INSTRUCTION(las) + DEFINE_OPERAND_INSTRUCTION(cpy) + DEFINE_OPERAND_INSTRUCTION(axs) + DEFINE_OPERAND_INSTRUCTION(cmp) + DEFINE_OPERAND_INSTRUCTION(dcp) + DEFINE_OPERAND_INSTRUCTION(dec) + DEFINE_SIMPLE_INSTRUCTION(iny) + DEFINE_SIMPLE_INSTRUCTION(dex) + DEFINE_OPERAND_INSTRUCTION(bne) + DEFINE_SIMPLE_INSTRUCTION(cld) + DEFINE_OPERAND_INSTRUCTION(cpx) + DEFINE_OPERAND_INSTRUCTION(sbc) + DEFINE_OPERAND_INSTRUCTION(isc) + DEFINE_OPERAND_INSTRUCTION(inc) + DEFINE_SIMPLE_INSTRUCTION(inx) + DEFINE_OPERAND_INSTRUCTION(beq) + DEFINE_SIMPLE_INSTRUCTION(sed) + +#undef DEFINE_SIMPLE_INSTRUCTION +#undef DEFINE_OPERAND_INSTRUCTION + + // Helpers + + auto advance_pc8() -> std::uint8_t; + auto advance_pc16() -> std::uint16_t; + auto push_stack8(std::uint8_t) -> void; + auto push_stack16(std::uint16_t) -> void; + auto pop_stack8() -> std::uint8_t; + auto pop_stack16() -> std::uint16_t; + auto update_zn(std::uint8_t value) -> void; + template<addressing_mode Mode> + auto branch(bool condition) -> void; + auto execute_interrupt(address) -> void; + + auto eval_ror(std::uint8_t arg) -> std::uint8_t; + auto eval_rol(std::uint8_t arg) -> std::uint8_t; + auto eval_asl(std::uint8_t arg) -> std::uint8_t; + auto eval_lsr(std::uint8_t arg) -> std::uint8_t; + auto eval_adc(std::uint8_t arg) -> void; + auto eval_and(std::uint8_t arg) -> void; + auto eval_ora(std::uint8_t arg) -> void; + auto eval_eor(std::uint8_t arg) -> void; + auto eval_cmp(std::uint8_t a, std::uint8_t b) -> void; + auto eval_plp() -> void; + auto eval_php() -> void; + + // Operands + +#define DEFINE_CYCLE_COUNT(name) \ + template<addressing_mode Mode> static auto name##_cycle_count() -> cycle_count; +#define DEFINE_CYCLE_COUNT_INSTANCE(name, mode, value) \ + template<> auto name##_cycle_count<addressing_mode::mode>() -> cycle_count { return cycle_count::from_cpu(value); } + + DEFINE_CYCLE_COUNT(jump) + DEFINE_CYCLE_COUNT_INSTANCE(jump, absolute, 3) + DEFINE_CYCLE_COUNT_INSTANCE(jump, indirect, 5) + + DEFINE_CYCLE_COUNT(shift) + DEFINE_CYCLE_COUNT_INSTANCE(shift, accumulator, 2) + DEFINE_CYCLE_COUNT_INSTANCE(shift, zero_page, 5) + DEFINE_CYCLE_COUNT_INSTANCE(shift, zero_page_indexed_x, 6) + DEFINE_CYCLE_COUNT_INSTANCE(shift, absolute, 6) + DEFINE_CYCLE_COUNT_INSTANCE(shift, absolute_indexed_x, 7) + + DEFINE_CYCLE_COUNT(inc_dec) + DEFINE_CYCLE_COUNT_INSTANCE(inc_dec, zero_page, 5) + DEFINE_CYCLE_COUNT_INSTANCE(inc_dec, zero_page_indexed_x, 6) + DEFINE_CYCLE_COUNT_INSTANCE(inc_dec, absolute, 6) + DEFINE_CYCLE_COUNT_INSTANCE(inc_dec, absolute_indexed_x, 7) + +#undef DEFINE_CYCLE_COUNT +#undef DEFINE_CYCLE_COUNT_INSTANCE + + template<addressing_mode Mode> + auto fetch_operand(force_page_crossing = force_page_crossing::no) -> operand<Mode>; + template<> + auto fetch_operand<addressing_mode::accumulator>(force_page_crossing) -> operand<addressing_mode::accumulator>; + template<> + auto fetch_operand<addressing_mode::immediate>(force_page_crossing) -> operand<addressing_mode::immediate>; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/display.cc b/lib/nes/display.cc new file mode 100644 index 0000000..4ff1d5f --- /dev/null +++ b/lib/nes/display.cc @@ -0,0 +1,16 @@ +#include "nes/display.hh" +#include "nes/util/rgb.hh" + +namespace nes +{ + auto display::clear(rgb const color) -> void + { + for (auto y = unsigned{ 0 }; y < height; ++y) + { + for (auto x = unsigned{ 0 }; x < width; ++x) + { + set(x, y, color); + } + } + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/display.hh b/lib/nes/display.hh new file mode 100644 index 0000000..14ccff1 --- /dev/null +++ b/lib/nes/display.hh @@ -0,0 +1,33 @@ +#pragma once + +namespace nes +{ + class rgb; + + /// Abstraction of the display logic. + class display + { + public: + static constexpr auto width = unsigned{ 256 }; + static constexpr auto height = unsigned{ 240 }; + + virtual ~display() = default; + + display(display const&) = delete; + display(display&&) = delete; + auto operator=(display const&) -> display& = delete; + auto operator=(display&&) -> display& = delete; + + /// Switch between front and back buffers. + virtual auto switch_buffers() -> void = 0; + /// Update the pixel at the given position in the back buffer. + virtual auto set(unsigned x, unsigned y, rgb value) -> void = 0; + /// Load the pixel value at the given position in the back buffer. + virtual auto get(unsigned x, unsigned y) const -> rgb = 0; + /// Clear the entire back buffer with a color. + virtual auto clear(rgb color) -> void; + + protected: + explicit display() = default; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/mapper.cc b/lib/nes/mapper.cc new file mode 100644 index 0000000..9346be7 --- /dev/null +++ b/lib/nes/mapper.cc @@ -0,0 +1,134 @@ +#include "nes/mapper.hh" +#include "nes/cartridge.hh" +#include <iostream> + +namespace nes +{ + mapper::mapper( + cartridge& cartridge, + read_cpu_func const read_cpu, write_cpu_func const write_cpu, + read_ppu_func const read_ppu, write_ppu_func const write_ppu) + : cartridge_{ cartridge } + , read_cpu_{ read_cpu } + , write_cpu_{ write_cpu } + , read_ppu_{ read_ppu } + , write_ppu_{ write_ppu } + { + } + + auto mapper::select(cartridge& cartridge) -> mapper + { + switch (cartridge.get_mapper_number()) + { + case 0x00: + return mapper{ + cartridge, + &mapper::nrom_read_cpu, &mapper::nrom_write_cpu, + &mapper::nrom_read_ppu, &mapper::nrom_write_ppu }; + default: + std::cerr << "Mapper not implemented: " << std::hex << cartridge.get_mapper_number() << std::endl; + std::abort(); + } + } + + // ----------------------------------------------------------------------------------------------------------------- + // NROM + // ----------------------------------------------------------------------------------------------------------------- + + auto mapper::nrom_read_cpu(address const addr) -> std::uint8_t + { + if (addr <= address{ 0x5FFF }) + { + return 0x0; + } + if (addr <= address{ 0x7FFF }) + { + return cartridge_.get_ram()[addr.get_absolute() - 0x6000]; + } + if (addr <= address{ 0xBFFF }) + { + auto const rel = addr.get_absolute() - 0x8000; + if (rel >= cartridge_.get_prg_rom_length()) { return 0x0; } + + return cartridge_.get_prg_rom()[rel]; + } + if (addr <= address{ 0xFFFF }) + { + auto rel = addr.get_absolute() - 0xC000; + if (cartridge_.get_prg_rom_length() > 0x4000) { rel += 0x4000; } + if (rel >= cartridge_.get_prg_rom_length()) { return 0x0; } + + return cartridge_.get_prg_rom()[rel]; + } + + return 0x0; + } + + auto mapper::nrom_write_cpu(address const addr, std::uint8_t const value) -> void + { + if (addr <= address{ 0x5FFF }) + { + return; + } + if (addr <= address{ 0x7FFF }) + { + cartridge_.get_ram_mut()[addr.get_absolute() - 0x6000] = value; + return; + } + } + + auto mapper::nrom_read_ppu(address const addr, std::uint8_t const* vram) -> std::uint8_t + { + if (addr <= address{ 0x1FFF }) + { + auto const rel = addr.get_absolute(); + if (rel >= cartridge_.get_chr_rom_length()) { return 0x0; } + + return cartridge_.get_chr_rom()[rel]; + } + if (addr <= address{ 0x3EFF }) + { + return vram[nrom_mirrored_vram_address(addr)]; + } + + return 0x0; + } + + auto mapper::nrom_write_ppu(address const addr, std::uint8_t* vram, std::uint8_t const value) -> void + { + if (addr <= address{ 0x1FFF }) + { + return; + } + if (addr <= address{ 0x3EFF }) + { + vram[nrom_mirrored_vram_address(addr)] = value; + return; + } + } + + auto mapper::nrom_mirrored_vram_address(address const addr) const -> unsigned + { + auto const rel = addr.get_absolute() % 0x1000; + + switch (cartridge_.get_name_table_arrangement()) + { + case name_table_arrangement::horizontal: + // $2000 = $2400, $2800 = $2c00 + if (rel <= 0x3FF) { return 0x000 + (rel % 0x400); } + if (rel <= 0x7FF) { return 0x000 + (rel % 0x400); } + if (rel <= 0xBFF) { return 0x400 + (rel % 0x400); } + if (rel <= 0xFFF) { return 0x400 + (rel % 0x400); } + break; + case name_table_arrangement::vertical: + // $2000 = $2800, $2400 = $2c00 + if (rel <= 0x3FF) { return 0x000 + (rel % 0x400); } + if (rel <= 0x7FF) { return 0x400 + (rel % 0x400); } + if (rel <= 0xBFF) { return 0x000 + (rel % 0x400); } + if (rel <= 0xFFF) { return 0x400 + (rel % 0x400); } + break; + } + + return 0; + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/mapper.hh b/lib/nes/mapper.hh new file mode 100644 index 0000000..f2873a6 --- /dev/null +++ b/lib/nes/mapper.hh @@ -0,0 +1,64 @@ +#pragma once + +#include "nes/util/address.hh" + +namespace nes +{ + class cartridge; + + class mapper + { + using read_cpu_func = std::uint8_t(mapper::*)(address); + using write_cpu_func = void(mapper::*)(address, std::uint8_t); + using read_ppu_func = std::uint8_t(mapper::*)(address, std::uint8_t const* vram); + using write_ppu_func = void(mapper::*)(address, std::uint8_t* vram, std::uint8_t); + + cartridge& cartridge_; + read_cpu_func read_cpu_; + write_cpu_func write_cpu_; + read_ppu_func read_ppu_; + write_ppu_func write_ppu_; + + public: + static auto select(cartridge&) -> mapper; + + mapper(mapper const&) = delete; + mapper(mapper&&) = delete; + auto operator=(mapper const&) -> mapper& = delete; + auto operator=(mapper&&) -> mapper& = delete; + + auto read_cpu(address const addr) -> std::uint8_t + { + return (this->*read_cpu_)(addr); + } + + auto write_cpu(address const addr, std::uint8_t const value) -> void + { + (this->*write_cpu_)(addr, value); + } + + auto read_ppu(address const addr, std::uint8_t const* vram) -> std::uint8_t + { + return (this->*read_ppu_)(addr, vram); + } + + auto write_ppu(address const addr, std::uint8_t* vram, std::uint8_t const value) -> void + { + (this->*write_ppu_)(addr, vram, value); + } + + private: + explicit mapper( + cartridge& cartridge, + read_cpu_func read_cpu, write_cpu_func write_cpu, + read_ppu_func read_ppu, write_ppu_func write_ppu); + + // Implementations + + auto nrom_read_cpu(address) -> std::uint8_t; + auto nrom_write_cpu(address, std::uint8_t) -> void; + auto nrom_read_ppu(address, std::uint8_t const* vram) -> std::uint8_t; + auto nrom_write_ppu(address, std::uint8_t* vram, std::uint8_t) -> void; + auto nrom_mirrored_vram_address(address) const -> unsigned; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/nes.cc b/lib/nes/nes.cc new file mode 100644 index 0000000..01e339d --- /dev/null +++ b/lib/nes/nes.cc @@ -0,0 +1,74 @@ +#include "nes/nes.hh" +#include "nes/mapper.hh" +#include "nes/display.hh" +#include "nes/util/rgb.hh" +#include "nes/util/snapshot.hh" +#include <iostream> + +namespace nes +{ + nes::nes(cartridge cartridge, display& display) + : cartridge_{ std::move(cartridge) } + , display_{ display } + , mapper_{ mapper::select(cartridge_) } + , ppu_{ cpu_, mapper_, display_ } + , cpu_{ ppu_, mapper_, controller_1_, controller_2_ } + { + if (!cartridge_.is_valid()) + { + std::cerr << "Cartridge not valid!" << std::endl; + std::abort(); + } + + display_.clear(rgb{}); + display_.switch_buffers(); + display_.clear(rgb{}); + } + + auto nes::step(std::chrono::microseconds const delta) -> void + { + step(cycle_count::from_duration(delta)); + } + + auto nes::step(cycle_count const delta) -> void + { + current_cycles_ += delta; + while (cpu_.get_cycles() < current_cycles_) + { + cpu_.step(); + while (ppu_.get_cycles() < cpu_.get_cycles()) + { + ppu_.step(); + } + } + } + + auto nes::step_to_nmi() -> void + { + while (cpu_.is_nmi_pending()) + { + cpu_.step(); + while (ppu_.get_cycles() < cpu_.get_cycles()) + { + ppu_.step(); + } + } + while (!cpu_.is_nmi_pending()) + { + cpu_.step(); + while (ppu_.get_cycles() < cpu_.get_cycles()) + { + ppu_.step(); + } + } + } + + auto nes::get_snapshot() -> snapshot + { + auto res = snapshot{}; + res.sram = std::vector(cartridge_.get_ram(), cartridge_.get_ram() + cartridge_.get_ram_length()); + cpu_.build_snapshot(res); + ppu_.build_snapshot(res); + return res; + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/nes.hh b/lib/nes/nes.hh new file mode 100644 index 0000000..fe57cd4 --- /dev/null +++ b/lib/nes/nes.hh @@ -0,0 +1,46 @@ +#pragma once + +#include "nes/util/cycle-count.hh" +#include "nes/cartridge.hh" +#include "nes/controller.hh" +#include "nes/cpu.hh" +#include "nes/ppu.hh" +#include "nes/mapper.hh" + +namespace nes +{ + class display; + struct snapshot; + + /// The main console abstraction. + class nes + { + cartridge cartridge_; + display& display_; + controller controller_1_; + controller controller_2_; + mapper mapper_; + ppu ppu_; + cpu cpu_; + cycle_count current_cycles_; + + public: + explicit nes(cartridge, display&); + + nes(nes const&) = delete; + nes(nes&&) = delete; + auto operator=(nes const&) -> nes& = delete; + auto operator=(nes&&) -> nes& = delete; + + auto get_controller_1() const -> controller const& { return controller_1_; } + auto get_controller_1() -> controller& { return controller_1_; } + auto get_controller_2() const -> controller const& { return controller_2_; } + auto get_controller_2() -> controller& { return controller_2_; } + + auto step(std::chrono::microseconds delta) -> void; + auto step(cycle_count delta) -> void; + auto step_to_nmi() -> void; // XXX: Debugging + + auto get_snapshot() -> snapshot; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/ppu.cc b/lib/nes/ppu.cc new file mode 100644 index 0000000..3170de9 --- /dev/null +++ b/lib/nes/ppu.cc @@ -0,0 +1,737 @@ +#include "nes/ppu.hh" +#include "nes/display.hh" +#include "nes/cpu.hh" +#include "nes/mapper.hh" +#include "nes/util/address.hh" +#include "nes/util/rgb.hh" +#include "nes/util/snapshot.hh" +#include "nes/util/debug.hh" + +namespace nes +{ + ppu::ppu(cpu& cpu, mapper& mapper, display& display) + : cpu_{ cpu } + , mapper_{ mapper } + , display_{ display } + { + } + + auto ppu::build_snapshot(snapshot& snapshot) -> void + { + snapshot.vram = std::vector(std::begin(vram_), std::end(vram_)); + snapshot.oam = std::vector(std::begin(oam_), std::end(oam_)); + } + + auto ppu::step() -> void + { + // See https://www.nesdev.org/wiki/PPU_rendering + // Based on: https://github.com/fogleman/nes/blob/master/nes/ppu.go + + auto const enable_rendering = mask_.enable_background || mask_.enable_sprites; + + current_cycles_ += cycle_count::from_ppu(1); + scanline_cycle_ += 1; + if (enable_rendering && !even_frame_ && scanline_ == 261 && scanline_cycle_ == 340) + { + current_cycles_ += cycle_count::from_ppu(1); + scanline_cycle_ += 1; + } + if (scanline_cycle_ == 341) + { + scanline_cycle_ = 0; + scanline_ += 1; + + if (scanline_ == 262) + { + scanline_ = 0; + even_frame_ = !even_frame_; + } + } + + auto const pre_line = scanline_ == 261; + auto const visible_line = scanline_ < 240; + auto const render_line = pre_line || visible_line; + auto const pre_fetch_cycle = scanline_cycle_ >= 321 && scanline_cycle_ <= 336; + auto const visible_cycle = scanline_cycle_ >= 1 && scanline_cycle_ <= 256; + auto const fetch_cycle = pre_fetch_cycle || visible_cycle; + + // Background Logic + if (enable_rendering) + { + if (visible_line && visible_cycle) + { + render_pixel(); + } + + if (render_line && fetch_cycle) + { + tile_data_ <<= 4; + switch (scanline_cycle_ % 8) + { + case 1: + fetch_name_table_byte(); + break; + case 3: + fetch_attribute_table_byte(); + break; + case 5: + fetch_low_tile_byte(); + break; + case 7: + fetch_high_tile_byte(); + break; + case 0: + store_tile_data(); + break; + default: + break; + } + } + + if (pre_line && scanline_cycle_ >= 280 && scanline_cycle_ <= 304) + { + copy_y(); + } + + if (render_line) + { + if (fetch_cycle && scanline_cycle_ % 8 == 0) + { + increment_x(); + } + if (scanline_cycle_ == 256) + { + increment_y(); + } + if (scanline_cycle_ == 257) + { + copy_x(); + } + } + } + + // Sprite Logic + if (enable_rendering) + { + if (scanline_cycle_ == 257) + { + if (visible_line) + { + evaluate_sprites(); + } + else + { + sprite_count_ = 0; + } + } + } + + // Vblank Logic + if (scanline_ == 241 && scanline_cycle_ == 1) + { + display_.switch_buffers(); + status_.vblank = true; + nmi_change(); + } + if (pre_line && scanline_cycle_ == 1) + { + status_.vblank = false; + status_.sprite_zero_hit = false; + status_.sprite_overflow = false; + } + } + + auto ppu::render_pixel() -> void + { + auto const x = scanline_cycle_ - 1; + auto const y = scanline_; + + auto background_color = color_index{ 0 }; + if (mask_.enable_background) + { + auto const tile_data = static_cast<std::uint32_t>(tile_data_ >> 32); + background_color = color_index{ static_cast<std::uint8_t>((tile_data >> ((7 - internal_.x) * 4)) & 0xF) }; + background_color.role = role::background; + } + + auto foreground = sprite{}; + auto foreground_color = color_index{ 0 }; + if (mask_.enable_sprites) + { + for (auto i = unsigned{ 0 }; i < sprite_count_; ++i) + { + auto const s = sprites_[i]; + auto const offset = static_cast<int>(x) - static_cast<int>(s.position); + if (offset < 0 || offset > 7) { continue; } + auto const color = color_index{ static_cast<std::uint8_t>((s.pattern >> ((7 - offset) * 4)) & 0xF) }; + if (color.color == 0) { continue; } + + foreground = s; + foreground_color = color; + foreground_color.role = role::sprite; + break; + } + } + + auto has_background = background_color.color != 0; + auto has_foreground = foreground_color.color != 0; + + if (x < 8 && !mask_.show_background_start) { has_background = false; } + if (x < 8 && !mask_.show_sprites_start) { has_foreground = false; } + + auto color = color_index{ 0 }; + if (!has_background && has_foreground) + { + color = foreground_color; + } + if (has_background && !has_foreground) + { + color = background_color; + } + if (has_background && has_foreground) + { + if (foreground.is_sprite_zero && x < 255) + { + status_.sprite_zero_hit = true; + } + + color = foreground.is_in_front ? foreground_color : background_color; + } + + display_.set(x, y, resolve_color(get_color(color))); + } + + auto ppu::fetch_name_table_byte() -> void + { + auto const addr = static_cast<std::uint16_t>(0x2000 | (internal_.v & 0x0FFF)); + name_table_byte_ = read8(address{ addr }); + } + + auto ppu::fetch_attribute_table_byte() -> void + { + auto const v = internal_.v; + auto const addr = static_cast<std::uint16_t>(0x23C0 | (v & 0x0C00) | ((v >> 4) & 0x38) | ((v >> 2) & 0x07)); + auto const shift = ((v >> 4) & 4) | (v & 2); + attribute_table_byte_ = static_cast<std::uint8_t>(((read8(address{ addr }) >> shift) & 3) << 2); + } + + auto ppu::fetch_low_tile_byte() -> void + { + auto const fine_y = (internal_.v >> 12) & 7; + auto const table = control_.background_pattern_table; + auto const tile = name_table_byte_; + auto const addr = static_cast<std::uint16_t>(0x1000 * table + 16 * tile + fine_y); + low_tile_byte_ = read8(address{ addr }); + } + + auto ppu::fetch_high_tile_byte() -> void + { + auto const fine_y = (internal_.v >> 12) & 7; + auto const table = control_.background_pattern_table; + auto const tile = name_table_byte_; + auto const addr = static_cast<std::uint16_t>(0x1000 * table + 16 * tile + fine_y); + high_tile_byte_ = read8(address{ addr } + 8); + } + + auto ppu::store_tile_data() -> void + { + auto data = std::uint32_t{ 0 }; + for (auto i = unsigned{ 0 }; i < 8; ++i) + { + auto const p1 = (low_tile_byte_ & 0x80) >> 7; + auto const p2 = (high_tile_byte_ & 0x80) >> 6; + low_tile_byte_ <<= 1; + high_tile_byte_ <<= 1; + data <<= 4; + data |= attribute_table_byte_ | p1 | p2; + } + tile_data_ |= data; + } + + auto ppu::increment_x() -> void + { + // If coarse X == 31 + if ((internal_.v & 0x1F) == 0x1F) + { + // Coarse X = 0 + // TODO: Check + // internal_.v &= ~0x1F; + internal_.v &= 0xFFE0; + // Switch horizontal nametable + internal_.v ^= 0x400; + } + else + { + // Increment coarse X + internal_.v += 1; + } + } + + auto ppu::increment_y() -> void + { + // If fine Y == 7 + if ((internal_.v & 0x7000) == 0x7000) + { + // Fine Y = 0 + internal_.v &= 0x8FFF; + // Let y = coarse Y + auto y = (internal_.v & 0x03E0) >> 5; + if (y == 29) + { + // Coarse Y = 0 + y = 0; + // Switch vertical nametable + internal_.v ^= 0x0800; + } + else if (y == 31) + { + // Coarse Y = 0, nametable not switched + y = 0; + } + else + { + // Increment coarse Y + y += 1; + } + // Put coarse Y back into v + // TODO: Check + // internal_.v &= ~0x03E0; + internal_.v &= 0xFC1F; + internal_.v |= y << 5; + } + else + { + // Increment fine Y + internal_.v += 0x1000; + } + } + + auto ppu::copy_x() -> void + { + internal_.v = (internal_.v & 0xFBE0) | (internal_.t & 0x041F); + } + + auto ppu::copy_y() -> void + { + internal_.v = (internal_.v & 0x841F) | (internal_.t & 0x7BE0); + } + + auto ppu::evaluate_sprites() -> void + { + auto const height = sprite_height(); + sprite_count_ = 0; + for (auto i = unsigned{ 0 }; i < 64; ++i) + { + auto const y = oam_[i * 4 + 0]; + auto const a = oam_[i * 4 + 2]; + auto const x = oam_[i * 4 + 3]; + auto const row = static_cast<int>(scanline_) - static_cast<int>(y); + if (row < 0 || row >= height) { continue; } + + if (sprite_count_ < 8) + { + sprites_[sprite_count_].pattern = fetch_sprite_pattern(i, row); + sprites_[sprite_count_].position = x; + // TODO: Check + // sprites_[sprite_count_].is_in_front = a & 0b00100000; + sprites_[sprite_count_].is_in_front = ((a >> 5) & 1) == 0; + sprites_[sprite_count_].is_sprite_zero = i == 0; + sprite_count_ += 1; + } + else + { + status_.sprite_overflow = true; + break; + } + } + } + + auto ppu::fetch_sprite_pattern(unsigned const i, unsigned row) -> std::uint32_t + { + auto tile = oam_[i * 4 + 1]; + auto const attributes = oam_[i * 4 + 2]; + auto const flip_vertical = (attributes & 0x80) == 0x80; + if (flip_vertical) { row = sprite_height() - 1 - row; } + auto addr = address{}; + switch (control_.sprite_size) + { + case sprite_size::_8x8: + { + auto const table = control_.sprite_pattern_table; + addr = address{ static_cast<std::uint16_t>(0x1000 * table + 16 * tile + row) }; + break; + } + case sprite_size::_8x16: + { + auto const table = tile & 1; + tile &= 0xFE; + if (row > 7) + { + tile += 1; + row -= 8; + } + addr = address{ static_cast<std::uint16_t>(0x1000 * table + 16 * tile + row) }; + break; + } + } + + auto const a = (attributes & 3) << 2; + auto low_tile_byte = read8(addr); + auto high_tile_byte = read8(addr + 8); + auto data = std::uint32_t{ 0 }; + for (auto j = unsigned{ 0 }; j < 8; ++j) + { + auto const flip_horizontal = (attributes & 0x40) == 0x40; + std::uint8_t p1, p2; + if (flip_horizontal) + { + p1 = (low_tile_byte & 1) << 0; + p2 = (high_tile_byte & 1) << 1; + low_tile_byte >>= 1; + high_tile_byte >>= 1; + } + else + { + p1 = (low_tile_byte & 0x80) >> 7; + p2 = (high_tile_byte & 0x80) >> 6; + low_tile_byte <<= 1; + high_tile_byte <<= 1; + } + data <<= 4; + data |= a | p1 | p2; + } + + return data; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Memory Access + // ----------------------------------------------------------------------------------------------------------------- + + // + // See: https://www.nesdev.org/wiki/PPU_memory_map + // + + auto ppu::read8(address addr) -> std::uint8_t + { + addr = addr % 0x4000; // PPU only has 16 KiB addresses. + if (addr <= address{ 0x3EFF }) { return mapper_.read_ppu(addr, vram_); } + if (addr <= address{ 0x3FFF }) + { + auto const index = color_index{ static_cast<std::uint8_t>(addr.get_absolute() % 0x20) }; + auto const color = get_color(index); + return static_cast<std::uint8_t>(color); + } + + return 0x0; + } + + auto ppu::write8(address addr, std::uint8_t const value) -> void + { + addr = addr % 0x4000; // PPU only has 16 KiB addresses. + if (addr <= address{ 0x3EFF }) + { + mapper_.write_ppu(addr, vram_, value); + return; + } + if (addr <= address{ 0x3FFF }) + { + auto const index = color_index{ static_cast<std::uint8_t>(addr.get_absolute() % 0x20) }; + get_color(index) = color{ value }; + return; + } + } + + // ----------------------------------------------------------------------------------------------------------------- + // IO Registers + // ----------------------------------------------------------------------------------------------------------------- + + // + // See: https://www.nesdev.org/wiki/PPU_registers + // + + auto ppu::read_latch() -> std::uint8_t + { + return latch_; + } + + auto ppu::read_ppustatus() -> std::uint8_t + { + auto const base = latch_ & ~status_mask; + auto const res = base | (status_.value & status_mask); + NES_DEBUG_LOG(ppu, "PPUSTATUS -> {:#2x}", res); + + status_.vblank = false; + internal_.w = false; + + write_latch(res); + return res; + } + + auto ppu::read_oamdata() -> std::uint8_t + { + auto res = oam_[oamaddr_]; + if ((oamaddr_ & 0x3) == 0x2) + { + res &= 0xE3; + } + NES_DEBUG_LOG(ppu, "OAMDATA -> {:#2x}", res); + write_latch(res); + return res; + } + + auto ppu::read_ppudata() -> std::uint8_t + { + auto const addr = address{ internal_.v }; + auto res = read8(addr); + write_latch(ppudata_read_buffer_); + + if (addr % 0x4000 <= address{ 0x3EFF }) + { + std::swap(res, ppudata_read_buffer_); + } + else + { + // For palette buffers: buffer the mirrored nametable instead. + ppudata_read_buffer_ = read8(addr - 0x1000); + } + + NES_DEBUG_LOG(ppu, "PPUDATA -> {:#2x}", res); + increment_vram(); + return res; + } + + auto ppu::write_latch(std::uint8_t const value) -> void + { + latch_ = value; + } + + auto ppu::write_ppuctrl(std::uint8_t const value) -> void + { + write_latch(value); + if (current_cycles_ > boot_up_cycles) + { + NES_DEBUG_LOG(ppu, "PPUCTRL <- {:#2x}", value); + control_.value = value; + internal_.t = (internal_.t & 0xF3FF) | ((value & 0x03) << 10); + nmi_change(); + } + } + + auto ppu::write_ppuscroll(std::uint8_t const value) -> void + { + write_latch(value); + if (current_cycles_ > boot_up_cycles) + { + NES_DEBUG_LOG(ppu, "PPUSCROLL <- {:#2x}", value); + if (!internal_.w) + { + // First write -> x value. + internal_.t = (internal_.t & 0b1111111111100000) | (value >> 3); + internal_.x = value & 0b00000111; + } + else + { + // Second write -> y value. + internal_.t = (internal_.t & 0b1000111111111111) | ((value & 0b00000111) << 12); + internal_.t = (internal_.t & 0b1111110000011111) | ((value & 0b11111000) << 2); + } + + internal_.w = !internal_.w; + } + } + + auto ppu::write_ppumask(std::uint8_t const value) -> void + { + write_latch(value); + if (current_cycles_ > boot_up_cycles) + { + NES_DEBUG_LOG(ppu, "PPUMASK <- {:#2x}", value); + mask_.value = value; + } + } + + auto ppu::write_ppuaddr(std::uint8_t const value) -> void + { + write_latch(value); + if (current_cycles_ > boot_up_cycles) + { + NES_DEBUG_LOG(ppu, "PPUADDR <- {:#2x}", value); + if (!internal_.w) + { + // First write. + internal_.t = (internal_.t & 0b1000000011111111) | ((value & 0b00111111) << 8); + } + else + { + // Second write. + internal_.t = (internal_.t & 0b1111111100000000) | ((value & 0b11111111) << 0); + internal_.v = internal_.t; + } + + internal_.w = !internal_.w; + } + } + + auto ppu::write_ppudata(std::uint8_t const value) -> void + { + NES_DEBUG_LOG(ppu, "PPUDATA <- {:#2x} (address: {:#4x})", value, internal_.v); + write_latch(value); + write8(address{ internal_.v }, value); + increment_vram(); + } + + auto ppu::write_oamaddr(std::uint8_t const value) -> void + { + NES_DEBUG_LOG(ppu, "OAMADDR <- {:#2x}", value); + write_latch(value); + oamaddr_ = value; + } + + auto ppu::write_oamdata(std::uint8_t const value) -> void + { + NES_DEBUG_LOG(ppu, "OAMDATA <- {:#2x}", value); + write_latch(value); + oam_[oamaddr_] = value; + oamaddr_ += 1; + } + + auto ppu::write_oamdma(std::uint8_t const value) -> void + { + // TODO: Do this in CPU::step instead in case there's an NMI? + NES_DEBUG_LOG(ppu, "OAMDMA <- {:#2x}", value); + auto addr = address{ value, 0x00 }; + for (auto i = unsigned{ 0 }; i < 256; ++i) + { + oam_[oamaddr_] = cpu_.read8(addr); + oamaddr_ += 1; + addr = addr + 1; + } + + auto const stalled_cycles = cycle_count::from_cpu((cpu_.get_cycles().to_cpu() % 2 == 0) ? 513 : 514); + cpu_.stall_cycles(stalled_cycles); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------------------------------------------------- + + auto ppu::nmi_change() -> void + { + if (control_.vblank_nmi && status_.vblank) + { + cpu_.trigger_nmi(); + } + } + + auto ppu::sprite_height() -> unsigned + { + switch (control_.sprite_size) + { + case sprite_size::_8x8: + return 8; + case sprite_size::_8x16: + return 16; + } + + return 8; + } + + auto ppu::increment_vram() -> void + { + switch (control_.vram_increment) + { + case vram_increment::add_1_across: + internal_.v += 1; + break; + case vram_increment::add_32_down: + internal_.v += 32; + break; + } + } + + auto ppu::get_color(color_index index) -> color& + { + // See https://www.nesdev.org/wiki/PPU_palettes + index.value = index.value % 0x20; + if (index.color == 0) + { + // The first color is mirrored between background and foreground palettes. + index.role = role::background; + } + return palette_buffer_[index.value]; + } + + auto ppu::resolve_color(color const color) -> rgb + { + auto const index = static_cast<std::uint8_t>(color); + switch (index & 0x3F) + { + case 0x00: return rgb::from_hex(0x666666); + case 0x01: return rgb::from_hex(0x002A88); + case 0x02: return rgb::from_hex(0x1412A7); + case 0x03: return rgb::from_hex(0x3B00A4); + case 0x04: return rgb::from_hex(0x5C007E); + case 0x05: return rgb::from_hex(0x6E0040); + case 0x06: return rgb::from_hex(0x6C0600); + case 0x07: return rgb::from_hex(0x561D00); + case 0x08: return rgb::from_hex(0x333500); + case 0x09: return rgb::from_hex(0x0B4800); + case 0x0A: return rgb::from_hex(0x005200); + case 0x0B: return rgb::from_hex(0x004F08); + case 0x0C: return rgb::from_hex(0x00404D); + case 0x0D: return rgb::from_hex(0x000000); + case 0x0E: return rgb::from_hex(0x000000); + case 0x0F: return rgb::from_hex(0x000000); + case 0x10: return rgb::from_hex(0xADADAD); + case 0x11: return rgb::from_hex(0x155FD9); + case 0x12: return rgb::from_hex(0x4240FF); + case 0x13: return rgb::from_hex(0x7527FE); + case 0x14: return rgb::from_hex(0xA01ACC); + case 0x15: return rgb::from_hex(0xB71E7B); + case 0x16: return rgb::from_hex(0xB53120); + case 0x17: return rgb::from_hex(0x994E00); + case 0x18: return rgb::from_hex(0x6B6D00); + case 0x19: return rgb::from_hex(0x388700); + case 0x1A: return rgb::from_hex(0x0C9300); + case 0x1B: return rgb::from_hex(0x008F32); + case 0x1C: return rgb::from_hex(0x007C8D); + case 0x1D: return rgb::from_hex(0x000000); + case 0x1E: return rgb::from_hex(0x000000); + case 0x1F: return rgb::from_hex(0x000000); + case 0x20: return rgb::from_hex(0xFFFEFF); + case 0x21: return rgb::from_hex(0x64B0FF); + case 0x22: return rgb::from_hex(0x9290FF); + case 0x23: return rgb::from_hex(0xC676FF); + case 0x24: return rgb::from_hex(0xF36AFF); + case 0x25: return rgb::from_hex(0xFE6ECC); + case 0x26: return rgb::from_hex(0xFE8170); + case 0x27: return rgb::from_hex(0xEA9E22); + case 0x28: return rgb::from_hex(0xBCBE00); + case 0x29: return rgb::from_hex(0x88D800); + case 0x2A: return rgb::from_hex(0x5CE430); + case 0x2B: return rgb::from_hex(0x45E082); + case 0x2C: return rgb::from_hex(0x48CDDE); + case 0x2D: return rgb::from_hex(0x4F4F4F); + case 0x2E: return rgb::from_hex(0x000000); + case 0x2F: return rgb::from_hex(0x000000); + case 0x30: return rgb::from_hex(0xFFFEFF); + case 0x31: return rgb::from_hex(0xC0DFFF); + case 0x32: return rgb::from_hex(0xD3D2FF); + case 0x33: return rgb::from_hex(0xE8C8FF); + case 0x34: return rgb::from_hex(0xFBC2FF); + case 0x35: return rgb::from_hex(0xFEC4EA); + case 0x36: return rgb::from_hex(0xFECCC5); + case 0x37: return rgb::from_hex(0xF7D8A5); + case 0x38: return rgb::from_hex(0xE4E594); + case 0x39: return rgb::from_hex(0xCFEF96); + case 0x3A: return rgb::from_hex(0xBDF4AB); + case 0x3B: return rgb::from_hex(0xB3F3CC); + case 0x3C: return rgb::from_hex(0xB5EBF2); + case 0x3D: return rgb::from_hex(0xB8B8B8); + case 0x3E: return rgb::from_hex(0x000000); + case 0x3F: return rgb::from_hex(0x000000); + default: return rgb::from_hex(0x000000); + } + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/ppu.hh b/lib/nes/ppu.hh new file mode 100644 index 0000000..d7d9baa --- /dev/null +++ b/lib/nes/ppu.hh @@ -0,0 +1,208 @@ +#pragma once + +#include "nes/util/cycle-count.hh" + +namespace nes +{ + class address; + class cpu; + class mapper; + class display; + class rgb; + struct snapshot; + + class ppu + { + static constexpr auto vram_size = std::size_t{ 0x800 }; + static constexpr auto oam_size = std::size_t{ 0x100 }; + static constexpr auto palette_buffer_size = std::size_t{ 0x20 }; + static constexpr auto status_mask = std::uint8_t{ 0b11100000 }; + // Writes to some registers are ignored until this clock cycle. + static constexpr auto boot_up_cycles = cycle_count::from_ppu(29658); + + enum class vram_increment : unsigned + { + add_1_across = 0, + add_32_down = 1, + }; + + enum class sprite_size : unsigned + { + _8x8 = 0, + _8x16 = 1, + }; + + enum class role : unsigned + { + background = 0, + sprite = 1, + }; + + enum class color : std::uint8_t + { + }; + + union color_index + { + struct __attribute__((packed)) + { + unsigned color : 2; + unsigned palette : 2; + role role : 1; + }; + std::uint8_t value; + + explicit color_index(std::uint8_t const value) + : value{ value } + { + } + }; + + struct sprite + { + std::uint32_t pattern{ 0 }; + std::uint8_t position{ 0 }; + bool is_in_front{ false }; + bool is_sprite_zero{ false }; + }; + + cycle_count current_cycles_; + cpu& cpu_; + mapper& mapper_; + display& display_; + std::uint8_t vram_[vram_size]{}; + std::uint8_t oam_[oam_size]{}; + color palette_buffer_[palette_buffer_size] + { + color{ 0x09 }, color{ 0x01 }, color{ 0x00 }, color{ 0x01 }, + color{ 0x00 }, color{ 0x02 }, color{ 0x02 }, color{ 0x0D }, + color{ 0x08 }, color{ 0x10 }, color{ 0x08 }, color{ 0x24 }, + color{ 0x00 }, color{ 0x00 }, color{ 0x04 }, color{ 0x2C }, + color{ 0x09 }, color{ 0x01 }, color{ 0x34 }, color{ 0x03 }, + color{ 0x00 }, color{ 0x04 }, color{ 0x00 }, color{ 0x14 }, + color{ 0x08 }, color{ 0x3A }, color{ 0x00 }, color{ 0x02 }, + color{ 0x00 }, color{ 0x20 }, color{ 0x2C }, color{ 0x08 }, + }; + std::uint8_t latch_{}; // The last read/written IO register value, returned when reading write-only registers. + union + { + struct __attribute__((packed)) + { + bool : 1; + bool : 1; + bool : 1; + bool : 1; + bool : 1; + bool sprite_overflow : 1; + bool sprite_zero_hit : 1; + bool vblank : 1; + }; + std::uint8_t value{ 0b00000000 }; + } status_{}; + union + { + struct __attribute__((packed)) + { + std::uint8_t base_name_table : 2; // Base nametable address. + vram_increment vram_increment : 1; // VRAM increment per CPU read/write of PPUDATA. + std::uint8_t sprite_pattern_table : 1; // Sprite pattern table address for 8x8 sprites. + std::uint8_t background_pattern_table : 1; // Background pattern table address. + sprite_size sprite_size : 1; // Sprite size. + bool enable_ext_pin : 1; // PPU master/slave select. + bool vblank_nmi : 1; // Enable/disable Vblank NMI + }; + std::uint8_t value{ 0b00000000 }; + } control_{}; + union + { + struct __attribute__((packed)) + { + bool grayscale : 1; // Enable/disable grayscale. + bool show_background_start : 1; // Show background in leftmost 8 pixels of screen. + bool show_sprites_start : 1; // Show sprites in leftmost 8 pixels of screen. + bool enable_background : 1; // Enable background rendering. + bool enable_sprites : 1; // Enable sprite rendering. + bool emphasize_red : 1; // Emphasize red. + bool emphasize_green : 1; // Emphasize green. + bool emphasize_blue : 1; // Emphasize blue. + }; + std::uint8_t value{ 0b00000000 }; + } mask_{}; + struct + { + // See https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers + std::uint16_t v{}; // Current VRAM address (15 bits) + std::uint16_t t{}; // Temporary VRAM address (15 bits) + std::uint8_t x{}; // Fine X scroll (3 bits) + bool w{}; // Second write toggle (1 bit) + } internal_{}; + std::uint8_t oamaddr_{}; // Address accessed by IO registers + std::uint8_t ppudata_read_buffer_{}; // Delays PPUDATA reads by one. + unsigned scanline_{ 240 }; + unsigned scanline_cycle_{ 340 }; + bool even_frame_{ true }; + std::uint64_t tile_data_{ 0 }; // Data for the current tile (higher 32 bits) and the next tile (lower 32 bits). + std::uint8_t name_table_byte_{ 0 }; + std::uint8_t attribute_table_byte_{ 0 }; + std::uint8_t low_tile_byte_{ 0 }; + std::uint8_t high_tile_byte_{ 0 }; + sprite sprites_[8]{}; // Evaluated sprites. + unsigned sprite_count_{ 0 }; // Number of evaluated sprites in sprites_. + + public: + explicit ppu(cpu&, mapper&, display&); + + ppu(ppu const&) = delete; + ppu(ppu&&) = delete; + auto operator=(ppu const&) -> ppu& = delete; + auto operator=(ppu&&) -> ppu& = delete; + + auto get_cycles() const -> cycle_count { return current_cycles_; } + auto build_snapshot(snapshot&) -> void; + auto step() -> void; + + // Memory access + + auto read8(address) -> std::uint8_t; + auto write8(address, std::uint8_t) -> void; + + // IO registers + + auto read_latch() -> std::uint8_t; + auto read_ppustatus() -> std::uint8_t; + auto read_oamdata() -> std::uint8_t; + auto read_ppudata() -> std::uint8_t; + + auto write_latch(std::uint8_t) -> void; + auto write_ppuctrl(std::uint8_t) -> void; + auto write_ppuscroll(std::uint8_t) -> void; + auto write_ppumask(std::uint8_t) -> void; + auto write_ppuaddr(std::uint8_t) -> void; + auto write_ppudata(std::uint8_t) -> void; + auto write_oamaddr(std::uint8_t) -> void; + auto write_oamdata(std::uint8_t) -> void; + auto write_oamdma(std::uint8_t) -> void; + + private: + auto render_pixel() -> void; + auto fetch_name_table_byte() -> void; + auto fetch_attribute_table_byte() -> void; + auto fetch_low_tile_byte() -> void; + auto fetch_high_tile_byte() -> void; + auto store_tile_data() -> void; + auto increment_x() -> void; + auto increment_y() -> void; + auto copy_x() -> void; + auto copy_y() -> void; + auto evaluate_sprites() -> void; + auto fetch_sprite_pattern(unsigned i, unsigned row) -> std::uint32_t; + + // Helpers + + auto nmi_change() -> void; + auto sprite_height() -> unsigned; + auto increment_vram() -> void; + auto get_color(color_index) -> color&; + auto resolve_color(color) -> rgb; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/ref/CMakeLists.txt b/lib/nes/ref/CMakeLists.txt new file mode 100644 index 0000000..ab8e74d --- /dev/null +++ b/lib/nes/ref/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources( + nes_ref + PRIVATE + test.cc)
\ No newline at end of file diff --git a/lib/nes/ref/test.cc b/lib/nes/ref/test.cc new file mode 100644 index 0000000..210ece0 --- /dev/null +++ b/lib/nes/ref/test.cc @@ -0,0 +1,3 @@ +// +// Created by Hannes Schulze on 23.01.25. +// diff --git a/lib/nes/util/CMakeLists.txt b/lib/nes/util/CMakeLists.txt new file mode 100644 index 0000000..0fdb0ba --- /dev/null +++ b/lib/nes/util/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources( + nes + PRIVATE + address.hh + button-mask.hh + cycle-count.hh + debug.hh + rgb.hh + snapshot.hh)
\ No newline at end of file diff --git a/lib/nes/util/address.hh b/lib/nes/util/address.hh new file mode 100644 index 0000000..7987d07 --- /dev/null +++ b/lib/nes/util/address.hh @@ -0,0 +1,73 @@ +#pragma once + +#include <cstdint> + +namespace nes +{ + /// An address on the NES. + class address + { + std::uint16_t absolute_{ 0 }; + + public: + /// Create a zero address. + constexpr explicit address() = default; + + /// Create an address from its absolute value. + constexpr explicit address(std::uint16_t const absolute) + : absolute_{ absolute } + { + } + + /// Create an address from its page and offset combination. + constexpr explicit address(std::uint8_t const page, std::uint8_t const offset) + : address{ static_cast<std::uint16_t>((page << 8) | (offset << 0)) } + { + } + + /// Get the absolute address value. + constexpr auto get_absolute() const -> std::uint16_t + { + return absolute_; + } + + /// Get the address page (the first 8 bits). + constexpr auto get_page() const -> std::uint8_t + { + return static_cast<std::uint8_t>((get_absolute() & 0xFF00) >> 8); + } + + /// Get the offset within the page (the last 8 bits). + constexpr auto get_offset() const -> std::uint8_t + { + return static_cast<std::uint8_t>((get_absolute() & 0x00FF) >> 0); + } + }; + + constexpr auto operator==(address const a, address const b) -> bool { return a.get_absolute() == b.get_absolute(); } + constexpr auto operator!=(address const a, address const b) -> bool { return a.get_absolute() != b.get_absolute(); } + constexpr auto operator<(address const a, address const b) -> bool { return a.get_absolute() < b.get_absolute(); } + constexpr auto operator>(address const a, address const b) -> bool { return a.get_absolute() > b.get_absolute(); } + constexpr auto operator<=(address const a, address const b) -> bool { return a.get_absolute() <= b.get_absolute(); } + constexpr auto operator>=(address const a, address const b) -> bool { return a.get_absolute() >= b.get_absolute(); } + + constexpr auto operator+(address const a, std::uint16_t const b) -> address + { + return address{ static_cast<std::uint16_t>(a.get_absolute() + b) }; + } + + constexpr auto operator+(std::uint16_t const a, address const b) -> address + { + return address{ static_cast<std::uint16_t>(a + b.get_absolute()) }; + } + + constexpr auto operator-(address const a, std::uint16_t const b) -> address + { + return address{ static_cast<std::uint16_t>(a.get_absolute() - b) }; + } + + constexpr auto operator%(address const a, std::uint16_t const b) -> address + { + return address{ static_cast<std::uint16_t>(a.get_absolute() % b) }; + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/util/button-mask.hh b/lib/nes/util/button-mask.hh new file mode 100644 index 0000000..b010b9e --- /dev/null +++ b/lib/nes/util/button-mask.hh @@ -0,0 +1,43 @@ +#pragma once + +#include <cstdint> + +namespace nes +{ + class button_mask + { + std::uint8_t raw_value_{ 0 }; + + public: + constexpr explicit button_mask() = default; + + static constexpr auto from_raw_value(std::uint8_t const raw_value) -> button_mask + { + return button_mask{ raw_value }; + } + + constexpr auto add(button_mask const other) -> void { raw_value_ |= other.raw_value_; } + constexpr auto remove(button_mask const other) -> void { raw_value_ ^= other.raw_value_; } + constexpr auto contains(button_mask const other) const -> bool { return (raw_value_ & other.raw_value_) == other.raw_value_; } + constexpr auto is_empty() const -> bool { return raw_value_ == 0; } + constexpr auto get_raw_value() const -> std::uint8_t { return raw_value_; } + + private: + constexpr explicit button_mask(std::uint8_t const raw_value) + : raw_value_{ raw_value } + { + } + }; + + namespace buttons + { + static constexpr auto a = button_mask::from_raw_value(1 << 0); + static constexpr auto b = button_mask::from_raw_value(1 << 1); + static constexpr auto select = button_mask::from_raw_value(1 << 2); + static constexpr auto start = button_mask::from_raw_value(1 << 3); + static constexpr auto up = button_mask::from_raw_value(1 << 4); + static constexpr auto down = button_mask::from_raw_value(1 << 5); + static constexpr auto left = button_mask::from_raw_value(1 << 6); + static constexpr auto right = button_mask::from_raw_value(1 << 7); + } // namespace buttons +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/util/cycle-count.hh b/lib/nes/util/cycle-count.hh new file mode 100644 index 0000000..f1a4a7e --- /dev/null +++ b/lib/nes/util/cycle-count.hh @@ -0,0 +1,87 @@ +#pragma once + +#include <chrono> + +namespace nes +{ + /// A cycle count that can be used for multiple components. + /// + /// Each component can have a different clock frequency and this structure offers conversions between different + /// cycle counts. + class cycle_count + { + friend constexpr auto operator+(cycle_count, cycle_count) -> cycle_count; + friend constexpr auto operator-(cycle_count, cycle_count) -> cycle_count; + friend constexpr auto operator*(std::uint64_t, cycle_count) -> cycle_count; + friend constexpr auto operator*(cycle_count, std::uint64_t) -> cycle_count; + friend constexpr auto operator/(cycle_count, cycle_count) -> std::uint64_t; + + std::uint64_t units_{ 0 }; // The most fine-granular cycle count (here: PPU cycles). + + static constexpr auto ppu_cycle() -> cycle_count { return cycle_count{ 1 }; } + static constexpr auto cpu_cycle() -> cycle_count { return cycle_count{ 3 }; } + static constexpr auto second() -> cycle_count { return 1789773 * cpu_cycle(); } + + public: + constexpr explicit cycle_count() = default; + + constexpr auto operator+=(cycle_count const other) -> cycle_count& { return *this = *this + other; } + constexpr auto operator*=(std::uint64_t const other) -> cycle_count& { return *this = *this * other; } + + static constexpr auto from_units(std::uint64_t const units) -> cycle_count + { + return cycle_count{ units }; + } + + static constexpr auto from_duration(std::chrono::microseconds const duration) -> cycle_count + { + return from_units(static_cast<std::uint64_t>(duration.count()) * second().get_units() / (1000 * 1000)); + } + + static constexpr auto from_cpu(std::uint64_t const cpu_cycles) -> cycle_count + { + return cpu_cycles * cpu_cycle(); + } + + static constexpr auto from_ppu(std::uint64_t const ppu_cycles) -> cycle_count + { + return ppu_cycles * ppu_cycle(); + } + + /// Value in implementation-defined units. + constexpr auto get_units() const -> std::uint64_t { return units_; } + + constexpr auto to_duration() const -> std::chrono::microseconds + { + return std::chrono::microseconds{ get_units() * 1000 * 1000 / second().get_units() }; + } + + constexpr auto to_cpu() const -> std::uint64_t + { + return *this / cpu_cycle(); + } + + constexpr auto to_ppu() const -> std::uint64_t + { + return *this / ppu_cycle(); + } + + private: + constexpr explicit cycle_count(std::uint64_t const units) + : units_{ units } + { + } + }; + + constexpr auto operator==(cycle_count const a, cycle_count const b) -> bool { return a.get_units() == b.get_units(); } + constexpr auto operator!=(cycle_count const a, cycle_count const b) -> bool { return a.get_units() != b.get_units(); } + constexpr auto operator<(cycle_count const a, cycle_count const b) -> bool { return a.get_units() < b.get_units(); } + constexpr auto operator>(cycle_count const a, cycle_count const b) -> bool { return a.get_units() > b.get_units(); } + constexpr auto operator<=(cycle_count const a, cycle_count const b) -> bool { return a.get_units() <= b.get_units(); } + constexpr auto operator>=(cycle_count const a, cycle_count const b) -> bool { return a.get_units() >= b.get_units(); } + constexpr auto operator+(cycle_count const a, cycle_count const b) -> cycle_count { return cycle_count{ a.get_units() + b.get_units() }; } + constexpr auto operator-(cycle_count const a, cycle_count const b) -> cycle_count { return cycle_count{ a.get_units() - b.get_units() }; } + constexpr auto operator*(std::uint64_t const a, cycle_count const b) -> cycle_count { return cycle_count{ a * b.get_units() }; } + constexpr auto operator*(cycle_count const a, std::uint64_t const b) -> cycle_count { return cycle_count{ a.get_units() * b }; } + constexpr auto operator/(cycle_count const a, cycle_count const b) -> std::uint64_t { return a.get_units() / b.get_units(); } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/util/debug.hh b/lib/nes/util/debug.hh new file mode 100644 index 0000000..409d55a --- /dev/null +++ b/lib/nes/util/debug.hh @@ -0,0 +1,15 @@ +#pragma once + +#ifdef NES_ENABLE_DEBUG_OUTPUT + +#include <spdlog/spdlog.h> + +#define NES_DEBUG_LOG(name, ...) \ + spdlog::info("[" #name "] " __VA_ARGS__) + +#else + +#define NES_DEBUG_LOG(name, fmt, ...) \ + (void)0 + +#endif
\ No newline at end of file diff --git a/lib/nes/util/rgb.hh b/lib/nes/util/rgb.hh new file mode 100644 index 0000000..d62c70c --- /dev/null +++ b/lib/nes/util/rgb.hh @@ -0,0 +1,33 @@ +#pragma once + +#include <cstdint> + +namespace nes +{ + /// An RGB value. + class rgb + { + public: + explicit rgb() = default; + + explicit rgb(std::uint8_t const r, std::uint8_t const g, std::uint8_t const b) + : r{ r } + , g{ g } + , b{ b } + { + } + + static auto from_hex(std::uint32_t const hex) -> rgb + { + auto res = rgb{}; + res.r = (hex & 0x00FF0000) >> 16; + res.g = (hex & 0x0000FF00) >> 8; + res.b = (hex & 0x000000FF) >> 0; + return res; + } + + std::uint8_t r{ 0 }; + std::uint8_t g{ 0 }; + std::uint8_t b{ 0 }; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/util/snapshot.hh b/lib/nes/util/snapshot.hh new file mode 100644 index 0000000..de23a46 --- /dev/null +++ b/lib/nes/util/snapshot.hh @@ -0,0 +1,63 @@ +#pragma once + +#include "nes/util/cycle-count.hh" +#include <vector> +#include <cstdint> + +namespace nes +{ + /// System snapshot used for comparison with a reference implementation. + struct snapshot + { + explicit snapshot() = default; + + cycle_count cpu_cycle; + cycle_count ppu_cycle; + std::vector<std::uint8_t> ram; + std::vector<std::uint8_t> sram; + std::vector<std::uint8_t> vram; + std::vector<std::uint8_t> oam; + struct + { + std::uint16_t pc{}; + std::uint8_t sp{}; + std::uint8_t a{}; + std::uint8_t x{}; + std::uint8_t y{}; + std::uint8_t p{}; + bool c{}; + bool z{}; + bool i{}; + bool d{}; + bool b{}; + bool v{}; + bool n{}; + } registers{}; + }; + + inline auto operator==(snapshot const& a, snapshot const& b) -> bool + { + if (a.cpu_cycle != b.cpu_cycle) { return false; } + if (a.ppu_cycle != b.ppu_cycle) { return false; } + if (a.ram != b.ram) { return false; } + if (a.sram != b.sram) { return false; } + if (a.vram != b.vram) { return false; } + if (a.oam != b.oam) { return false; } + if (a.registers.pc != b.registers.pc) { return false; } + if (a.registers.sp != b.registers.sp) { return false; } + if (a.registers.a != b.registers.a) { return false; } + if (a.registers.x != b.registers.x) { return false; } + if (a.registers.y != b.registers.y) { return false; } + if (a.registers.p != b.registers.p) { return false; } + if (a.registers.c != b.registers.c) { return false; } + if (a.registers.z != b.registers.z) { return false; } + if (a.registers.i != b.registers.i) { return false; } + if (a.registers.d != b.registers.d) { return false; } + if (a.registers.b != b.registers.b) { return false; } + if (a.registers.v != b.registers.v) { return false; } + if (a.registers.n != b.registers.n) { return false; } + return true; + } + + inline auto operator!=(snapshot const& a, snapshot const& b) -> bool { return !(a == b); } +} // namespace nes
\ No newline at end of file |
