diff options
| -rw-r--r-- | src/nes/controller.cc | 11 | ||||
| -rw-r--r-- | src/nes/controller.hh | 2 | ||||
| -rw-r--r-- | src/nes/cpu.cc | 21 | ||||
| -rw-r--r-- | src/nes/cpu.hh | 5 | ||||
| -rw-r--r-- | src/nes/nes.cc | 9 | ||||
| -rw-r--r-- | src/nes/nes.hh | 1 | ||||
| -rw-r--r-- | src/nes/ppu.cc | 402 | ||||
| -rw-r--r-- | src/nes/ppu.hh | 38 | ||||
| -rw-r--r-- | src/scene.mm | 4 |
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 |
