srctree

Johan Norberg parent e3b8722c 9046091e
Implement pre-render scanline in ppu

core/include/nes/core/ppu_registers.h added: 334, removed: 4, total 330
@@ -16,6 +16,18 @@ struct PpuRegisters {
uint16_t temp_vram_addr;
bool write_toggle;
 
uint8_t name_table_latch;
uint8_t name_table;
 
uint8_t pattern_table_latch_low;
uint8_t pattern_table_latch_hi;
uint16_t pattern_table_shifter_low;
uint16_t pattern_table_shifter_hi;
 
uint8_t attribute_table_latch;
uint16_t attribute_table_shifter_low;
uint16_t attribute_table_shifter_hi;
 
[[nodiscard]] constexpr bool is_rendering_enabled() const {
return (mask & (1u << 3u)) || (mask & (1u << 4u));
}
 
core/src/ppu.cpp added: 334, removed: 4, total 330
@@ -22,6 +22,76 @@ const uint16_t kPostRenderScanline = 240;
const uint16_t kVBlankScanlineStart = 241;
const uint16_t kVBlankScanlineEnd = 260;
 
constexpr uint16_t increase_vram_coarse_x(uint16_t vram_addr) {
// From http://wiki.nesdev.com/w/index.php/PPU_scrolling
if ((vram_addr & 0x001Fu) == 31u) {
vram_addr &= ~0x001Fu; // coarse X = 0
vram_addr ^= 0x0400u; // switch horizontal nametable
} else {
vram_addr += 1u;
}
return vram_addr;
}
 
constexpr uint16_t increase_vram_y(uint16_t vram_addr) {
// From http://wiki.nesdev.com/w/index.php/PPU_scrolling
if ((vram_addr & 0x7000u) != 0x7000u) {
// if fine Y < 7
vram_addr += 0x1000u;
} else {
vram_addr &= ~0x7000u; // fine Y = 0
uint16_t y = (vram_addr & 0x03E0u) >> 5u; // let y = coarse Y
if (y == 29u) {
y = 0u; // coarse Y = 0
vram_addr ^= 0x0800u; // switch vertical nametable
} else if (y == 31u) {
y = 0u; // coarse Y = 0, nametable not switched
} else {
y += 1u; // increment coarse Y
}
vram_addr = static_cast<uint16_t>(
vram_addr & static_cast<uint16_t>(~0x03E0u)) |
static_cast<uint16_t>(y << 5u); // put coarse Y back into v
}
return vram_addr;
}
 
// From http://wiki.nesdev.com/w/index.php/PPU_scrolling
// The 15 bit registers t and v are composed this way during rendering:
// yyy NN YYYYY XXXXX
// ||| || ||||| +++++-- coarse X scroll
// ||| || +++++-------- coarse Y scroll
// ||| ++-------------- nametable select
// +++----------------- fine Y scroll
constexpr uint8_t get_fine_scroll_y(uint16_t vram_addr) {
uint8_t fine_scroll = static_cast<uint8_t>(vram_addr >> 12u) & 0b0000'0111u;
return fine_scroll;
}
 
constexpr uint8_t get_coarse_scroll_y(uint16_t vram_addr) {
uint8_t y = static_cast<uint8_t>(vram_addr >> 5u) & 0b0001'1111u;
return y;
}
 
constexpr uint8_t get_coarse_scroll_x(uint16_t vram_addr) {
uint8_t x = vram_addr & 0b0001'1111u;
return x;
}
 
// From
// https://wiki.nesdev.org/w/index.php/PPU_scrolling#Tile_and_attribute_fetching
uint16_t get_nametable_address(uint16_t vram_addr) {
return 0x2000u | (vram_addr & 0x0FFFu);
}
 
// From
// https://wiki.nesdev.org/w/index.php/PPU_scrolling#Tile_and_attribute_fetching
constexpr uint16_t get_attribute_address(uint16_t vram_addr) {
return 0x23C0u | (vram_addr & 0x0C00u) |
(static_cast<uint16_t>(vram_addr >> 4u) & 0x38u) |
(static_cast<uint16_t>(vram_addr >> 2u) & 0x07u);
}
 
} // namespace
 
