summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/nes/controller.cc11
-rw-r--r--src/nes/controller.hh2
-rw-r--r--src/nes/cpu.cc21
-rw-r--r--src/nes/cpu.hh5
-rw-r--r--src/nes/nes.cc9
-rw-r--r--src/nes/nes.hh1
-rw-r--r--src/nes/ppu.cc402
-rw-r--r--src/nes/ppu.hh38
-rw-r--r--src/scene.mm4
9 files changed, 476 insertions, 17 deletions
diff --git a/src/nes/controller.cc b/src/nes/controller.cc
index e73f8e5..87842e2 100644
--- a/src/nes/controller.cc
+++ b/src/nes/controller.cc
@@ -4,12 +4,15 @@ namespace nes
{
auto controller::read() -> std::uint8_t
{
- // TODO
- return 0x0;
+ auto const res = index_ < 8 ? (pressed_.get_raw_value() >> index_) & 1 : 0;
+ index_ += 1;
+ if (strobing_) { index_ = 0; }
+ return res;
}
- auto controller::write(std::uint8_t) -> void
+ auto controller::write(std::uint8_t const value) -> void
{
- // TODO
+ strobing_ = value & 1;
+ if (strobing_) { index_ = 0; }
}
} // namespace nes \ No newline at end of file
diff --git a/src/nes/controller.hh b/src/nes/controller.hh
index d0872a6..fa16859 100644
--- a/src/nes/controller.hh
+++ b/src/nes/controller.hh
@@ -7,6 +7,8 @@ namespace nes
class controller
{
button_mask pressed_;
+ unsigned index_{ 0 };
+ bool strobing_{ false };
public:
explicit controller() = default;
diff --git a/src/nes/cpu.cc b/src/nes/cpu.cc
index 9b5ed66..bb4830f 100644
--- a/src/nes/cpu.cc
+++ b/src/nes/cpu.cc
@@ -38,6 +38,11 @@ namespace nes
current_cycles_ += count;
}
+ auto cpu::trigger_nmi() -> void
+ {
+ nmi_pending_ = true;
+ }
+
auto cpu::step_to(cycle_count const target) -> void
{
while (current_cycles_ < target)
@@ -50,6 +55,12 @@ namespace nes
{
// See https://www.nesdev.org/wiki/CPU_unofficial_opcodes
+ if (nmi_pending_)
+ {
+ execute_interrupt(address{ 0xFFFA });
+ nmi_pending_ = false;
+ }
+
auto const opcode = advance_pc8();
switch (opcode)
{
@@ -1035,6 +1046,16 @@ namespace nes
}
}
+ auto cpu::execute_interrupt(address vector) -> void
+ {
+ push_stack16(registers_.pc);
+ run_php();
+ registers_.pc = read16(vector);
+ registers_.i = true;
+ current_cycles_ += cycle_count::from_cpu(7);
+ }
+
+
auto cpu::eval_ror(std::uint8_t const arg) -> std::uint8_t
{
auto const res = static_cast<std::uint8_t>((registers_.c << 7) | (arg >> 1));
diff --git a/src/nes/cpu.hh b/src/nes/cpu.hh
index 75b1943..070a72d 100644
--- a/src/nes/cpu.hh
+++ b/src/nes/cpu.hh
@@ -21,6 +21,7 @@ namespace nes
controller& controller_1_;
controller& controller_2_;
std::uint8_t ram_[ram_size]{};
+ bool nmi_pending_{ false };
// Registers
struct
@@ -124,7 +125,7 @@ namespace nes
};
public:
- explicit cpu(ppu& ppu, mapper& mapper, controller& controller_1, controller& controller_2);
+ explicit cpu(ppu&, mapper&, controller& controller_1, controller& controller_2);
cpu(cpu const&) = delete;
cpu(cpu&&) = delete;
@@ -134,6 +135,7 @@ namespace nes
auto snapshot(test::status&) -> void;
auto stall_cycles(cycle_count) -> void;
+ auto trigger_nmi() -> void;
auto step_to(cycle_count) -> void;
// Memory access
@@ -244,6 +246,7 @@ namespace nes
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;
diff --git a/src/nes/nes.cc b/src/nes/nes.cc
index 84f3309..3ed7a22 100644
--- a/src/nes/nes.cc
+++ b/src/nes/nes.cc
@@ -10,7 +10,7 @@ namespace nes
: cartridge_{ std::move(cartridge) }
, display_{ display }
, mapper_{ mapper::select(cartridge_) }
- , ppu_{ cpu_, mapper_ }
+ , ppu_{ cpu_, mapper_, display_ }
, cpu_{ ppu_, mapper_, controller_1_, controller_2_ }
{
if (!cartridge_.is_valid())
@@ -19,8 +19,9 @@ namespace nes
std::abort();
}
- display_.clear(rgb{ animation_progress_, animation_progress_, animation_progress_ });
+ display_.clear(rgb{});
display_.switch_buffers();
+ display_.clear(rgb{});
}
auto nes::step(std::chrono::microseconds const delta) -> void
@@ -33,10 +34,6 @@ namespace nes
current_cycles_ += delta;
cpu_.step_to(current_cycles_);
ppu_.step_to(current_cycles_);
-
- animation_progress_ -= 2;
- display_.clear(rgb{ animation_progress_, animation_progress_, animation_progress_ });
- display_.switch_buffers();
}
auto nes::snapshot() -> test::status
diff --git a/src/nes/nes.hh b/src/nes/nes.hh
index 2701f4b..16e645a 100644
--- a/src/nes/nes.hh
+++ b/src/nes/nes.hh
@@ -23,7 +23,6 @@ namespace nes
ppu ppu_;
cpu cpu_;
cycle_count current_cycles_;
- std::uint8_t animation_progress_{ 255 };
public:
explicit nes(cartridge, display&);
diff --git a/src/nes/ppu.cc b/src/nes/ppu.cc
index e57754d..003c3d6 100644
--- a/src/nes/ppu.cc
+++ b/src/nes/ppu.cc
@@ -1,4 +1,5 @@
#include "nes/ppu.hh"
+#include "nes/display.hh"
#include "nes/cpu.hh"
#include "nes/mapper.hh"
#include "nes/util/address.hh"
@@ -6,9 +7,10 @@
namespace nes
{
- ppu::ppu(cpu& cpu, mapper& mapper)
+ ppu::ppu(cpu& cpu, mapper& mapper, display& display)
: cpu_{ cpu }
, mapper_{ mapper }
+ , display_{ display }
{
}
@@ -16,14 +18,380 @@ namespace nes
{
while (current_cycles_ < cycle)
{
- current_cycles_ += cycle_count::from_ppu(1);
step();
}
}
auto ppu::step() -> void
{
- // TODO
+ // See https://www.nesdev.org/wiki/PPU_rendering
+ // Based on: https://github.com/fogleman/nes/blob/master/nes/ppu.go
+
+ if (nmi_delay_ > 0)
+ {
+ nmi_delay_ -= 1;
+ if (nmi_delay_ == 0 && control_.vblank_nmi && status_.vblank)
+ {
+ cpu_.trigger_nmi();
+ }
+ }
+
+ auto const enable_rendering = mask_.enable_background || mask_.enable_sprites;
+
+ current_cycles_ += cycle_count::from_ppu(1);
+ scanline_cycle_ += 1;
+ if (enable_rendering && !even_frame_ && scanline_ == 261 && scanline_cycle_ == 340)
+ {
+ current_cycles_ += cycle_count::from_ppu(1);
+ scanline_cycle_ += 1;
+ }
+ if (scanline_cycle_ == 341)
+ {
+ scanline_cycle_ = 0;
+ scanline_ += 1;
+
+ if (scanline_ == 261)
+ {
+ scanline_ = 0;
+ even_frame_ = !even_frame_;
+
+ display_.switch_buffers();
+ }
+ }
+
+ 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)
+ {
+ switch (scanline_cycle_ % 8)
+ {
+ case 1:
+ fetch_name_table_byte();
+ break;
+ case 3:
+ fetch_attribute_table_byte();
+ break;
+ case 5:
+ fetch_low_tile_byte();
+ break;
+ case 7:
+ fetch_high_tile_byte();
+ break;
+ case 0:
+ store_tile_data();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (pre_line && scanline_cycle_ >= 280 && scanline_cycle_ <= 304)
+ {
+ copy_y();
+ }
+
+ if (render_line)
+ {
+ if (fetch_cycle && scanline_cycle_ % 8 == 0)
+ {
+ increment_x();
+ }
+ if (scanline_cycle_ == 256)
+ {
+ increment_y();
+ }
+ if (scanline_cycle_ == 257)
+ {
+ copy_x();
+ }
+ }
+ }
+
+ // Sprite Logic
+ if (enable_rendering)
+ {
+ if (scanline_cycle_ == 257)
+ {
+ if (visible_line)
+ {
+ evaluate_sprites();
+ }
+ else
+ {
+ sprite_count_ = 0;
+ }
+ }
+ }
+
+ // Vblank Logic
+ if (scanline_ == 241 && scanline_cycle_ == 1)
+ {
+ status_.vblank = true;
+ nmi_change();
+ }
+ if (pre_line && scanline_cycle_ == 1)
+ {
+ status_.vblank = false;
+ nmi_change();
+ status_.sprite_0_hit = false;
+ status_.sprite_overflow = false;
+ }
+ }
+
+ auto ppu::render_pixel() -> void
+ {
+ auto const x = scanline_cycle_ - 1;
+ auto const y = scanline_;
+
+ auto background_color = color_index{ 0 };
+ if (mask_.enable_background)
+ {
+ auto const tile_data = static_cast<std::uint32_t>(tile_data_ >> 32);
+ background_color = color_index{ (tile_data >> ((7 - internal_.x) * 4)) & 0xF };
+ background_color.role = role::background;
+ }
+
+ auto foreground = sprite{};
+ auto foreground_color = color_index{ 0 };
+ for (auto i = unsigned{ 0 }; i < sprite_count_; ++i)
+ {
+ auto const s = sprites_[i];
+ auto const offset = static_cast<int>(x) - static_cast<int>(s.position);
+ if (offset < 0 || offset > 7) { continue; }
+ auto const color = color_index{ (s.pattern >> ((7 - offset) * 4)) & 0xF };
+ if (color.color == 0) { continue; }
+
+ foreground = s;
+ foreground_color = color;
+ foreground_color.role = role::sprite;
+ break;
+ }
+
+ if (x < 8 && !mask_.show_background_start) { background_color = color_index{ 0 }; }
+ if (x < 8 && !mask_.show_sprites_start) { foreground_color = color_index{ 0 }; }
+
+ auto const has_background = background_color.color != 0;
+ auto const has_foreground = foreground_color.color != 0;
+ auto color = color_index{ 0 };
+ if (!has_background && has_foreground)
+ {
+ color = foreground_color;
+ }
+ if (has_background && !has_foreground)
+ {
+ color = background_color;
+ }
+ if (has_background && has_foreground)
+ {
+ if (foreground.is_sprite_zero && x < 255)
+ {
+ status_.sprite_0_hit = true;
+ }
+
+ color = foreground.is_in_front ? foreground_color : background_color;
+ }
+
+ display_.set(x, y, resolve_color(get_color(color)));
+ }
+
+ auto ppu::fetch_name_table_byte() -> void
+ {
+ auto const addr = static_cast<std::uint16_t>(0x2000 | (internal_.v & 0x0FFF));
+ name_table_byte_ = read8(address{ addr });
+ }
+
+ auto ppu::fetch_attribute_table_byte() -> void
+ {
+ auto const v = internal_.v;
+ auto const addr = static_cast<std::uint16_t>(0x23C0 | (v & 0x0C00) | ((v >> 4) & 0x38) | ((v >> 2) & 0x07));
+ auto const shift = ((v >> 4) & 4) | (v & 2);
+ attribute_table_byte_ = static_cast<std::uint8_t>(((read8(address{ addr }) >> shift) & 3) << 2);
+ }
+
+ auto ppu::fetch_low_tile_byte() -> void
+ {
+ auto const fine_y = (internal_.v >> 12) & 7;
+ auto const table = control_.background_pattern_table;
+ auto const tile = name_table_byte_;
+ auto const addr = static_cast<std::uint16_t>(0x1000 * table + 16 * tile + fine_y);
+ low_tile_byte_ = read8(address{ addr });
+ }
+
+ auto ppu::fetch_high_tile_byte() -> void
+ {
+ auto const fine_y = (internal_.v >> 12) & 7;
+ auto const table = control_.background_pattern_table;
+ auto const tile = name_table_byte_;
+ auto const addr = static_cast<std::uint16_t>(0x1000 * table + 16 * tile + fine_y);
+ low_tile_byte_ = read8(address{ addr } + 8);
+ }
+
+ auto ppu::store_tile_data() -> void
+ {
+ auto data = std::uint32_t{ 0 };
+ for (auto i = unsigned{ 0 }; i < 8; ++i)
+ {
+ auto const p1 = (low_tile_byte_ & 0x80) >> 7;
+ auto const p2 = (high_tile_byte_ & 0x80) >> 6;
+ low_tile_byte_ <<= 1;
+ high_tile_byte_ <<= 1;
+ data <<= 4;
+ data |= attribute_table_byte_ | p1 | p2;
+ }
+ tile_data_ <<= 32;
+ tile_data_ |= data;
+ }
+
+ auto ppu::increment_x() -> void
+ {
+ // If coarse X == 31
+ if ((internal_.v & 0x1F) == 0x1F)
+ {
+ // Coarse X = 0
+ internal_.v &= ~0x1F;
+ // Switch horizontal nametable
+ internal_.v ^= 0x400;
+ }
+ else
+ {
+ // Increment coarse X
+ internal_.v += 1;
+ }
+ }
+
+ auto ppu::increment_y() -> void
+ {
+ // If fine Y == 7
+ if ((internal_.v & 0x7000) == 0x7000)
+ {
+ // Fine Y = 0
+ internal_.v &= 0x8FFF;
+ // Let y = coarse Y
+ auto y = (internal_.v & 0x03E0) >> 5;
+ if (y == 29)
+ {
+ // Coarse Y = 0
+ y = 0;
+ // Switch vertical nametable
+ internal_.v ^= 0x0800;
+ }
+ else if (y == 31)
+ {
+ // Coarse Y = 0, nametable not switched
+ y = 0;
+ }
+ else
+ {
+ // Increment coarse Y
+ y += 1;
+ }
+ // Put coarse Y back into v
+ internal_.v &= ~0x03E0;
+ internal_.v |= y << 5;
+ }
+ else
+ {
+ // Increment fine Y
+ internal_.v += 0x1000;
+ }
+ }
+
+ auto ppu::copy_x() -> void
+ {
+ internal_.v = (internal_.v & 0xFBE0) | (internal_.t & 0x041F);
+ }
+
+ auto ppu::copy_y() -> void
+ {
+ internal_.v = (internal_.v & 0x841F) | (internal_.t & 0x7BE0);
+ }
+
+ auto ppu::evaluate_sprites() -> void
+ {
+ auto const height = sprite_height();
+ sprite_count_ = 0;
+ for (auto i = unsigned{ 0 }; i < 64; ++i)
+ {
+ auto const y = oam_[i * 4 + 0];
+ auto const a = oam_[i * 4 + 2];
+ auto const x = oam_[i * 4 + 3];
+ auto const row = static_cast<int>(scanline_) - static_cast<int>(y);
+ if (row < 0 || row >= height) { continue; }
+
+ if (sprite_count_ < 8)
+ {
+ sprites_[sprite_count_].pattern = fetch_sprite_pattern(i, row);
+ sprites_[sprite_count_].position = x;
+ sprites_[sprite_count_].is_in_front = a & 0b00100000;
+ sprites_[sprite_count_].is_sprite_zero = i == 0;
+ sprite_count_ += 1;
+ }
+ else
+ {
+ status_.sprite_overflow = true;
+ break;
+ }
+ }
+ }
+
+ auto ppu::fetch_sprite_pattern(unsigned const i, unsigned row) -> std::uint32_t
+ {
+ auto tile = oam_[i * 4 + 1];
+ auto const attributes = oam_[i * 4 + 2];
+ auto const flip_vertical = (attributes & 0x80) == 0x80;
+ if (flip_vertical) { row = sprite_height() - 1 - row; }
+ auto addr = address{};
+ switch (control_.sprite_size)
+ {
+ case sprite_size::_8x8:
+ {
+ auto const table = control_.sprite_pattern_table;
+ addr = address{ static_cast<std::uint16_t>(0x1000 * table + 16 * tile + row) };
+ break;
+ }
+ case sprite_size::_8x16:
+ {
+ auto const table = tile & 1;
+ tile &= 0xFE;
+ if (row > 7)
+ {
+ tile += 1;
+ row -= 8;
+ }
+ addr = address{ static_cast<std::uint16_t>(0x1000 * table + 16 * tile + row) };
+ break;
+ }
+ }
+
+ auto const a = (attributes & 3) << 2;
+ auto low_tile_byte = read8(addr);
+ auto high_tile_byte = read8(addr + 8);
+ auto data = std::uint32_t{ 0 };
+ for (auto j = unsigned{ 0 }; j < 8; ++j)
+ {
+ auto const flip_horizontal = (attributes & 0x40) == 0x40;
+ auto const p1 = flip_horizontal ? (low_tile_byte & 1) << 0 : (low_tile_byte & 0x80) >> 7;
+ auto const p2 = flip_horizontal ? (high_tile_byte & 1) << 1 : (high_tile_byte & 0x80) >> 6;
+ low_tile_byte <<= 1;
+ high_tile_byte <<= 1;
+ data <<= 4;
+ data |= a | p1 | p2;
+ }
+
+ return data;
}
// -----------------------------------------------------------------------------------------------------------------
@@ -84,6 +452,7 @@ namespace nes
status_.vblank = false;
internal_.w = false;
+ nmi_change();
write_latch(res);
return res;
@@ -127,6 +496,7 @@ namespace nes
if (current_cycles_ > boot_up_cycles)
{
control_.value = value;
+ nmi_change();
}
}
@@ -219,6 +589,30 @@ namespace nes
// Helpers
// -----------------------------------------------------------------------------------------------------------------
+ auto ppu::nmi_change() -> void
+ {
+ auto const should_trigger = control_.vblank_nmi && status_.vblank;
+ if (!nmi_requested_ && should_trigger)
+ {
+ // TODO: HACK
+ nmi_delay_ = 15;
+ }
+ nmi_requested_ = should_trigger;
+ }
+
+ auto ppu::sprite_height() -> unsigned
+ {
+ switch (control_.sprite_size)
+ {
+ case sprite_size::_8x8:
+ return 8;
+ case sprite_size::_8x16:
+ return 16;
+ }
+
+ return 8;
+ }
+
auto ppu::increment_vram() -> void
{
switch (control_.vram_increment)
@@ -244,7 +638,7 @@ namespace nes
return palette_buffer_[index.value];
}
- auto ppu::resolve_color(color color) -> rgb
+ auto ppu::resolve_color(color const color) -> rgb
{
auto const index = static_cast<std::uint8_t>(color);
switch (index & 0x3F)
diff --git a/src/nes/ppu.hh b/src/nes/ppu.hh
index 1d08614..60784f7 100644
--- a/src/nes/ppu.hh
+++ b/src/nes/ppu.hh
@@ -7,6 +7,7 @@ namespace nes
class address;
class cpu;
class mapper;
+ class display;
class rgb;
class ppu
@@ -56,9 +57,18 @@ namespace nes
}
};
+ struct sprite
+ {
+ std::uint32_t pattern{ 0 };
+ std::uint8_t position{ 0 };
+ bool is_in_front{ false };
+ bool is_sprite_zero{ false };
+ };
+
cycle_count current_cycles_;
cpu& cpu_;
mapper& mapper_;
+ display& display_;
std::uint8_t vram_[vram_size]{};
std::uint8_t oam_[oam_size]{};
color palette_buffer_[palette_buffer_size]
@@ -127,9 +137,21 @@ namespace nes
} internal_{};
std::uint8_t oamaddr_{}; // Address accessed by IO registers
std::uint8_t ppudata_read_buffer_{}; // Delays PPUDATA reads by one.
+ unsigned scanline_{ 0 };
+ unsigned scanline_cycle_{ 0 };
+ bool even_frame_{ true };
+ std::uint64_t tile_data_{ 0 }; // Data for the current tile (higher 32 bits) and the next tile (lower 32 bits).
+ std::uint8_t name_table_byte_{ 0 };
+ std::uint8_t attribute_table_byte_{ 0 };
+ std::uint8_t low_tile_byte_{ 0 };
+ std::uint8_t high_tile_byte_{ 0 };
+ sprite sprites_[8]{}; // Evaluated sprites.
+ unsigned sprite_count_{ 0 }; // Number of evaluated sprites in sprites_.
+ unsigned nmi_delay_{ 0 }; // Delay until an NMI is requested if conditions still met.
+ bool nmi_requested_{ false };
public:
- explicit ppu(cpu& cpu, mapper& mapper);
+ explicit ppu(cpu&, mapper&, display&);
ppu(ppu const&) = delete;
ppu(ppu&&) = delete;
@@ -162,9 +184,23 @@ namespace nes
private:
auto step() -> void;
+ auto render_pixel() -> void;
+ auto fetch_name_table_byte() -> void;
+ auto fetch_attribute_table_byte() -> void;
+ auto fetch_low_tile_byte() -> void;
+ auto fetch_high_tile_byte() -> void;
+ auto store_tile_data() -> void;
+ auto increment_x() -> void;
+ auto increment_y() -> void;
+ auto copy_x() -> void;
+ auto copy_y() -> void;
+ auto evaluate_sprites() -> void;
+ auto fetch_sprite_pattern(unsigned i, unsigned row) -> std::uint32_t;
// Helpers
+ auto nmi_change() -> void;
+ auto sprite_height() -> unsigned;
auto increment_vram() -> void;
auto get_color(color_index) -> color&;
auto resolve_color(color) -> rgb;
diff --git a/src/scene.mm b/src/scene.mm
index ee2e2be..e4cb6b6 100644
--- a/src/scene.mm
+++ b/src/scene.mm
@@ -31,7 +31,10 @@ namespace
buffer_back_ = buffer_front_;
buffer_front_ = tmp;
bytes_back_ = static_cast<std::uint8_t*>([buffer_back_ mutableBytes]);
+ }
+ auto actually_switch_buffers() -> void
+ {
auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height)];
[texture setFilteringMode:SKTextureFilteringNearest];
[node_ setTexture:texture];
@@ -170,6 +173,7 @@ namespace
last_time_ = currentTime;
auto const delta_us = static_cast<std::uint64_t>((currentTime - last_time) * 1000. * 1000.);
self->nes_->step(std::chrono::microseconds{ delta_us });
+ self->display_->actually_switch_buffers();
}
@end \ No newline at end of file