summaryrefslogtreecommitdiff
path: root/lib/nes/sys
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2025-01-29 14:49:20 +0100
committerHannes Schulze <haschu0103@outlook.de>2025-01-29 14:49:20 +0100
commit5959349f341a2380c0c3f41ec40a1e8e0d969d00 (patch)
tree8b36ba3f8cade68562af1dfdcea8b42160619975 /lib/nes/sys
parent1d32f7561305d6c86b1c80858bc73d55b1c6c402 (diff)
refactor: move emulation logic into sys
Diffstat (limited to 'lib/nes/sys')
-rw-r--r--lib/nes/sys/CMakeLists.txt17
-rw-r--r--lib/nes/sys/cartridge.cc68
-rw-r--r--lib/nes/sys/cartridge.hh78
-rw-r--r--lib/nes/sys/controller.cc18
-rw-r--r--lib/nes/sys/controller.hh30
-rw-r--r--lib/nes/sys/cpu.cc1425
-rw-r--r--lib/nes/sys/cpu.hh300
-rw-r--r--lib/nes/sys/mapper.cc172
-rw-r--r--lib/nes/sys/mapper.hh32
-rw-r--r--lib/nes/sys/nes.cc65
-rw-r--r--lib/nes/sys/nes.hh47
-rw-r--r--lib/nes/sys/ppu.cc697
-rw-r--r--lib/nes/sys/ppu.hh279
-rw-r--r--lib/nes/sys/types/CMakeLists.txt8
-rw-r--r--lib/nes/sys/types/address.hh74
-rw-r--r--lib/nes/sys/types/button-mask.hh44
-rw-r--r--lib/nes/sys/types/cycle-count.hh87
-rw-r--r--lib/nes/sys/types/snapshot.hh63
-rw-r--r--lib/nes/sys/types/status.hh36
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