namespace n_e_s::core {
@@ -196,6 +266,9 @@ void Ppu::increment_vram_address() {
}
 
void Ppu::execute_pre_render_scanline() {
fetch();
shift_registers();
increase_scroll_counters();
if (cycle() == 1) {
clear_vblank_flag();
}
@@ -222,4 +295,119 @@ void Ppu::clear_vblank_flag() {
registers_->status &= ~(1u << 7u);
}
 
void Ppu::shift_registers() {
registers_->pattern_table_shifter_low <<= 1u;
registers_->pattern_table_shifter_hi <<= 1u;
 
registers_->attribute_table_shifter_low <<= 1u;
registers_->attribute_table_shifter_hi <<= 1u;
}
 
void Ppu::increase_scroll_counters() {
if (!registers_->is_rendering_enabled()) {
return;
}
 
// During pixels 280 through 304 of this scanline, the vertical scroll bits
// are reloaded if rendering is enabled. vert(v) == vert(t)
if (scanline() == kPreRenderScanline) {
if (cycle() >= 280 && cycle() <= 304) {
registers_->vram_addr &= static_cast<uint16_t>(~0x7BE0u);
registers_->vram_addr |= (registers_->temp_vram_addr & 0x7BE0u);
}
}
 
const bool should_increase_coarse_x =
!(cycle() == 0 || (cycle() >= 256 && cycle() <= 320));
if (should_increase_coarse_x && (cycle() % 8) == 0) {
registers_->vram_addr = increase_vram_coarse_x(registers_->vram_addr);
}
 
if (cycle() == 256) {
registers_->vram_addr = increase_vram_y(registers_->vram_addr);
} else if (cycle() == 257) {
// Copies all bits related to horizontal position from
// temporal to current vram_address
// From http://wiki.nesdev.com/w/index.php/PPU_scrolling
// If rendering is enabled, the PPU copies all bits related to
// horizontal position from t to v:
registers_->vram_addr &= ~0x41Fu;
registers_->vram_addr |= registers_->temp_vram_addr & 0x41Fu;
}
}
void Ppu::fetch() {
if ((cycle() >= 1 && cycle() <= 256) ||
(cycle() >= 321 && cycle() <= 336)) {
const uint16_t background_pattern_table_base_address =
(registers_->ctrl & 0b0001'0000u) ? 0x1000u : 0x0000u;
const uint8_t fine_scroll_y = get_fine_scroll_y(registers_->vram_addr);
 
switch ((cycle() - 1) % 8) {
case 0: {
registers_->pattern_table_shifter_low =
(registers_->pattern_table_shifter_low & 0xFF00u) |
registers_->pattern_table_latch_low;
registers_->pattern_table_shifter_hi =
(registers_->pattern_table_shifter_hi & 0xFF00u) |
registers_->pattern_table_latch_hi;
 
registers_->attribute_table_shifter_low =
(registers_->attribute_table_shifter_low & 0xFF00u) |
((registers_->attribute_table_latch & 0b01u) ? 0xFFu
: 0x00u);
registers_->attribute_table_shifter_hi =
(registers_->attribute_table_shifter_hi & 0xFF00u) |
((registers_->attribute_table_latch & 0b10u) ? 0xFFu
: 0x00u);
 
registers_->name_table = registers_->name_table_latch;
const uint16_t nametable_address =
get_nametable_address(registers_->vram_addr);
registers_->name_table_latch = mmu_->read_byte(nametable_address);
break;
}
case 2: {
const uint16_t attribute_address =
get_attribute_address(registers_->vram_addr);
uint8_t byte = mmu_->read_byte(attribute_address);
// Figure out which quadrant we are in and get the two corresponding
// bits.
// 7654 3210
// |||| ||++- Color bits 3-2 for top left quadrant of this byte
// |||| ++--- Color bits 3-2 for top right quadrant of this byte
// ||++------ Color bits 3-2 for bottom left quadrant of this byte
// ++-------- Color bits 3-2 for bottom right quadrant of this byte
const uint8_t coarse_y = get_coarse_scroll_y(registers_->vram_addr);
const uint8_t coarse_x = get_coarse_scroll_x(registers_->vram_addr);
if (coarse_y % 4u >= 2u) {
// We are in the bottom quadrant
byte >>= 4u;
}
if (coarse_x % 4u >= 2u) {
// We are in the right quadrant
byte >>= 2u;
}
byte &= 0x0000'0003u;
registers_->attribute_table_latch = byte;
break;
}
case 4: {
const uint16_t index = background_pattern_table_base_address +
(registers_->name_table_latch * 16u) +
fine_scroll_y;
registers_->pattern_table_latch_low = mmu_->read_byte(index);
break;
}
case 6: {
const uint16_t index = background_pattern_table_base_address +
(registers_->name_table_latch * 16u) +
fine_scroll_y;
// +8 for next bit plane
registers_->pattern_table_latch_hi = mmu_->read_byte(index + 0x8u);
break;
}
};
}
}
 
} // namespace n_e_s::core
 
core/src/ppu.h added: 334, removed: 4, total 330
@@ -64,6 +64,10 @@ private:
 
void set_vblank_flag();
void clear_vblank_flag();
 
void shift_registers();
void increase_scroll_counters();
void fetch();
};
 
} // namespace n_e_s::core
 
core/test/src/test_ppu.cpp added: 334, removed: 4, total 330
@@ -360,4 +360,130 @@ TEST_F(PpuTest, increment_vram_addr_by_32_after_reading) {
EXPECT_EQ(expected, registers);
}
 
