srctree

Johan Norberg parent 25d2bad2 1a87a0bb
Render background in ppu

core/src/ppu.cpp added: 289, removed: 22, total 267
@@ -206,10 +206,12 @@ void Ppu::write_byte(uint16_t addr, uint8_t byte) {
} // namespace n_e_s::core
 
std::optional<Pixel> Ppu::execute() {
std::optional<Pixel> pixel;
 
if (is_pre_render_scanline()) {
execute_pre_render_scanline();
} else if (is_visible_scanline()) {
execute_visible_scanline();
pixel = execute_visible_scanline();
} else if (is_post_render_scanline()) {
execute_post_render_scanline();
} else if (is_vblank_scanline()) {
@@ -217,7 +219,7 @@ std::optional<Pixel> Ppu::execute() {
}
 
update_counters();
return {};
return pixel;
}
 
void Ppu::set_nmi_handler(const std::function<void()> &on_nmi) {
@@ -302,9 +304,10 @@ void Ppu::execute_pre_render_scanline() {
}
}
 
void Ppu::execute_visible_scanline() {
std::optional<Pixel> Ppu::execute_visible_scanline() {
fetch();
increase_scroll_counters();
return pixel();
}
 
void Ppu::execute_post_render_scanline() {
@@ -455,6 +458,60 @@ void Ppu::fetch() {
}
}
 
std::optional<Pixel> Ppu::pixel() {
const bool visible_cycle = cycle() >= 1u && cycle() <= 256u;
if (!visible_cycle) {
return std::nullopt;
}
 
uint8_t background_pixel = 0u;
uint8_t background_palette = 0u;
 
// The first 8 pixels should be skipped if "render background left" is
// not set
const bool is_background_visible =
registers_->mask.render_background() &&
(registers_->mask.render_background_left() || cycle() > 8u);
 
if (is_background_visible) {
const auto fine_x = registers_->fine_x_scroll;
const uint16_t horizontal_pixel_index = 0x8000u >> fine_x;
 
const uint8_t low_pixel = (registers_->pattern_table_shifter_low &
horizontal_pixel_index) > 0u;
const uint8_t high_pixel = (registers_->pattern_table_shifter_hi &
horizontal_pixel_index) > 0u;
background_pixel = static_cast<uint8_t>(high_pixel << 1u) | low_pixel;
 
const uint8_t low_attribute = (registers_->attribute_table_shifter_low &
horizontal_pixel_index) > 0u;
const uint8_t high_attribute = (registers_->attribute_table_shifter_hi &
horizontal_pixel_index) > 0u;
background_palette =
static_cast<uint8_t>(high_attribute << 1u) | low_attribute;
}
// TODO(JN): render sprites and implement sprite priority.
uint8_t combined_pixel = background_pixel;
uint8_t combined_palette = background_palette;
 
if (background_pixel == 0u) {
// Both are transparent, output background ($3F00).
combined_pixel = 0u;
combined_palette = 0u;
}
 
// Each palette is 4 bytes and starts at 0x3F00.
const uint16_t color_address =
0x3F00u + combined_palette * 4u + combined_pixel;
// TODO(JN): Handle greyscale.
const uint8_t palette_index = mmu_->read_byte(color_address);
const auto color = get_color_from_palette_index(palette_index);
 
const auto x = static_cast<uint8_t>(cycle() - 1u);
const auto y = static_cast<uint8_t>(scanline());
return Pixel{.x = x, .y = y, .color = color};
}
 
Color Ppu::get_color_from_palette_index(uint8_t index) const {
return kPalette[index % kPalette.size()];
}
 
core/src/ppu.h added: 289, removed: 22, total 267
@@ -58,7 +58,7 @@ private:
void increment_vram_address();
 
void execute_pre_render_scanline();
void execute_visible_scanline();
std::optional<Pixel> execute_visible_scanline();
void execute_post_render_scanline();
void execute_vblank_scanline();
 
@@ -68,6 +68,7 @@ private:
void shift_registers();
void increase_scroll_counters();
void fetch();
std::optional<Pixel> pixel();
 
Color get_color_from_palette_index(uint8_t index) const;
};
 
core/test/src/test_ppu.cpp added: 289, removed: 22, total 267
@@ -5,6 +5,7 @@
#include "nes/core/test/mock_mmu.h"
 
#include <gtest/gtest.h>
#include <array>
 
using namespace n_e_s::core;
using namespace n_e_s::core::test;
@@ -25,7 +26,17 @@ public:
 
void step_execution(uint32_t cycles) {
for (uint32_t i = 0; i < cycles; ++i) {
EXPECT_FALSE(ppu->execute().has_value());
ppu->execute();
}
}
 
void step_execution(uint32_t cycles, bool expect_pixel) {
for (uint32_t i = 0; i < cycles; ++i) {
if (expect_pixel) {
EXPECT_TRUE(ppu->execute().has_value());
} else {
EXPECT_TRUE(ppu->execute().has_value());
}
}
}
 
@@ -458,10 +469,173 @@ TEST_F(PpuTest, skips_first_cycle_on_odd_frames_when_rendering_is_enabled) {
EXPECT_EQ(expected, registers);
}
 
TEST_F(PpuTest, render_one_pixel) {
registers.cycle = 1;
registers.mask = expected.mask = PpuMask(0x1E); // Enable all rendering.
// Shift registers will be shifted left once before
// choosing bits to be rendered.
// Final pixel value (from second highest bits here): 1*2 + 1 = 3;
registers.pattern_table_shifter_hi = 0b0110'0000'0000'0000u;
registers.pattern_table_shifter_low = 0b0100'0000'0000'0000u;
registers.attribute_table_shifter_hi = 0x0000u;
registers.attribute_table_shifter_low = 0xFFFFu;
 
// Nametable fetch
EXPECT_CALL(mmu, read_byte(0x2000)).WillOnce(Return(0xFF));
 
const auto expected_pixel =
Pixel{.x = 0u, .y = 0u, .color = Color{8, 16, 144}}; // Second color
 
// Palette index
// Background palette 1, third color (from pixel value in tile).
// Return second color.
EXPECT_CALL(mmu, read_byte(0x3F00 + 1u * 4u + 3u))
.WillRepeatedly(Return(0x02));
 
const auto pixel = ppu->execute();
EXPECT_TRUE(pixel.has_value());
EXPECT_EQ(expected_pixel, *pixel);
}
 
TEST_F(PpuTest, render_one_pixel_with_fine_x_scrolling) {
registers.cycle = 1;
registers.mask = expected.mask = PpuMask(0x1E); // Enable all rendering.
registers.fine_x_scroll = 7u;
// Fine x scroll is set to 7, so the ninth bits will be used for rendering.
// Shift registers will be shifted left once before
// choosing bit to be rendered.
// Final pixel value (from eighth bit here): 0*2 + 1 = 1;
registers.pattern_table_shifter_hi = 0b0100'0000'0000'0000u;
registers.pattern_table_shifter_low = 0b0100'0000'1000'0000u;
registers.attribute_table_shifter_hi = 0x0000;
registers.attribute_table_shifter_low = 0xFFFF;
 
// Nametable fetch
EXPECT_CALL(mmu, read_byte(0x2000)).WillOnce(Return(0xFF));
 
const auto expected_pixel = Pixel{
.x = 0u, .y = 0u, .color = Color{8, 16, 144}}; // Second color.
 
// Palette index
// Background palette 1, first color (from pixel value in tile).
// Return second color.
EXPECT_CALL(mmu, read_byte(0x3F00 + 1u * 4u + 1u))
.WillRepeatedly(Return(0x02));
 
const auto pixel = ppu->execute();
EXPECT_TRUE(pixel.has_value());
EXPECT_EQ(expected_pixel, *pixel);
}
 
TEST_F(PpuTest, render_one_tile) {
registers.mask = expected.mask = PpuMask(0x1E); // Enable all rendering
registers.cycle = 1;
registers.vram_addr = PpuVram(0x0002);
 
registers.pattern_table_shifter_hi = 0b1111'1111'1111'1111;
registers.pattern_table_shifter_low = 0b0000'0000'0000'0000u;
registers.attribute_table_shifter_hi = 0x0000;
registers.attribute_table_shifter_low = 0xFFFF;
 
{
testing::InSequence seq;
// Next tile
// Nametable
EXPECT_CALL(mmu, read_byte(0x2002)).WillOnce(Return(0x03));
// Attribute
EXPECT_CALL(mmu, read_byte(0x23C0)).WillOnce(Return(0x00));
 
// Pattern table low
EXPECT_CALL(mmu, read_byte(0x03 * 16u)).WillOnce(Return(0x00));
// Pattern table high
EXPECT_CALL(mmu, read_byte(0x03 * 16u + 8u)).WillOnce(Return(0x00));
}
 
constexpr std::array kExpectedPixels = {
Pixel{.x = 0u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 1u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 2u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 3u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 4u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 5u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 6u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 7u, .y = 0u, .color = Color{152, 150, 152}},
Pixel{.x = 8u, .y = 0u, .color = Color{152, 150, 152}},
};
 
// Palette index
EXPECT_CALL(mmu, read_byte(0x3F00 + 1u * 4u + 2u))
.WillRepeatedly(Return(0x10));
 
for (int i = 0; i < 8; ++i) {
const auto pixel = ppu->execute();
EXPECT_TRUE(pixel.has_value());
const Pixel expected_pixel = kExpectedPixels.at(i);
EXPECT_EQ(expected_pixel, *pixel);
}
 
expected.scanline = 0;
expected.cycle = 9;
expected.vram_addr = PpuVram(0x0003);
expected.name_table = 0x00;
expected.name_table_latch = 0x03;
expected.pattern_table_shifter_hi = 0x8000;
expected.pattern_table_latch_hi = 0x00;
expected.pattern_table_shifter_low = 0x0000;
expected.pattern_table_latch_low = 0x00;
expected.attribute_table_shifter_low = 0x8000;
expected.attribute_table_shifter_hi = 0x0000;
expected.attribute_table_latch = 0x00;
 
EXPECT_EQ(expected, registers);
}
 
TEST_F(PpuTest, visible_scanline_output_background_if_disabled) {
registers.mask = expected.mask = PpuMask(0x00); // Disable all rendering
registers.cycle = 1;
 
EXPECT_CALL(mmu, read_byte(testing::_)).WillRepeatedly(Return(0x00));
EXPECT_CALL(mmu, read_byte(0x3F00)).WillRepeatedly(Return(0x05));
 
for (int i = 0; i < 50; ++i) {
const auto pixel = ppu->execute();
EXPECT_TRUE(pixel.has_value());
const auto expected_color = Color{92, 0, 48}; // 5th color.
EXPECT_EQ(expected_color, pixel->color);
}
}
 
TEST_F(PpuTest, visible_scanline_output_background_left_disabled) {
// Render background, but not first 8 pixels.
registers.mask = expected.mask = PpuMask(0b0000'1000);
registers.cycle = 1;
registers.pattern_table_shifter_hi = 0b1111'1111'1111'1111;
registers.pattern_table_shifter_low = 0b0000'0000'0000'0000u;
registers.pattern_table_latch_hi = 0xFF;
registers.attribute_table_shifter_hi = 0xFFFF;
registers.attribute_table_shifter_low = 0xFFFF;
registers.attribute_table_latch = 0xFF;
 
EXPECT_CALL(mmu, read_byte(testing::_)).WillRepeatedly(Return(0xFF));
EXPECT_CALL(mmu, read_byte(0x3F00)).WillRepeatedly(Return(0x05));
// Third palette (from attribute)
EXPECT_CALL(mmu, read_byte(0x3F00 + 3u * 4u + 2u))
.WillRepeatedly(Return(0x06));
 
for (int i = 0; i < 10; ++i) {
const auto pixel = ppu->execute();
EXPECT_TRUE(pixel.has_value());
// Return 5th color first 8 pixels (background from 0x3F00)
// and 6th color the following pixels.
const auto expected_color = i < 8 ? Color{92, 0, 48} : Color{84, 4, 0};
EXPECT_EQ(expected_color, pixel->color);
}
}
 
TEST_F(PpuTest, visible_two_sub_cycles) {
registers.scanline = expected.scanline = 0;
registers.mask = expected.mask =
PpuMask(0b000'1000); // Enable background rendering
PpuMask(0x1E); // Enable background rendering
 
expected.cycle = 17;
// Vram should be increased at cycle 8 and 16
@@ -509,9 +683,19 @@ TEST_F(PpuTest, visible_two_sub_cycles) {
EXPECT_CALL(mmu, read_byte(0x03 * 16u + 8u)).WillOnce(Return(0x99));
}
 
// Palette index
EXPECT_CALL(mmu, read_byte(0x3F00)).WillRepeatedly(Return(0x00));
EXPECT_CALL(mmu, read_byte(0x3F01)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F02)).WillRepeatedly(Return(0x02));
EXPECT_CALL(mmu, read_byte(0x3F03)).WillRepeatedly(Return(0x03));
 
for (int i = 0; i < 17; ++i) {
const auto pixel = ppu->execute();
EXPECT_FALSE(pixel.has_value());
if (i == 0) {
EXPECT_FALSE(pixel.has_value());
} else {
EXPECT_TRUE(pixel.has_value());
}
}
 
EXPECT_EQ(expected, registers);
@@ -520,7 +704,7 @@ TEST_F(PpuTest, visible_two_sub_cycles) {
TEST_F(PpuTest, visible_scanline) {
registers.scanline = 0u; // Start at visible scanline
registers.mask = expected.mask =
PpuMask(0b000'1000); // Enable background rendering
PpuMask(0b0000'1000); // Enable background rendering
 
expected.cycle = 257;
expected.scanline = 0u;
@@ -542,6 +726,21 @@ TEST_F(PpuTest, visible_scanline) {
ppu->write_byte(0x2005, 0);
ppu->write_byte(0x2005, 0);
 
// Palette index
EXPECT_CALL(mmu, read_byte(0x3F00)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F01)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F02)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F03)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F05)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F06)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F07)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F09)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F0A)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F0B)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F0D)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F0E)).WillRepeatedly(Return(0x01));
EXPECT_CALL(mmu, read_byte(0x3F0F)).WillRepeatedly(Return(0x01));
 
