diff options
Diffstat (limited to 'lib/nes/sys')
| -rw-r--r-- | lib/nes/sys/CMakeLists.txt | 17 | ||||
| -rw-r--r-- | lib/nes/sys/cartridge.cc | 68 | ||||
| -rw-r--r-- | lib/nes/sys/cartridge.hh | 78 | ||||
| -rw-r--r-- | lib/nes/sys/controller.cc | 18 | ||||
| -rw-r--r-- | lib/nes/sys/controller.hh | 30 | ||||
| -rw-r--r-- | lib/nes/sys/cpu.cc | 1425 | ||||
| -rw-r--r-- | lib/nes/sys/cpu.hh | 300 | ||||
| -rw-r--r-- | lib/nes/sys/mapper.cc | 172 | ||||
| -rw-r--r-- | lib/nes/sys/mapper.hh | 32 | ||||
| -rw-r--r-- | lib/nes/sys/nes.cc | 65 | ||||
| -rw-r--r-- | lib/nes/sys/nes.hh | 47 | ||||
| -rw-r--r-- | lib/nes/sys/ppu.cc | 697 | ||||
| -rw-r--r-- | lib/nes/sys/ppu.hh | 279 | ||||
| -rw-r--r-- | lib/nes/sys/types/CMakeLists.txt | 8 | ||||
| -rw-r--r-- | lib/nes/sys/types/address.hh | 74 | ||||
| -rw-r--r-- | lib/nes/sys/types/button-mask.hh | 44 | ||||
| -rw-r--r-- | lib/nes/sys/types/cycle-count.hh | 87 | ||||
| -rw-r--r-- | lib/nes/sys/types/snapshot.hh | 63 | ||||
| -rw-r--r-- | lib/nes/sys/types/status.hh | 36 |
19 files changed, 3540 insertions, 0 deletions
diff --git a/lib/nes/sys/CMakeLists.txt b/lib/nes/sys/CMakeLists.txt new file mode 100644 index 0000000..60f77d2 --- /dev/null +++ b/lib/nes/sys/CMakeLists.txt @@ -0,0 +1,17 @@ +target_sources( + nes + PRIVATE + cartridge.hh + cartridge.cc + controller.hh + controller.cc + cpu.hh + cpu.cc + ppu.hh + ppu.cc + mapper.hh + mapper.cc + nes.hh + nes.cc) + +add_subdirectory(types)
\ No newline at end of file diff --git a/lib/nes/sys/cartridge.cc b/lib/nes/sys/cartridge.cc new file mode 100644 index 0000000..d60ea8b --- /dev/null +++ b/lib/nes/sys/cartridge.cc @@ -0,0 +1,68 @@ +#include "nes/sys/cartridge.hh" +#include "nes/sys/types/status.hh" +#include <fstream> + +namespace nes +{ + cartridge::cartridge(status const status) + : status_{ status } + { + } + + 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{ status::success }; + + // Parse header. + if (length < header_length) { return cartridge{ status::error_invalid_ines_data }; } + auto const h = header{ bytes }; + if (h.get_magic_0() != 'N' || h.get_magic_1() != 'E' || h.get_magic_2() != 'S' || h.get_magic_3() != 0x1A) + { + return cartridge{ status::error_invalid_ines_data }; + } + auto const mapper_number = static_cast<std::uint8_t>((h.get_mapper_high() << 4) | (h.get_mapper_low() << 0)); + + res.mapper_ = &mapper::get(mapper_number); + res.name_table_arrangement_ = h.get_name_table_arrangement(); + + // Load program data. + auto offset = std::size_t{ 16 }; + if (h.get_has_trainer()) { offset += 512; } + + auto const prg_rom_size = h.get_prg_rom_banks() * prg_rom_bank_size; + if (length < offset + prg_rom_size) { return cartridge{ status::error_invalid_ines_data }; } + res.prg_rom_ = std::vector(&bytes[offset], &bytes[offset + prg_rom_size]); + + offset += prg_rom_size; + + auto const chr_rom_size = h.get_chr_rom_banks() * chr_rom_bank_size; + if (length < offset + chr_rom_size) { return cartridge{ status::error_invalid_ines_data }; } + res.chr_rom_ = std::vector(&bytes[offset], &bytes[offset + chr_rom_size]); + + res.ram_ = std::vector(std::max(h.get_ram_banks(), 1u) * ram_bank_size, std::uint8_t{ 0 }); + + if (auto const status = res.get_mapper().validate(res); status != status::success) + { + return cartridge{ status }; + } + + 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{ status::error_system_error }; } + + stream.seekg(0, std::ios::end); + auto const length = stream.tellg(); + stream.seekg(0, std::ios::beg); + auto data = std::vector<std::uint8_t>(static_cast<std::size_t>(length)); + stream.read(reinterpret_cast<char*>(data.data()), length); + + return from_data(data.data(), data.size()); + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/sys/cartridge.hh b/lib/nes/sys/cartridge.hh new file mode 100644 index 0000000..a8b61a2 --- /dev/null +++ b/lib/nes/sys/cartridge.hh @@ -0,0 +1,78 @@ +#pragma once + +#include "nes/sys/types/status.hh" +#include "nes/sys/mapper.hh" +#include <cstddef> +#include <vector> +#include <string_view> + +namespace nes +{ + enum class name_table_arrangement + { + horizontal, + vertical, + }; + + class cartridge + { + static constexpr auto prg_rom_bank_size = std::size_t{ 16 * 1024 }; + static constexpr auto chr_rom_bank_size = std::size_t{ 8 * 1024 }; + static constexpr auto ram_bank_size = std::size_t{ 8 * 1024 }; + static constexpr auto header_length = std::size_t{ 16 }; + + struct header + { + auto get_magic_0() const -> std::uint8_t { return value[0]; } + auto get_magic_1() const -> std::uint8_t { return value[1]; } + auto get_magic_2() const -> std::uint8_t { return value[2]; } + auto get_magic_3() const -> std::uint8_t { return value[3]; } + auto get_prg_rom_banks() const -> unsigned { return value[4]; } + auto get_chr_rom_banks() const -> unsigned { return value[5]; } + auto get_control_1() const -> std::uint8_t { return value[6]; } + auto get_control_2() const -> std::uint8_t { return value[7]; } + auto get_ram_banks() const -> unsigned { return value[8]; } + auto get_has_trainer() const -> bool { return get_control_1() & 0b00000100; } + auto get_mapper_low() const -> unsigned { return (get_control_1() & 0b11110000) >> 4; } + auto get_mapper_high() const -> unsigned { return (get_control_2() & 0b11110000) >> 0; } + auto get_name_table_arrangement() const -> name_table_arrangement { return static_cast<name_table_arrangement>((get_control_1() & 0b00000001) >> 0); } + + std::uint8_t value[header_length]{}; + + explicit header(std::uint8_t const* v) + { + for (auto i = std::size_t{ 0 }; i < header_length; ++i) + { + value[i] = v[i]; + } + } + }; + + status status_{ status::error_uninitialized }; + std::vector<std::uint8_t> prg_rom_; + std::vector<std::uint8_t> chr_rom_; + std::vector<std::uint8_t> ram_; + name_table_arrangement name_table_arrangement_{}; + mapper* mapper_{ &mapper::invalid() }; + + 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 get_status() const -> status { return status_; } + 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() const -> mapper& { return *mapper_; } + auto get_name_table_arrangement() const -> name_table_arrangement { return name_table_arrangement_; } + + private: + explicit cartridge(status); + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/sys/controller.cc b/lib/nes/sys/controller.cc new file mode 100644 index 0000000..910ef57 --- /dev/null +++ b/lib/nes/sys/controller.cc @@ -0,0 +1,18 @@ +#include "nes/sys/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/sys/controller.hh b/lib/nes/sys/controller.hh new file mode 100644 index 0000000..441e236 --- /dev/null +++ b/lib/nes/sys/controller.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "nes/sys/types/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 ref_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/sys/cpu.cc b/lib/nes/sys/cpu.cc new file mode 100644 index 0000000..84fbe78 --- /dev/null +++ b/lib/nes/sys/cpu.cc @@ -0,0 +1,1425 @@ +#include "nes/sys/cpu.hh" +#include "nes/sys/ppu.hh" +#include "nes/sys/controller.hh" +#include "nes/sys/cartridge.hh" +#include "nes/sys/types/snapshot.hh" + +namespace nes +{ + cpu::cpu(ppu& ppu, cartridge& cartridge, controller& controller_1, controller& controller_2) + : ppu_{ ppu } + , cartridge_{ cartridge } + , 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.value; + snapshot.registers.c = registers_.p.get_c(); + snapshot.registers.z = registers_.p.get_z(); + snapshot.registers.i = registers_.p.get_i(); + snapshot.registers.d = registers_.p.get_d(); + snapshot.registers.b = registers_.p.get_b(); + snapshot.registers.v = registers_.p.get_v(); + snapshot.registers.n = registers_.p.get_n(); + } + + auto cpu::stall_cycles(cycle_count const count) -> void + { + current_cycles_ += count; + } + + auto cpu::trigger_nmi() -> void + { + nmi_pending_ = true; + } + + auto cpu::step() -> status + { + // 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: return run_brk<addressing_mode::immediate>(); + case 0x01: return run_ora<addressing_mode::indexed_indirect>(); + case 0x02: return run_stp(); + case 0x03: return run_slo<addressing_mode::indexed_indirect>(); + case 0x04: return run_nop<addressing_mode::zero_page>(); + case 0x05: return run_ora<addressing_mode::zero_page>(); + case 0x06: return run_asl<addressing_mode::zero_page>(); + case 0x07: return run_slo<addressing_mode::zero_page>(); + case 0x08: return run_php(); + case 0x09: return run_ora<addressing_mode::immediate>(); + case 0x0A: return run_asl<addressing_mode::accumulator>(); + case 0x0B: return run_anc<addressing_mode::immediate>(); + case 0x0C: return run_nop<addressing_mode::absolute>(); + case 0x0D: return run_ora<addressing_mode::absolute>(); + case 0x0E: return run_asl<addressing_mode::absolute>(); + case 0x0F: return run_slo<addressing_mode::absolute>(); + case 0x10: return run_bpl<addressing_mode::relative>(); + case 0x11: return run_ora<addressing_mode::indirect_indexed>(); + case 0x12: return run_stp(); + case 0x13: return run_slo<addressing_mode::indirect_indexed>(); + case 0x14: return run_nop<addressing_mode::zero_page_indexed_x>(); + case 0x15: return run_ora<addressing_mode::zero_page_indexed_x>(); + case 0x16: return run_asl<addressing_mode::zero_page_indexed_x>(); + case 0x17: return run_slo<addressing_mode::zero_page_indexed_x>(); + case 0x18: return run_clc(); + case 0x19: return run_ora<addressing_mode::absolute_indexed_y>(); + case 0x1A: return run_nop(); + case 0x1B: return run_slo<addressing_mode::absolute_indexed_y>(); + case 0x1C: return run_nop<addressing_mode::absolute_indexed_x>(); + case 0x1D: return run_ora<addressing_mode::absolute_indexed_x>(); + case 0x1E: return run_asl<addressing_mode::absolute_indexed_x>(); + case 0x1F: return run_slo<addressing_mode::absolute_indexed_x>(); + case 0x20: return run_jsr<addressing_mode::absolute>(); + case 0x21: return run_and<addressing_mode::indexed_indirect>(); + case 0x22: return run_stp(); + case 0x23: return run_rla<addressing_mode::indexed_indirect>(); + case 0x24: return run_bit<addressing_mode::zero_page>(); + case 0x25: return run_and<addressing_mode::zero_page>(); + case 0x26: return run_rol<addressing_mode::zero_page>(); + case 0x27: return run_rla<addressing_mode::zero_page>(); + case 0x28: return run_plp(); + case 0x29: return run_and<addressing_mode::immediate>(); + case 0x2A: return run_rol<addressing_mode::accumulator>(); + case 0x2B: return run_anc<addressing_mode::immediate>(); + case 0x2C: return run_bit<addressing_mode::absolute>(); + case 0x2D: return run_and<addressing_mode::absolute>(); + case 0x2E: return run_rol<addressing_mode::absolute>(); + case 0x2F: return run_rla<addressing_mode::absolute>(); + case 0x30: return run_bmi<addressing_mode::relative>(); + case 0x31: return run_and<addressing_mode::indirect_indexed>(); + case 0x32: return run_stp(); + case 0x33: return run_rla<addressing_mode::indirect_indexed>(); + case 0x34: return run_nop<addressing_mode::zero_page_indexed_x>(); + case 0x35: return run_and<addressing_mode::zero_page_indexed_x>(); + case 0x36: return run_rol<addressing_mode::zero_page_indexed_x>(); + case 0x37: return run_rla<addressing_mode::zero_page_indexed_x>(); + case 0x38: return run_sec(); + case 0x39: return run_and<addressing_mode::absolute_indexed_y>(); + case 0x3A: return run_nop(); + case 0x3B: return run_rla<addressing_mode::absolute_indexed_y>(); + case 0x3C: return run_nop<addressing_mode::absolute_indexed_x>(); + case 0x3D: return run_and<addressing_mode::absolute_indexed_x>(); + case 0x3E: return run_rol<addressing_mode::absolute_indexed_x>(); + case 0x3F: return run_rla<addressing_mode::absolute_indexed_x>(); + case 0x40: return run_rti(); + case 0x41: return run_eor<addressing_mode::indexed_indirect>(); + case 0x42: return run_stp(); + case 0x43: return run_sre<addressing_mode::indexed_indirect>(); + case 0x44: return run_nop<addressing_mode::zero_page>(); + case 0x45: return run_eor<addressing_mode::zero_page>(); + case 0x46: return run_lsr<addressing_mode::zero_page>(); + case 0x47: return run_sre<addressing_mode::zero_page>(); + case 0x48: return run_pha(); + case 0x49: return run_eor<addressing_mode::immediate>(); + case 0x4A: return run_lsr<addressing_mode::accumulator>(); + case 0x4B: return run_alr<addressing_mode::immediate>(); + case 0x4C: return run_jmp<addressing_mode::absolute>(); + case 0x4D: return run_eor<addressing_mode::absolute>(); + case 0x4E: return run_lsr<addressing_mode::absolute>(); + case 0x4F: return run_sre<addressing_mode::absolute>(); + case 0x50: return run_bvc<addressing_mode::relative>(); + case 0x51: return run_eor<addressing_mode::indirect_indexed>(); + case 0x52: return run_stp(); + case 0x53: return run_sre<addressing_mode::indirect_indexed>(); + case 0x54: return run_nop<addressing_mode::zero_page_indexed_x>(); + case 0x55: return run_eor<addressing_mode::zero_page_indexed_x>(); + case 0x56: return run_lsr<addressing_mode::zero_page_indexed_x>(); + case 0x57: return run_sre<addressing_mode::zero_page_indexed_x>(); + case 0x58: return run_cli(); + case 0x59: return run_eor<addressing_mode::absolute_indexed_y>(); + case 0x5A: return run_nop(); + case 0x5B: return run_sre<addressing_mode::absolute_indexed_y>(); + case 0x5C: return run_nop<addressing_mode::absolute_indexed_x>(); + case 0x5D: return run_eor<addressing_mode::absolute_indexed_x>(); + case 0x5E: return run_lsr<addressing_mode::absolute_indexed_x>(); + case 0x5F: return run_sre<addressing_mode::absolute_indexed_x>(); + case 0x60: return run_rts(); + case 0x61: return run_adc<addressing_mode::indexed_indirect>(); + case 0x62: return run_stp(); + case 0x63: return run_rra<addressing_mode::indexed_indirect>(); + case 0x64: return run_nop<addressing_mode::zero_page>(); + case 0x65: return run_adc<addressing_mode::zero_page>(); + case 0x66: return run_ror<addressing_mode::zero_page>(); + case 0x67: return run_rra<addressing_mode::zero_page>(); + case 0x68: return run_pla(); + case 0x69: return run_adc<addressing_mode::immediate>(); + case 0x6A: return run_ror<addressing_mode::accumulator>(); + case 0x6B: return run_arr<addressing_mode::immediate>(); + case 0x6C: return run_jmp<addressing_mode::indirect>(); + case 0x6D: return run_adc<addressing_mode::absolute>(); + case 0x6E: return run_ror<addressing_mode::absolute>(); + case 0x6F: return run_rra<addressing_mode::absolute>(); + case 0x70: return run_bvs<addressing_mode::relative>(); + case 0x71: return run_adc<addressing_mode::indirect_indexed>(); + case 0x72: return run_stp(); + case 0x73: return run_rra<addressing_mode::indirect_indexed>(); + case 0x74: return run_nop<addressing_mode::zero_page_indexed_x>(); + case 0x75: return run_adc<addressing_mode::zero_page_indexed_x>(); + case 0x76: return run_ror<addressing_mode::zero_page_indexed_x>(); + case 0x77: return run_rra<addressing_mode::zero_page_indexed_x>(); + case 0x78: return run_sei(); + case 0x79: return run_adc<addressing_mode::absolute_indexed_y>(); + case 0x7A: return run_nop(); + case 0x7B: return run_rra<addressing_mode::absolute_indexed_y>(); + case 0x7C: return run_nop<addressing_mode::absolute_indexed_x>(); + case 0x7D: return run_adc<addressing_mode::absolute_indexed_x>(); + case 0x7E: return run_ror<addressing_mode::absolute_indexed_x>(); + case 0x7F: return run_rra<addressing_mode::absolute_indexed_x>(); + case 0x80: return run_nop<addressing_mode::immediate>(); + case 0x81: return run_sta<addressing_mode::indexed_indirect>(); + case 0x82: return run_nop<addressing_mode::immediate>(); + case 0x83: return run_sax<addressing_mode::indexed_indirect>(); + case 0x84: return run_sty<addressing_mode::zero_page>(); + case 0x85: return run_sta<addressing_mode::zero_page>(); + case 0x86: return run_stx<addressing_mode::zero_page>(); + case 0x87: return run_sax<addressing_mode::zero_page>(); + case 0x88: return run_dey(); + case 0x89: return run_nop<addressing_mode::immediate>(); + case 0x8A: return run_txa(); + case 0x8B: return run_xaa<addressing_mode::immediate>(); + case 0x8C: return run_sty<addressing_mode::absolute>(); + case 0x8D: return run_sta<addressing_mode::absolute>(); + case 0x8E: return run_stx<addressing_mode::absolute>(); + case 0x8F: return run_sax<addressing_mode::absolute>(); + case 0x90: return run_bcc<addressing_mode::relative>(); + case 0x91: return run_sta<addressing_mode::indirect_indexed>(); + case 0x92: return run_stp(); + case 0x93: return run_ahx<addressing_mode::indirect_indexed>(); + case 0x94: return run_sty<addressing_mode::zero_page_indexed_x>(); + case 0x95: return run_sta<addressing_mode::zero_page_indexed_x>(); + case 0x96: return run_stx<addressing_mode::zero_page_indexed_y>(); + case 0x97: return run_sax<addressing_mode::zero_page_indexed_y>(); + case 0x98: return run_tya(); + case 0x99: return run_sta<addressing_mode::absolute_indexed_y>(); + case 0x9A: return run_txs(); + case 0x9B: return run_tas<addressing_mode::absolute_indexed_y>(); + case 0x9C: return run_shy<addressing_mode::absolute_indexed_x>(); + case 0x9D: return run_sta<addressing_mode::absolute_indexed_x>(); + case 0x9E: return run_shx<addressing_mode::absolute_indexed_y>(); + case 0x9F: return run_ahx<addressing_mode::absolute_indexed_y>(); + case 0xA0: return run_ldy<addressing_mode::immediate>(); + case 0xA1: return run_lda<addressing_mode::indexed_indirect>(); + case 0xA2: return run_ldx<addressing_mode::immediate>(); + case 0xA3: return run_lax<addressing_mode::indexed_indirect>(); + case 0xA4: return run_ldy<addressing_mode::zero_page>(); + case 0xA5: return run_lda<addressing_mode::zero_page>(); + case 0xA6: return run_ldx<addressing_mode::zero_page>(); + case 0xA7: return run_lax<addressing_mode::zero_page>(); + case 0xA8: return run_tay(); + case 0xA9: return run_lda<addressing_mode::immediate>(); + case 0xAA: return run_tax(); + case 0xAB: return run_lax<addressing_mode::immediate>(); + case 0xAC: return run_ldy<addressing_mode::absolute>(); + case 0xAD: return run_lda<addressing_mode::absolute>(); + case 0xAE: return run_ldx<addressing_mode::absolute>(); + case 0xAF: return run_lax<addressing_mode::absolute>(); + case 0xB0: return run_bcs<addressing_mode::relative>(); + case 0xB1: return run_lda<addressing_mode::indirect_indexed>(); + case 0xB2: return run_stp(); + case 0xB3: return run_lax<addressing_mode::indirect_indexed>(); + case 0xB4: return run_ldy<addressing_mode::zero_page_indexed_x>(); + case 0xB5: return run_lda<addressing_mode::zero_page_indexed_x>(); + case 0xB6: return run_ldx<addressing_mode::zero_page_indexed_y>(); + case 0xB7: return run_lax<addressing_mode::zero_page_indexed_y>(); + case 0xB8: return run_clv(); + case 0xB9: return run_lda<addressing_mode::absolute_indexed_y>(); + case 0xBA: return run_tsx(); + case 0xBB: return run_las<addressing_mode::absolute_indexed_y>(); + case 0xBC: return run_ldy<addressing_mode::absolute_indexed_x>(); + case 0xBD: return run_lda<addressing_mode::absolute_indexed_x>(); + case 0xBE: return run_ldx<addressing_mode::absolute_indexed_y>(); + case 0xBF: return run_lax<addressing_mode::absolute_indexed_y>(); + case 0xC0: return run_cpy<addressing_mode::immediate>(); + case 0xC1: return run_cmp<addressing_mode::indexed_indirect>(); + case 0xC2: return run_nop<addressing_mode::immediate>(); + case 0xC3: return run_dcp<addressing_mode::indexed_indirect>(); + case 0xC4: return run_cpy<addressing_mode::zero_page>(); + case 0xC5: return run_cmp<addressing_mode::zero_page>(); + case 0xC6: return run_dec<addressing_mode::zero_page>(); + case 0xC7: return run_dcp<addressing_mode::zero_page>(); + case 0xC8: return run_iny(); + case 0xC9: return run_cmp<addressing_mode::immediate>(); + case 0xCA: return run_dex(); + case 0xCB: return run_axs<addressing_mode::immediate>(); + case 0xCC: return run_cpy<addressing_mode::absolute>(); + case 0xCD: return run_cmp<addressing_mode::absolute>(); + case 0xCE: return run_dec<addressing_mode::absolute>(); + case 0xCF: return run_dcp<addressing_mode::absolute>(); + case 0xD0: return run_bne<addressing_mode::relative>(); + case 0xD1: return run_cmp<addressing_mode::indirect_indexed>(); + case 0xD2: return run_stp(); + case 0xD3: return run_dcp<addressing_mode::indirect_indexed>(); + case 0xD4: return run_nop<addressing_mode::zero_page_indexed_x>(); + case 0xD5: return run_cmp<addressing_mode::zero_page_indexed_x>(); + case 0xD6: return run_dec<addressing_mode::zero_page_indexed_x>(); + case 0xD7: return run_dcp<addressing_mode::zero_page_indexed_x>(); + case 0xD8: return run_cld(); + case 0xD9: return run_cmp<addressing_mode::absolute_indexed_y>(); + case 0xDA: return run_nop(); + case 0xDB: return run_dcp<addressing_mode::absolute_indexed_y>(); + case 0xDC: return run_nop<addressing_mode::absolute_indexed_x>(); + case 0xDD: return run_cmp<addressing_mode::absolute_indexed_x>(); + case 0xDE: return run_dec<addressing_mode::absolute_indexed_x>(); + case 0xDF: return run_dcp<addressing_mode::absolute_indexed_x>(); + case 0xE0: return run_cpx<addressing_mode::immediate>(); + case 0xE1: return run_sbc<addressing_mode::indexed_indirect>(); + case 0xE2: return run_nop<addressing_mode::immediate>(); + case 0xE3: return run_isc<addressing_mode::indexed_indirect>(); + case 0xE4: return run_cpx<addressing_mode::zero_page>(); + case 0xE5: return run_sbc<addressing_mode::zero_page>(); + case 0xE6: return run_inc<addressing_mode::zero_page>(); + case 0xE7: return run_isc<addressing_mode::zero_page>(); + case 0xE8: return run_inx(); + case 0xE9: return run_sbc<addressing_mode::immediate>(); + case 0xEA: return run_nop(); + case 0xEB: return run_sbc<addressing_mode::immediate>(); + case 0xEC: return run_cpx<addressing_mode::absolute>(); + case 0xED: return run_sbc<addressing_mode::absolute>(); + case 0xEE: return run_inc<addressing_mode::absolute>(); + case 0xEF: return run_isc<addressing_mode::absolute>(); + case 0xF0: return run_beq<addressing_mode::relative>(); + case 0xF1: return run_sbc<addressing_mode::indirect_indexed>(); + case 0xF2: return run_stp(); + case 0xF3: return run_isc<addressing_mode::indirect_indexed>(); + case 0xF4: return run_nop<addressing_mode::zero_page_indexed_x>(); + case 0xF5: return run_sbc<addressing_mode::zero_page_indexed_x>(); + case 0xF6: return run_inc<addressing_mode::zero_page_indexed_x>(); + case 0xF7: return run_isc<addressing_mode::zero_page_indexed_x>(); + case 0xF8: return run_sed(); + case 0xF9: return run_sbc<addressing_mode::absolute_indexed_y>(); + case 0xFA: return run_nop(); + case 0xFB: return run_isc<addressing_mode::absolute_indexed_y>(); + case 0xFC: return run_nop<addressing_mode::absolute_indexed_x>(); + case 0xFD: return run_sbc<addressing_mode::absolute_indexed_x>(); + case 0xFE: return run_inc<addressing_mode::absolute_indexed_x>(); + case 0xFF: return run_isc<addressing_mode::absolute_indexed_x>(); + default: return status::error_unsupported_instruction; + } + } + + // ----------------------------------------------------------------------------------------------------------------- + // Instructions + // ----------------------------------------------------------------------------------------------------------------- + + template<cpu::addressing_mode Mode> + auto cpu::run_brk() -> status + { + // BRK: Break + fetch_operand<Mode>(); + execute_interrupt(address{ 0xFFFE }); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ora() -> status + { + // ORA: Logical Inclusive OR + auto operand = fetch_operand<Mode>(); + eval_ora(operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + auto cpu::run_stp() -> status + { + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_slo() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_nop() -> status + { + // NOP: NOP + auto operand = fetch_operand<Mode>(); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + auto cpu::run_nop() -> status + { + // NOP: NOP + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_php() -> status + { + // PHP: Push Processor Status + eval_php(); + current_cycles_ += cycle_count::from_cpu(3); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_asl() -> status + { + // ASL: Arithmetic Shift Left + auto operand = fetch_operand<Mode>(); + operand.write(eval_asl(operand.read())); + current_cycles_ += shift_cycle_count<Mode>(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_anc() -> status + { + // ANC: AND #i + copy N to C + auto operand = fetch_operand<Mode>(); + eval_and(operand.read()); + registers_.p.set_c(registers_.p.get_n()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bpl() -> status + { + // BPL: Break If Positive + branch<Mode>(!registers_.p.get_n()); + return status::success; + } + + auto cpu::run_clc() -> status + { + // CLC: Clear Carry Flag + registers_.p.set_c(false); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_jsr() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_and() -> status + { + // AND: Logical AND + auto operand = fetch_operand<Mode>(); + eval_and(operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_rla() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bit() -> status + { + // BIT: Bit Test + auto operand = fetch_operand<Mode>(); + auto const arg = operand.read(); + auto const res = (registers_.a & arg) != 0; + registers_.p.set_z(res == 0); + registers_.p.set_v((arg & 0x40) != 0); + registers_.p.set_n((arg & 0x80) != 0); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_rol() -> status + { + // ROR: Rotate Left + auto operand = fetch_operand<Mode>(); + auto const result = eval_rol(operand.read()); + operand.write(result); + current_cycles_ += shift_cycle_count<Mode>(); + return status::success; + } + + auto cpu::run_plp() -> status + { + // PLP: Pull Processor Status + eval_plp(); + current_cycles_ += cycle_count::from_cpu(4); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bmi() -> status + { + branch<Mode>(registers_.p.get_n()); + return status::success; + } + + auto cpu::run_sec() -> status + { + // SEC: Set Carry Flag + registers_.p.set_c(true); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_rti() -> status + { + // RTI: Return From Interrupt + eval_plp(); + registers_.pc = pop_stack16(); + current_cycles_ += cycle_count::from_cpu(6); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_eor() -> status + { + // EOR: Exclusive OR + auto operand = fetch_operand<Mode>(); + eval_eor(operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sre() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_lsr() -> status + { + // 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_.p.set_c((old_val & 0x1) != 0); + update_zn(new_val); + current_cycles_ += shift_cycle_count<Mode>(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_alr() -> status + { + // 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(); + return status::success; + } + + auto cpu::run_pha() -> status + { + // PHA: Push Accumulator + push_stack8(registers_.a); + current_cycles_ += cycle_count::from_cpu(3); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_jmp() -> status + { + // JMP: Jump + auto operand = fetch_operand<Mode>(); + registers_.pc = operand.get_address().get_absolute(); + current_cycles_ += jump_cycle_count<Mode>(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bvc() -> status + { + // BVC: Branch If Overflow Clear + branch<Mode>(!registers_.p.get_v()); + return status::success; + } + + auto cpu::run_cli() -> status + { + // CLI: Clear Interrupt Disable + registers_.p.set_i(false); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_rts() -> status + { + // RTS: Return from Subroutine + auto const addr = address{ pop_stack16() } + 1; + registers_.pc = addr.get_absolute(); + current_cycles_ += cycle_count::from_cpu(6); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_adc() -> status + { + // ADC: Add With Carry + auto operand = fetch_operand<Mode>(); + eval_adc(operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_rra() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ror() -> status + { + // ROR: Rotate Right + auto operand = fetch_operand<Mode>(); + auto const result = eval_ror(operand.read()); + operand.write(result); + current_cycles_ += shift_cycle_count<Mode>(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_arr() -> status + { + // ARR: AND #i + ROR A but with different flags + // TODO + return status::error_unsupported_instruction; + } + + auto cpu::run_pla() -> status + { + // PLA: Pull Accumulator + registers_.a = pop_stack8(); + update_zn(registers_.a); + current_cycles_ += cycle_count::from_cpu(4); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bvs() -> status + { + // BVS: Branch If Overflow Set + branch<Mode>(registers_.p.get_v()); + return status::success; + } + + auto cpu::run_sei() -> status + { + // SEI: Set Interrupt Disable + registers_.p.set_i(true); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sta() -> status + { + // STA: Store Accumulator + auto operand = fetch_operand<Mode>(force_page_crossing::yes); + operand.write(registers_.a); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ahx() -> status + { + // AHX: ??? + // TODO + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sax() -> status + { + // 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(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sty() -> status + { + // STX: Store Y + auto operand = fetch_operand<Mode>(); + operand.write(registers_.y); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_stx() -> status + { + // STX: Store X + auto operand = fetch_operand<Mode>(); + operand.write(registers_.x); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + auto cpu::run_dey() -> status + { + // DEY: Decrement Y Register + registers_.y -= 1; + update_zn(registers_.y); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_txa() -> status + { + // TXA: Transfer X to Accumulator + registers_.a = registers_.x; + update_zn(registers_.a); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_xaa() -> status + { + // XAA: Depends on analog behavior. + run_stp(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bcc() -> status + { + // BCC: Branch If Carry Clear + branch<Mode>(!registers_.p.get_c()); + return status::success; + } + + auto cpu::run_tya() -> status + { + // TYA: Transfer Y to Accumulator + registers_.a = registers_.y; + update_zn(registers_.a); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_txs() -> status + { + // TXS: Transfer X to Stack Pointer + registers_.sp = registers_.x; + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_tas() -> status + { + // TAS: ??? + // TODO + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_shy() -> status + { + // SHY: ??? + // TODO + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_shx() -> status + { + // SHY: ??? + // TODO + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ldy() -> status + { + // LDY: Load Y Register + auto operand = fetch_operand<Mode>(); + registers_.y = operand.read(); + update_zn(registers_.y); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_lda() -> status + { + // LDA: Load Accumulator + auto operand = fetch_operand<Mode>(); + registers_.a = operand.read(); + update_zn(registers_.a); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_ldx() -> status + { + // LDX: Load X Register + auto operand = fetch_operand<Mode>(); + registers_.x = operand.read(); + update_zn(registers_.x); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_lax() -> status + { + // LAX: LDA + TAX + auto operand = fetch_operand<Mode>(); + registers_.x = registers_.a = operand.read(); + update_zn(registers_.a); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + auto cpu::run_tay() -> status + { + // TAY: Transfer Accumulator to Y + registers_.y = registers_.a; + update_zn(registers_.y); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_tax() -> status + { + // TAX: Transfer Accumulator to X + registers_.x = registers_.a; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bcs() -> status + { + // BCS: Branch If Carry Set + branch<Mode>(registers_.p.get_c()); + return status::success; + } + + auto cpu::run_clv() -> status + { + // CLV: Clear Overflow Flag + registers_.p.set_v(false); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_tsx() -> status + { + // TSX: Transfer Stack Pointer to X + registers_.x = registers_.sp; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_las() -> status + { + // LAS: ??? + // TODO + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_cpy() -> status + { + // CPY: Compare Y Register + auto operand = fetch_operand<Mode>(); + eval_cmp(registers_.y, operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_axs() -> status + { + // AXS: Set X to ((A AND X) - #value without borrow), updates NZC + // TODO + return status::error_unsupported_instruction; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_cmp() -> status + { + // CMP: Compare + auto operand = fetch_operand<Mode>(); + eval_cmp(registers_.a, operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_dcp() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_dec() -> status + { + // 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>(); + return status::success; + } + + auto cpu::run_iny() -> status + { + // INY: Increment Y + registers_.y += 1; + update_zn(registers_.y); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + auto cpu::run_dex() -> status + { + // DEX: Decrement X Register + registers_.x -= 1; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_bne() -> status + { + // BNE: Branch If Not Equal + branch<Mode>(!registers_.p.get_z()); + return status::success; + } + + auto cpu::run_cld() -> status + { + // CLD: Clear Decimal Mode + registers_.p.set_d(false); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_cpx() -> status + { + // CPX: Compare X Register + auto operand = fetch_operand<Mode>(); + eval_cmp(registers_.x, operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_sbc() -> status + { + // SBC: Subtract With Carry + auto operand = fetch_operand<Mode>(); + eval_adc(~operand.read()); + current_cycles_ += operand.get_cycles(); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_isc() -> status + { + // 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); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_inc() -> status + { + // 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>(); + return status::success; + } + + auto cpu::run_inx() -> status + { + // INX: Increment X + registers_.x += 1; + update_zn(registers_.x); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + template<cpu::addressing_mode Mode> + auto cpu::run_beq() -> status + { + // BEQ: Branch If Equal + branch<Mode>(registers_.p.get_z()); + return status::success; + } + + auto cpu::run_sed() -> status + { + // SED: Set Decimal Flag + registers_.p.set_d(true); + current_cycles_ += cycle_count::from_cpu(2); + return status::success; + } + + // ----------------------------------------------------------------------------------------------------------------- + // 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_.p.set_z(value == 0); + registers_.p.set_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_.p.set_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_.p.get_c() ? 1 :0) << 7) | (arg >> 1)); + registers_.p.set_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_.p.get_c() ? 1 : 0)); + registers_.p.set_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_.p.set_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_.p.set_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_.p.get_c() ? 1 : 0); + auto const new_val = static_cast<std::uint8_t>(tmp); + + registers_.a = new_val; + registers_.p.set_c(tmp > 0xFF); + registers_.p.set_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_.p.set_c(a >= b); + } + + auto cpu::eval_plp() -> void + { + // Ignore bits 4 and 5 + registers_.p.value = + (pop_stack8() & 0b11001111) | + (registers_.p.value & 0b00110000); + } + + auto cpu::eval_php() -> void + { + // Always set bits 4 and 5. + push_stack8(registers_.p.value | 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 cartridge_.get_mapper().read_cpu(addr, cartridge_); + } + + 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; } + cartridge_.get_mapper().write_cpu(addr, value, cartridge_); + } + + 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/sys/cpu.hh b/lib/nes/sys/cpu.hh new file mode 100644 index 0000000..d0901c0 --- /dev/null +++ b/lib/nes/sys/cpu.hh @@ -0,0 +1,300 @@ +#pragma once + +#include "nes/sys/types/cycle-count.hh" +#include "nes/sys/types/address.hh" +#include "nes/sys/types/status.hh" + +namespace nes +{ + class ppu; + class controller; + class cartridge; + 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_; + cartridge& cartridge_; + 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 }; + struct + { + auto get_c() const -> bool { return value & 0b00000001; } // carry + auto get_z() const -> bool { return value & 0b00000010; } // zero + auto get_i() const -> bool { return value & 0b00000100; } // interrupt inhibit + auto get_d() const -> bool { return value & 0b00001000; } // decimal + auto get_b() const -> bool { return value & 0b00010000; } // break + auto get_v() const -> bool { return value & 0b01000000; } // overflow + auto get_n() const -> bool { return value & 0b10000000; } // negative + + auto set_c(bool const v) -> void { value = (value & ~0b00000001) | (v ? 0b00000001 : 0); } + auto set_z(bool const v) -> void { value = (value & ~0b00000010) | (v ? 0b00000010 : 0); } + auto set_i(bool const v) -> void { value = (value & ~0b00000100) | (v ? 0b00000100 : 0); } + auto set_d(bool const v) -> void { value = (value & ~0b00001000) | (v ? 0b00001000 : 0); } + auto set_b(bool const v) -> void { value = (value & ~0b00010000) | (v ? 0b00010000 : 0); } + auto set_v(bool const v) -> void { value = (value & ~0b01000000) | (v ? 0b01000000 : 0); } + auto set_n(bool const v) -> void { value = (value & ~0b10000000) | (v ? 0b10000000 : 0); } + + std::uint8_t value{ 0b00100100 }; + } p{}; + } 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&, cartridge&, 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() -> status; + 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() -> status; +#define DEFINE_OPERAND_INSTRUCTION(name) \ + template<addressing_mode Mode> auto run_##name() -> status; + + 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/sys/mapper.cc b/lib/nes/sys/mapper.cc new file mode 100644 index 0000000..4951966 --- /dev/null +++ b/lib/nes/sys/mapper.cc @@ -0,0 +1,172 @@ +#include "nes/sys/mapper.hh" +#include "nes/sys/cartridge.hh" + +namespace nes +{ + namespace + { + // ------------------------------------------------------------------------------------------------------------- + // NROM + // ------------------------------------------------------------------------------------------------------------- + + class mapper_nrom final : public mapper + { + public: + explicit mapper_nrom() = default; + + auto validate(cartridge& cartridge) -> status override + { + if (cartridge.get_ram_length() != 0x2000) + { + return status::error_invalid_ines_data; + } + + if (cartridge.get_prg_rom_length() != 0x4000 && cartridge.get_prg_rom_length() != 0x8000) + { + return status::error_invalid_ines_data; + } + + if (cartridge.get_chr_rom_length() != 0x2000) + { + return status::error_invalid_ines_data; + } + + return status::success; + } + + auto read_cpu(address const addr, cartridge& cartridge) -> std::uint8_t override + { + 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() - 0x8000u; + return cartridge.get_prg_rom()[rel]; + } + if (addr <= address{ 0xFFFF }) + { + auto rel = addr.get_absolute() - 0xC000u; + if (cartridge.get_prg_rom_length() > 0x4000) { rel += 0x4000; } + return cartridge.get_prg_rom()[rel]; + } + + return 0x0; + } + + auto write_cpu(address const addr, std::uint8_t const value, cartridge& cartridge) -> void override + { + if (addr <= address{ 0x5FFF }) + { + return; + } + if (addr <= address{ 0x7FFF }) + { + cartridge.get_ram_mut()[addr.get_absolute() - 0x6000] = value; + return; + } + } + + auto read_ppu(address const addr, cartridge& cartridge, std::uint8_t const* vram) -> std::uint8_t override + { + if (addr <= address{ 0x1FFF }) + { + auto const rel = addr.get_absolute(); + return cartridge.get_chr_rom()[rel]; + } + if (addr <= address{ 0x3EFF }) + { + return vram[mirrored_vram_address(addr, cartridge)]; + } + + return 0x0; + } + + auto write_ppu(address const addr, std::uint8_t const value, cartridge& cartridge, std::uint8_t* vram) -> void override + { + if (addr <= address{ 0x1FFF }) + { + return; + } + if (addr <= address{ 0x3EFF }) + { + vram[mirrored_vram_address(addr, cartridge)] = value; + return; + } + } + + private: + static auto mirrored_vram_address(address const addr, cartridge const& cartridge) -> unsigned + { + auto const rel = addr.get_absolute() % 0x1000u; + + switch (cartridge.get_name_table_arrangement()) + { + case name_table_arrangement::horizontal: + // $2000 = $2400, $2800 = $2c00 + if (rel <= 0x3FF) { return 0x000u + (rel % 0x400u); } + if (rel <= 0x7FF) { return 0x000u + (rel % 0x400u); } + if (rel <= 0xBFF) { return 0x400u + (rel % 0x400u); } + if (rel <= 0xFFF) { return 0x400u + (rel % 0x400u); } + break; + case name_table_arrangement::vertical: + // $2000 = $2800, $2400 = $2c00 + if (rel <= 0x3FF) { return 0x000u + (rel % 0x400u); } + if (rel <= 0x7FF) { return 0x400u + (rel % 0x400u); } + if (rel <= 0xBFF) { return 0x000u + (rel % 0x400u); } + if (rel <= 0xFFF) { return 0x400u + (rel % 0x400u); } + break; + } + + return 0; + } + }; + + // ------------------------------------------------------------------------------------------------------------- + // Invalid + // ------------------------------------------------------------------------------------------------------------- + + class mapper_invalid final : public mapper + { + public: + explicit mapper_invalid() = default; + + auto validate(cartridge&) -> status override { return status::error_unsupported_mapper; } + auto read_cpu(address, cartridge&) -> std::uint8_t override { return 0; } + auto write_cpu(address, std::uint8_t, cartridge&) -> void override {} + auto read_ppu(address, cartridge&, std::uint8_t const*) -> std::uint8_t override { return 0; } + auto write_ppu(address, std::uint8_t, cartridge&, std::uint8_t*) -> void override {} + }; + } // namespace + + // ----------------------------------------------------------------------------------------------------------------- + // Instances + // ----------------------------------------------------------------------------------------------------------------- + + auto mapper::invalid() -> mapper& + { + static auto instance = mapper_invalid{}; + return instance; + } + + auto mapper::get(std::uint8_t const number) -> mapper& + { + switch (number) + { + case 0x00: + { + static auto instance = mapper_nrom{}; + return instance; + } + default: + { + return invalid(); + } + } + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/sys/mapper.hh b/lib/nes/sys/mapper.hh new file mode 100644 index 0000000..e196b25 --- /dev/null +++ b/lib/nes/sys/mapper.hh @@ -0,0 +1,32 @@ +#pragma once + +#include "nes/sys/types/address.hh" +#include "nes/sys/types/status.hh" + +namespace nes +{ + class cartridge; + + class mapper + { + public: + static auto invalid() -> mapper&; + static auto get(std::uint8_t number) -> mapper&; + + virtual ~mapper() = default; + + mapper(mapper const&) = delete; + mapper(mapper&&) = delete; + auto operator=(mapper const&) -> mapper& = delete; + auto operator=(mapper&&) -> mapper& = delete; + + virtual auto validate(cartridge&) -> status = 0; + virtual auto read_cpu(address, cartridge&) -> std::uint8_t = 0; + virtual auto write_cpu(address, std::uint8_t, cartridge&) -> void = 0; + virtual auto read_ppu(address, cartridge&, std::uint8_t const* vram) -> std::uint8_t = 0; + virtual auto write_ppu(address, std::uint8_t, cartridge&, std::uint8_t* vram) -> void = 0; + + protected: + explicit mapper() = default; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/sys/nes.cc b/lib/nes/sys/nes.cc new file mode 100644 index 0000000..f922b7d --- /dev/null +++ b/lib/nes/sys/nes.cc @@ -0,0 +1,65 @@ +#include "nes/sys/nes.hh" +#include "nes/sys/types/snapshot.hh" +#include "nes/common/display.hh" +#include "nes/common/rgb.hh" + +namespace nes +{ + nes::nes(cartridge cartridge, display& display) + : cartridge_{ std::move(cartridge) } + , display_{ display } + , ppu_{ cpu_, cartridge_, display_ } + , cpu_{ ppu_, cartridge_, controller_1_, controller_2_ } + , status_{ cartridge_.get_status() } + { + 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() -> void + { + if (get_status() != status::success) { return; } + + status_ = cpu_.step(); + while (ppu_.get_cycles() < cpu_.get_cycles()) + { + ppu_.step(); + } + } + + auto nes::step(cycle_count const delta) -> void + { + current_cycles_ += delta; + while (cpu_.get_cycles() < current_cycles_ && get_status() == status::success) + { + step(); + } + } + + auto nes::step_to_nmi() -> void + { + while (cpu_.is_nmi_pending()) + { + step(); + } + while (!cpu_.is_nmi_pending()) + { + 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/sys/nes.hh b/lib/nes/sys/nes.hh new file mode 100644 index 0000000..b421a5f --- /dev/null +++ b/lib/nes/sys/nes.hh @@ -0,0 +1,47 @@ +#pragma once + +#include "nes/sys/types/cycle-count.hh" +#include "nes/sys/cartridge.hh" +#include "nes/sys/controller.hh" +#include "nes/sys/cpu.hh" +#include "nes/sys/ppu.hh" + +namespace nes +{ + class display; + struct snapshot; + + /// The main console abstraction. + class nes + { + cartridge cartridge_; + display& display_; + controller controller_1_; + controller controller_2_; + ppu ppu_; + cpu cpu_; + cycle_count current_cycles_; + status status_{ status::success }; + + 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_status() const -> status { return status_; } + auto get_controller_1() const -> controller const& { return controller_1_; } + auto ref_controller_1() -> controller& { return controller_1_; } + auto get_controller_2() const -> controller const& { return controller_2_; } + auto ref_controller_2() -> controller& { return controller_2_; } + + auto step() -> void; + auto step(std::chrono::microseconds delta) -> void; + auto step(cycle_count delta) -> void; + auto step_to_nmi() -> void; + + auto get_snapshot() -> snapshot; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/sys/ppu.cc b/lib/nes/sys/ppu.cc new file mode 100644 index 0000000..2a5f01c --- /dev/null +++ b/lib/nes/sys/ppu.cc @@ -0,0 +1,697 @@ +#include "nes/sys/ppu.hh" +#include "nes/sys/cpu.hh" +#include "nes/sys/cartridge.hh" +#include "nes/sys/types/address.hh" +#include "nes/sys/types/snapshot.hh" +#include "nes/common/display.hh" +#include "nes/common/rgb.hh" +#include "nes/common/debug.hh" + +namespace nes +{ + ppu::ppu(cpu& cpu, cartridge& cartridge, display& display) + : cpu_{ cpu } + , cartridge_{ cartridge } + , 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 + // Inspired by https://github.com/fogleman/nes/blob/master/nes/ppu.go + + auto const enable_rendering = mask_.get_enable_background() || mask_.get_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) + { + // Fetch cycle for the background. + switch (scanline_cycle_ % 8) + { + case 1: + { + // Load the tile pattern for the background from the name table. + fetch_cycle_.tile = tile{ read8(address{ 0x2000 } + internal_.v.get_tile_address()) }; + break; + } + case 3: + { + // Load the tile palette for the background from the attribute table. + auto const addr = + address{ 0x23C0 } + + (static_cast<unsigned>(internal_.v.get_name_table()) * 0x400u) + + ((internal_.v.get_coarse_y() & 0b11100u) << 1) + + ((internal_.v.get_coarse_x() & 0b11100u) >> 2); + auto const shift = + ((internal_.v.get_coarse_y() & 0b00010u) << 1) | + ((internal_.v.get_coarse_x() & 0b00010u) << 0); + fetch_cycle_.palette = static_cast<palette>((read8(addr) >> shift) & 0b11); + break; + } + case 5: + { + // Load bitplane 0 for the background tile's pattern. + fetch_cycle_.bitplane_0 = get_tile_bitplane( + control_.get_background_pattern_table(), + fetch_cycle_.tile, + internal_.v.get_fine_y(), + bitplane::_0); + break; + } + case 7: + { + // Load bitplane 1 for the background tile's pattern. + fetch_cycle_.bitplane_1 = get_tile_bitplane( + control_.get_background_pattern_table(), + fetch_cycle_.tile, + internal_.v.get_fine_y(), + bitplane::_1); + break; + } + case 0: + { + // Fetch cycle done -> build the tile row for the background. + current_background_ = next_background_; + next_background_ = get_tile_row( + fetch_cycle_.palette, fetch_cycle_.bitplane_0, fetch_cycle_.bitplane_1); + 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_.set_vblank(true); + if (control_.get_vblank_nmi()) { cpu_.trigger_nmi(); } + } + if (pre_line && scanline_cycle_ == 1) + { + status_.set_vblank(false); + status_.set_sprite_zero_hit(false); + status_.set_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_.get_enable_background()) + { + background_color = current_background_.colors[internal_.x + x % tile_size]; + background_color.set_role(role::background); + } + + auto foreground = evaluated_sprite{}; + auto foreground_color = color_index{ 0 }; + if (mask_.get_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.x); + if (offset < 0 || static_cast<unsigned>(offset) >= tile_size) { continue; } + auto const color = s.pattern.colors[offset]; + if (color.get_color() == palette_color::_0) { continue; } + + foreground = s; + foreground_color = color; + foreground_color.set_role(role::foreground); + break; + } + } + + auto has_background = background_color.get_color() != palette_color::_0; + auto has_foreground = foreground_color.get_color() != palette_color::_0; + + if (x < tile_size && !mask_.get_show_background_start()) { has_background = false; } + if (x < tile_size && !mask_.get_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_.set_sprite_zero_hit(true); + } + + color = foreground.is_in_front ? foreground_color : background_color; + } + + display_.set(x, y, resolve_color(ref_color(color))); + } + + auto ppu::evaluate_sprites() -> void + { + auto const height = get_sprite_height(); + sprite_count_ = 0; + for (auto i = unsigned{ 0 }; i < sprite_max_count; ++i) + { + auto const s = sprite{ &oam_[i * 4] }; + auto const row = static_cast<int>(scanline_) - static_cast<int>(s.get_y()); + if (row < 0 || static_cast<unsigned>(row) >= height) { continue; } + + if (sprite_count_ < 8) + { + sprites_[sprite_count_].pattern = fetch_sprite_pattern(s, static_cast<unsigned>(row)); + sprites_[sprite_count_].x = s.get_x(); + sprites_[sprite_count_].is_in_front = !s.get_behind_background(); + sprites_[sprite_count_].is_sprite_zero = i == 0; + sprite_count_ += 1; + } + else + { + status_.set_sprite_overflow(true); + break; + } + } + } + + auto ppu::fetch_sprite_pattern(sprite const s, unsigned row) -> tile_row + { + if (s.get_flip_vertical()) { row = get_sprite_height() - 1 - row; } + tile tile; + pattern_table pattern_table; + switch (control_.get_sprite_size()) + { + case sprite_size::single_height: + tile = s.get_small_tile(); + pattern_table = control_.get_sprite_pattern_table(); + break; + case sprite_size::double_height: + tile = s.get_large_top_tile(); + if (row >= tile_size) + { + tile = static_cast<ppu::tile>(1); + row -= tile_size; + } + pattern_table = s.get_large_pattern_table(); + break; + default: + return tile_row{}; + } + + auto const bitplane_0 = get_tile_bitplane(pattern_table, tile, row, bitplane::_0); + auto const bitplane_1 = get_tile_bitplane(pattern_table, tile, row, bitplane::_1); + auto res = get_tile_row(s.get_palette(), bitplane_0, bitplane_1); + + if (s.get_flip_horizontal()) + { + for (auto i = unsigned{ 0 }; i < tile_size / 2; ++i) + { + std::swap(res.colors[i], res.colors[tile_size - 1 - i]); + } + } + + return res; + } + + // ----------------------------------------------------------------------------------------------------------------- + // 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 cartridge_.get_mapper().read_ppu(addr, cartridge_, vram_); } + if (addr <= address{ 0x3FFF }) + { + auto const index = color_index{ static_cast<std::uint8_t>(addr.get_absolute() % 0x20) }; + auto const color = ref_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 }) + { + cartridge_.get_mapper().write_ppu(addr, value, cartridge_, vram_); + return; + } + if (addr <= address{ 0x3FFF }) + { + auto const index = color_index{ static_cast<std::uint8_t>(addr.get_absolute() % 0x20) }; + ref_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 res = status_; + res.set_remaining(latch_); + NES_DEBUG_LOG(ppu, "PPUSTATUS -> {:#2x}", res.value); + + status_.set_vblank(false); + internal_.w = false; + + write_latch(res.value); + return res.value; + } + + 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.value }; + 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} (address: {:#4x})", res, internal_.v); + 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.set_name_table(control_.get_base_name_table()); + if (control_.get_vblank_nmi() && status_.get_vblank()) { cpu_.trigger_nmi(); } + } + } + + 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.set_coarse_x((value & 0b11111000) >> 3); + internal_.x = (value & 0b00000111) >> 0; + } + else + { + // Second write -> y value. + internal_.t.set_coarse_y((value & 0b11111000) >> 3); + internal_.t.set_fine_y((value & 0b00000111) >> 0); + } + + 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.set_address_high(value & 0b00111111); + } + else + { + // Second write. + internal_.t.set_address_low(value); + 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 }, 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 + { + 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::increment_vram() -> void + { + switch (control_.get_vram_increment()) + { + case vram_increment::forward: + internal_.v.value += 1; + break; + case vram_increment::downward: + internal_.v.value += 32; + break; + } + } + + auto ppu::increment_x() -> void + { + if (internal_.v.get_coarse_x() == 31) + { + internal_.v.set_coarse_x(0); + internal_.v.set_horizontal_name_table(1 ^ internal_.v.get_horizontal_name_table()); + } + else + { + internal_.v.set_coarse_x(internal_.v.get_coarse_x() + 1); + } + } + + auto ppu::increment_y() -> void + { + if (internal_.v.get_fine_y() == 7) + { + internal_.v.set_fine_y(0); + if (internal_.v.get_coarse_y() == 29) + { + internal_.v.set_coarse_y(0); + internal_.v.set_vertical_name_table(1 ^ internal_.v.get_vertical_name_table()); + } + else if (internal_.v.get_coarse_y() == 31) + { + internal_.v.set_coarse_y(0); + // Nametable not switched + } + else + { + internal_.v.set_coarse_y(internal_.v.get_coarse_y() + 1); + } + } + else + { + internal_.v.set_fine_y(internal_.v.get_fine_y() + 1); + } + } + + auto ppu::copy_x() -> void + { + internal_.v.set_coarse_x(internal_.t.get_coarse_x()); + internal_.v.set_horizontal_name_table(internal_.t.get_horizontal_name_table()); + } + + auto ppu::copy_y() -> void + { + internal_.v.set_coarse_y(internal_.t.get_coarse_y()); + internal_.v.set_fine_y(internal_.t.get_fine_y()); + internal_.v.set_vertical_name_table(internal_.t.get_vertical_name_table()); + } + + auto ppu::get_sprite_height() const -> unsigned + { + switch (control_.get_sprite_size()) + { + case sprite_size::single_height: + return tile_size; + case sprite_size::double_height: + return tile_size * 2; + } + + return tile_size; + } + + auto ppu::get_tile_bitplane( + pattern_table const pattern_table, tile const tile, unsigned const row, bitplane const bitplane) -> std::uint8_t + { + auto const addr = static_cast<std::uint16_t>( + 0x1000 * static_cast<unsigned>(pattern_table) + 0x10 * static_cast<unsigned>(tile) + row); + return read8(address{ addr } + (static_cast<unsigned>(bitplane) * 8u)); + } + + auto ppu::get_tile_row(palette const palette, std::uint8_t bitplane_0, std::uint8_t bitplane_1) const -> tile_row + { + auto res = tile_row{}; + for (auto i = unsigned{ 0 }; i < tile_size; ++i) + { + auto const bit_0 = (bitplane_0 & 0b10000000u) >> 7; + auto const bit_1 = (bitplane_1 & 0b10000000u) >> 7; + bitplane_0 <<= 1; + bitplane_1 <<= 1; + res.colors[i].set_palette(palette); + res.colors[i].set_color(palette_color{ (bit_1 << 1) | (bit_0 << 0) }); + } + return res; + } + + auto ppu::ref_color(color_index index) -> color& + { + // See https://www.nesdev.org/wiki/PPU_palettes + index.value = index.value % 0x20; + if (index.get_color() == palette_color::_0) + { + // The first color is mirrored between background and foreground palettes. + index.set_role(role::background); + } + return palette_buffer_[index.value]; + } + + auto ppu::resolve_color(color const color) const -> 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/sys/ppu.hh b/lib/nes/sys/ppu.hh new file mode 100644 index 0000000..b1b8842 --- /dev/null +++ b/lib/nes/sys/ppu.hh @@ -0,0 +1,279 @@ +#pragma once + +#include "nes/sys/types/cycle-count.hh" + +namespace nes +{ + class address; + class cpu; + class cartridge; + class display; + class rgb; + struct snapshot; + + class ppu + { + static constexpr auto sprite_max_count = unsigned{ 64 }; + static constexpr auto vram_size = std::size_t{ 0x800 }; + static constexpr auto oam_size = std::size_t{ sprite_max_count * 4 }; + static constexpr auto palette_buffer_size = std::size_t{ 0x20 }; + static constexpr auto tile_size = unsigned{ 8 }; + // Writes to some registers are ignored until this clock cycle. + static constexpr auto boot_up_cycles = cycle_count::from_ppu(29658); + + enum class bitplane : unsigned { _0, _1 }; + enum class palette : unsigned { _0, _1, _2, _3 }; + enum class palette_color : unsigned { _0, _1, _2, _3 }; + enum class name_table : unsigned { _0, _1, _2, _3 }; + enum class role : unsigned { background, foreground }; + enum class pattern_table : unsigned { _0, _1 }; + enum class vram_increment : unsigned { forward, downward }; + enum class sprite_size : unsigned { single_height, double_height }; + enum class tile : std::uint8_t {}; + enum class color : std::uint8_t {}; + +#define BITFIELD_VALIDATE(mask, offset_from_right) \ + static_assert((mask & (1 << offset_from_right)) != 0); \ + static_assert(offset_from_right == 0 || (mask & (1 << (offset_from_right - 1))) == 0); +#define BITFIELD_VALUE(name, base, mask, offset_from_right) \ + BITFIELD_VALIDATE(mask, offset_from_right) \ + auto get_##name() const -> unsigned { return (base & mask) >> offset_from_right; } \ + auto set_##name(unsigned const v) -> void { base = (base & ~mask) | ((v << offset_from_right) & mask); } +#define BITFIELD_ENUM(name, type, base, mask, offset_from_right) \ + BITFIELD_VALIDATE(mask, offset_from_right) \ + auto get_##name() const -> type { return static_cast<type>((base & mask) >> offset_from_right); } \ + auto set_##name(type const v) -> void { base = (base & ~mask) | ((static_cast<unsigned>(v) << offset_from_right) & mask); } +#define BITFIELD_FLAG(name, base, mask, offset_from_right) \ + BITFIELD_VALIDATE(mask, offset_from_right) \ + auto get_##name() const -> bool { return base & mask; } \ + auto set_##name(unsigned const v) -> void { base = (base & ~mask) | (v ? mask : 0); } + + struct color_index + { + BITFIELD_ENUM(color, palette_color, value, 0b00000011, 0) + BITFIELD_ENUM(palette, palette, value, 0b00001100, 2) + BITFIELD_ENUM(role, role, value, 0b00010000, 4) + + std::uint8_t value{}; + + color_index() = default; + + explicit color_index(std::uint8_t const value) + : value{ value } + { + } + }; + + struct sprite + { + // Tile index (and pattern table) for sprites + BITFIELD_ENUM(large_pattern_table, pattern_table, value[1], 0b00000001, 0) + BITFIELD_ENUM(large_top_tile, tile, value[1], 0b11111110, 1) + BITFIELD_ENUM(small_tile, tile, value[1], 0b11111111, 0) + + // Attributes + BITFIELD_ENUM(palette, palette, value[2], 0b00000011, 0) + BITFIELD_FLAG(behind_background, value[2], 0b00100000, 5) + BITFIELD_FLAG(flip_horizontal, value[2], 0b01000000, 6) + BITFIELD_FLAG(flip_vertical, value[2], 0b10000000, 7) + + auto get_y() const -> std::uint8_t { return value[0]; } + auto get_tile_index() const -> std::uint8_t { return value[1]; } + auto get_attributes() const -> std::uint8_t { return value[2]; } + auto get_x() const -> std::uint8_t { return value[3]; } + + auto set_y(std::uint8_t const v) -> void { value[0] = v; } + auto set_tile_index(std::uint8_t const v) -> void { value[1] = v; } + auto set_attributes(std::uint8_t const v) -> void { value[2] = v; } + auto set_x(std::uint8_t const v) -> void { value[3] = v; } + + std::uint8_t value[4]{}; + + explicit sprite() = default; + + explicit sprite(std::uint8_t const* data) + : value{ data[0], data[1], data[2], data[3] } + { + } + }; + + struct tile_row + { + color_index colors[tile_size]{}; + }; + + struct evaluated_sprite + { + tile_row pattern; + std::uint8_t x{ 0 }; + bool is_in_front{ false }; + bool is_sprite_zero{ false }; + }; + + cycle_count current_cycles_; + cpu& cpu_; + cartridge& cartridge_; + 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. + struct + { + BITFIELD_FLAG(sprite_overflow, value, 0b00100000, 5) + BITFIELD_FLAG(sprite_zero_hit, value, 0b01000000, 6) + BITFIELD_FLAG(vblank, value, 0b10000000, 7) + + // Set the remaining (unused) bits based on the given value. + auto set_remaining(std::uint8_t const v) -> void { value = (value & ~0b00011111) | (v & 0b00011111); } + + std::uint8_t value{ 0b00000000 }; + } status_{}; + struct + { + // Base nametable address. + BITFIELD_ENUM(base_name_table, name_table, value, 0b00000011, 0) + // How VRAM addresses should be incremented each read/write. + BITFIELD_ENUM(vram_increment, vram_increment, value, 0b00000100, 2) + // Sprite pattern table address for 8x8 sprites. + BITFIELD_ENUM(sprite_pattern_table, pattern_table, value, 0b00001000, 3) + // Background pattern table address. + BITFIELD_ENUM(background_pattern_table, pattern_table, value, 0b00010000, 4) + // The size of the sprites used (i.e. single-height or double-height). + BITFIELD_ENUM(sprite_size, sprite_size, value, 0b00100000, 5) + // PPU master/slave select. + BITFIELD_FLAG(enable_ext_pin, value, 0b01000000, 6) + // Enable/disable vblank NMI. + BITFIELD_FLAG(vblank_nmi, value, 0b10000000, 7) + + std::uint8_t value{ 0b00000000 }; + } control_{}; + struct + { + // Enable/disable grayscale. + BITFIELD_FLAG(grayscale, value, 0b00000001, 0) + // Show background in leftmost 8 pixels of screen. + BITFIELD_FLAG(show_background_start, value, 0b00000010, 1) + // Show sprites in leftmost 8 pixels of screen. + BITFIELD_FLAG(show_sprites_start, value, 0b00000100, 2) + // Enable background rendering. + BITFIELD_FLAG(enable_background, value, 0b00001000, 3) + // Enable sprite rendering. + BITFIELD_FLAG(enable_sprites, value, 0b00010000, 4) + // Emphasize red. + BITFIELD_FLAG(emphasize_red, value, 0b00100000, 5) + // Emphasize green. + BITFIELD_FLAG(emphasize_green, value, 0b01000000, 6) + // Emphasize blue. + BITFIELD_FLAG(emphasize_blue, value, 0b10000000, 7) + + std::uint8_t value{ 0b00000000 }; + } mask_{}; + struct + { + // See https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers + struct + { + // Scroll position + BITFIELD_VALUE(coarse_x, value, 0b0000000000011111, 0) + BITFIELD_VALUE(coarse_y, value, 0b0000001111100000, 5) + BITFIELD_ENUM(name_table, name_table, value, 0b0000110000000000, 10) + BITFIELD_VALUE(horizontal_name_table, value, 0b0000010000000000, 10) + BITFIELD_VALUE(vertical_name_table, value, 0b0000100000000000, 11) + BITFIELD_VALUE(tile_address, value, 0b0000111111111111, 0) + BITFIELD_VALUE(fine_y, value, 0b0111000000000000, 12) + + // Address + BITFIELD_VALUE(address_low, value, 0b0000000011111111, 0) + BITFIELD_VALUE(address_high, value, 0b0111111100000000, 8) + + std::uint16_t value{}; + } v{}, t{}; // Current and temporary VRAM address and scroll position (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 }; + tile_row current_background_{}; + tile_row next_background_{}; + struct + { + tile tile{ 0 }; + palette palette{ 0 }; + std::uint8_t bitplane_0{ 0 }; + std::uint8_t bitplane_1{ 0 }; + } fetch_cycle_{}; // Data populated during the fetch cycle. + evaluated_sprite sprites_[8]{}; // Evaluated sprites. + unsigned sprite_count_{ 0 }; // Number of evaluated sprites in sprites_. + +#undef BITFIELD_VALIDATE +#undef BITFIELD_VALUE +#undef BITFIELD_ENUM +#undef BITFIELD_FLAG + + public: + explicit ppu(cpu&, cartridge&, 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 evaluate_sprites() -> void; + auto fetch_sprite_pattern(sprite, unsigned row) -> tile_row; + + // Helpers + + auto increment_vram() -> void; + auto increment_x() -> void; + auto increment_y() -> void; + auto copy_x() -> void; + auto copy_y() -> void; + auto get_sprite_height() const -> unsigned; + auto get_tile_bitplane(pattern_table, tile, unsigned row, bitplane) -> std::uint8_t; + auto get_tile_row(palette, std::uint8_t bitplane_0, std::uint8_t bitplane_1) const -> tile_row; + auto ref_color(color_index) -> color&; + auto resolve_color(color) const -> rgb; + }; +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/sys/types/CMakeLists.txt b/lib/nes/sys/types/CMakeLists.txt new file mode 100644 index 0000000..f51b0ae --- /dev/null +++ b/lib/nes/sys/types/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources( + nes + PRIVATE + address.hh + button-mask.hh + cycle-count.hh + snapshot.hh + status.hh)
\ No newline at end of file diff --git a/lib/nes/sys/types/address.hh b/lib/nes/sys/types/address.hh new file mode 100644 index 0000000..c2e1352 --- /dev/null +++ b/lib/nes/sys/types/address.hh @@ -0,0 +1,74 @@ +#pragma once + +#include <cstdint> + +namespace nes +{ + /// An address on the NES (for simplicity, we share the same type for CPU and PPU, even though the PPU only uses + /// 14 bit addresses). + 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, unsigned const b) -> address + { + return address{ static_cast<std::uint16_t>(a.get_absolute() + b) }; + } + + constexpr auto operator+(unsigned const a, address const b) -> address + { + return address{ static_cast<std::uint16_t>(a + b.get_absolute()) }; + } + + constexpr auto operator-(address const a, unsigned const b) -> address + { + return address{ static_cast<std::uint16_t>(a.get_absolute() - b) }; + } + + constexpr auto operator%(address const a, unsigned 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/sys/types/button-mask.hh b/lib/nes/sys/types/button-mask.hh new file mode 100644 index 0000000..16b0dad --- /dev/null +++ b/lib/nes/sys/types/button-mask.hh @@ -0,0 +1,44 @@ +#pragma once + +#include <cstdint> + +namespace nes +{ + /// Buttons pressed on a controller. + 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/sys/types/cycle-count.hh b/lib/nes/sys/types/cycle-count.hh new file mode 100644 index 0000000..f1a4a7e --- /dev/null +++ b/lib/nes/sys/types/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/sys/types/snapshot.hh b/lib/nes/sys/types/snapshot.hh new file mode 100644 index 0000000..683e862 --- /dev/null +++ b/lib/nes/sys/types/snapshot.hh @@ -0,0 +1,63 @@ +#pragma once + +#include "nes/sys/types/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 diff --git a/lib/nes/sys/types/status.hh b/lib/nes/sys/types/status.hh new file mode 100644 index 0000000..64f5969 --- /dev/null +++ b/lib/nes/sys/types/status.hh @@ -0,0 +1,36 @@ +#pragma once + +namespace nes +{ + /// A status code. + enum class status + { + success, + error_uninitialized, + error_system_error, + error_invalid_ines_data, + error_unsupported_mapper, + error_unsupported_instruction, + }; + + constexpr auto to_string(status const status) -> char const* + { + switch (status) + { + case status::success: + return "Success"; + case status::error_uninitialized: + return "Uninitialized"; + case status::error_system_error: + return "System error"; + case status::error_invalid_ines_data: + return "Invalid iNES data"; + case status::error_unsupported_mapper: + return "Unsupported mapper"; + case status::error_unsupported_instruction: + return "Unsupported instruction"; + } + + return "(invalid)"; + } +} // namespace nes
\ No newline at end of file |