TEST_F(PpuTest, pre_render_two_sub_cycles) {
registers.scanline = expected.scanline = 261; // Start at pre-render
registers.mask = expected.mask = 0b000'1000; // Enable background rendering
 
expected.cycle = 17;
// Vram should be increased at cycle 8 and 16
expected.vram_addr = 0x0002;
 
// Clear scrolling
ppu->write_byte(0x2005, 0);
ppu->write_byte(0x2005, 0);
 
// Nametable on cycle 1 set to index 2, and index 3 on cycle 9
EXPECT_CALL(mmu, read_byte(0x2000)).WillOnce(Return(0x02));
EXPECT_CALL(mmu, read_byte(0x2001)).WillOnce(Return(0x03));
 
// Attribyte on cycle 3 and 12.
// Second tile has the same attribute byte as the first.
EXPECT_CALL(mmu, read_byte(0x23C0)).Times(2).WillRepeatedly(Return(0xAB));
 
// Pattern table low byte on cycle 5.
// Each tile uses 8 bytes. So tile with index 2 starts at address 16*2.
EXPECT_CALL(mmu, read_byte(0x02 * 16u)).WillOnce(Return(0x80));
// Pattern table high byte on cycle 7
// Second bit plane address is 8 bytes after the first bit plane.
EXPECT_CALL(mmu, read_byte(0x02 * 16u + 8u)).WillOnce(Return(0x99));
 
// Pattern table low byte on cycle 13.
EXPECT_CALL(mmu, read_byte(0x03 * 16u)).WillOnce(Return(0x80));
// Pattern table high byte on cycle 15
EXPECT_CALL(mmu, read_byte(0x03 * 16u + 8u)).WillOnce(Return(0x99));
 
for (int i = 0; i < 17; ++i) {
ppu->execute();
}
 
EXPECT_EQ(expected, registers);
}
 
TEST_F(PpuTest, pre_render_scaneline) {
registers.scanline = 261u; // Start at pre-render
registers.mask = expected.mask = 0b000'1000; // Enable background rendering
 
expected.cycle = 257;
expected.scanline = 261u;
// vram addr: yyy NN YYYYY XXXXX
// Fine scroll should be increase once, and coarse x for each tile except
// the last (31x).
expected.vram_addr = 0b001'00'00000'11111;
 
// Clear scrolling
ppu->write_byte(0x2005, 0);
ppu->write_byte(0x2005, 0);
 
// Nametables
for (int i = 0; i < 32; ++i) {
EXPECT_CALL(mmu, read_byte(0x2000 + i)).WillOnce(Return(i));
}
 
// Attributes. Each address should be fetched four times since each byte
// controlls 4x4 tiles.
for (int i = 0; i < 8; ++i) {
EXPECT_CALL(mmu, read_byte(0x23C0 + i))
.Times(4)
.WillRepeatedly(Return(i));
}
 
// Background
for (int i = 0; i < 32; ++i) {
EXPECT_CALL(mmu, read_byte(i * 16u)).WillOnce(Return(i));
EXPECT_CALL(mmu, read_byte(i * 16u + 8u)).WillOnce(Return(i + 0xF0));
}
 
for (int i = 0; i <= 256; ++i) {
ppu->execute();
}
EXPECT_EQ(expected, registers);
 
// During cycle 257 the horizontal bits should be reloaded.
expected.vram_addr = 0b001'00'00000'00000;
expected.cycle = 258;
 
ppu->execute(); // Cycle 257
EXPECT_EQ(expected, registers);
 
// Cycle 258-321
// During cycle 280-304 the vertical scroll bits should be reloaded.
expected.vram_addr = 0b000'00'00000'00000;
expected.cycle = 321;
for (int i = 258; i <= 320; ++i) {
ppu->execute();
}
EXPECT_EQ(expected, registers);
 
// Cycle 322-336.
// Fetch first two tiles for next scanline.
expected.vram_addr = 0b000'00'00000'00010;
expected.cycle = 337;
// Nametables
EXPECT_CALL(mmu, read_byte(0x2000)).WillOnce(Return(0x02));
EXPECT_CALL(mmu, read_byte(0x2001)).WillOnce(Return(0x03));
 
// Attributes
EXPECT_CALL(mmu, read_byte(0x23C0)).Times(2).WillRepeatedly(Return(0xAB));
 
// Background
EXPECT_CALL(mmu, read_byte(0x02 * 16u)).WillOnce(Return(0x80));
EXPECT_CALL(mmu, read_byte(0x02 * 16u + 8u)).WillOnce(Return(0x99));
EXPECT_CALL(mmu, read_byte(0x03 * 16u)).WillOnce(Return(0x80));
EXPECT_CALL(mmu, read_byte(0x03 * 16u + 8u)).WillOnce(Return(0x99));
 
for (int i = 321; i <= 336; ++i) {
ppu->execute();
}
EXPECT_EQ(expected, registers);
 
// Finally cycle 337-340.
// Two unused nametable fetches (not implemented).
expected.scanline = 0;
expected.cycle = 0;
for (int i = 337; i <= 340; ++i) {
ppu->execute();
}
EXPECT_EQ(expected, registers);
}
 
} // namespace