// Nametables
for (int i = 0; i < 32; ++i) {
EXPECT_CALL(mmu, read_byte(0x2000 + i)).WillOnce(Return(i));
@@ -563,7 +762,11 @@ TEST_F(PpuTest, visible_scanline) {
 
for (int i = 0; i <= 256; ++i) {
const auto pixel = ppu->execute();
EXPECT_FALSE(pixel.has_value());
if (i == 0) {
EXPECT_FALSE(pixel.has_value());
} else {
EXPECT_TRUE(pixel.has_value());
}
}
EXPECT_EQ(expected, registers);
 
@@ -632,7 +835,7 @@ TEST_F(PpuTest, visible_scanline) {
TEST_F(PpuTest, pre_render_two_sub_cycles) {
registers.scanline = expected.scanline = 261; // Start at pre-render
registers.mask = expected.mask =
PpuMask(0b000'1000); // Enable background rendering
PpuMask(0b0000'1000); // Enable background rendering
 
expected.cycle = 17;
// Vram should be increased at cycle 8 and 16
@@ -694,7 +897,7 @@ TEST_F(PpuTest, pre_render_scanline) {
registers.scanline = 261u; // Start at pre-render
registers.odd_frame = expected.odd_frame = true;
registers.mask = expected.mask =
PpuMask(0b000'1000); // Enable background rendering
PpuMask(0b0000'1000); // Enable background rendering
 
expected.cycle = 257;
expected.scanline = 261u;
 
core/test/src/test_rom.cpp added: 289, removed: 22, total 267
@@ -1,3 +1,4 @@
#include "ippu_helpers.h"
#include "nes/core/irom.h"
#include "nes/core/rom_factory.h"
 
 
nes/CMakeLists.txt added: 289, removed: 22, total 267
@@ -26,8 +26,9 @@ set_target_properties(${PROJECT_NAME}
)
 
target_link_libraries(${PROJECT_NAME}
PRIVATE
PUBLIC
n_e_s::core
PRIVATE
n_e_s::warnings
)
 
 
nes/include/nes/nes.h added: 289, removed: 22, total 267
@@ -1,8 +1,11 @@
#pragma once
 
#include <memory>
#include <optional>
#include <string>
 
#include "nes/core/pixel.h"
 
namespace n_e_s::core {
class IMos6502;
struct CpuRegisters;
@@ -26,7 +29,7 @@ public:
~Nes();
 
// Run at 263.25 / 11 Mhz for NTSC "realtime."
void execute();
std::optional<core::Pixel> execute();
void reset();
void load_rom(std::istream &bytestream);
 
 
nes/src/nes.cpp added: 289, removed: 22, total 267
@@ -63,16 +63,17 @@ Nes::Nes()
Nes::~Nes() = default;
 
// https://wiki.nesdev.com/w/index.php/Cycle_reference_chart#Clock_rates
void Nes::execute() {
std::optional<core::Pixel> Nes::execute() {
if (cycle_++ % 12 == 0) {
cpu_->execute();
}
 
if (cycle_ % 4 == 0) {
ppu_->execute();
return ppu_->execute();
}
 
// The APU runs at master clock % 24. (every other CPU tick)
return {};
}
 
void Nes::reset() {