srctree

Johan parent 8d11c46d ef2ef1e6 760fd7ef
Merge pull request #360 from johnor/mapper-2

Mapper 2 and improve rom tests
core/CMakeLists.txt added: 354, removed: 37, total 317
@@ -41,6 +41,8 @@ add_library(${PROJECT_NAME}
src/ppu_factory.cpp
src/rom/nrom.cpp
src/rom/nrom.h
src/rom/mapper_2.cpp
src/rom/mapper_2.h
src/rom_factory.cpp
)
add_library(n_e_s::core ALIAS ${PROJECT_NAME})
 
filename was Deleted added: 354, removed: 37, total 317
@@ -0,0 +1,127 @@
#include "rom/mapper_2.h"
 
#include <cstddef>
#include <stdexcept>
#include <string>
#include "nes/core/ines_header.h"
#include "nes/core/invalid_address.h"
 
namespace n_e_s::core {
 
Mapper2::Mapper2(const INesHeader &h,
std::vector<uint8_t> prg_rom,
std::vector<uint8_t> chr_mem)
: IRom(h),
select_bank_hi_(h.prg_rom_size - 1),
prg_rom_(std::move(prg_rom)),
chr_mem_(std::move(chr_mem)),
nametables_{} {
if (prg_rom_.size() !=
static_cast<std::size_t>(16u * 1024u * h.prg_rom_size)) {
throw std::invalid_argument("Invalid prg_rom size");
}
 
if (chr_mem_.size() != static_cast<std::size_t>(8u * 1024u)) {
throw std::invalid_argument("Invalid chr_ram size");
}
}
 
bool Mapper2::is_cpu_address_in_range(uint16_t addr) const {
const bool in_prg = addr >= 0x8000;
return in_prg;
}
 
uint8_t Mapper2::cpu_read_byte(uint16_t addr) const {
if (addr >= kSwitchablePrgRomStart && addr <= kSwitchablePrgRomEnd) {
const uint32_t mapped_addr =
select_bank_low_ * 0x4000u + (addr & 0x3FFFu);
return prg_rom_[mapped_addr];
}
 
if (addr >= kLastBankPrgRomStart) {
const uint32_t mapped_addr =
select_bank_hi_ * 0x4000u + (addr & 0x3FFFu);
return prg_rom_[mapped_addr];
}
 
throw InvalidAddress(addr);
}
 
void Mapper2::cpu_write_byte(uint16_t addr, uint8_t byte) {
if (addr >= kSwitchablePrgRomStart) {
select_bank_low_ = byte & 0x0Fu;
}
}
 
bool Mapper2::is_ppu_address_in_range(uint16_t addr) const {
const bool in_chr = addr <= kChrEnd;
const bool in_nametable = addr >= kNametableStart && addr <= kNametableEnd;
return in_chr || in_nametable;
}
 
uint8_t Mapper2::ppu_read_byte(uint16_t addr) const {
if (addr <= kChrEnd) {
return chr_mem_.at(addr);
}
const auto [index, addr_mod] =
translate_nametable_addr(addr, header().mirroring());
return nametables_[index][addr_mod];
}
 
void Mapper2::ppu_write_byte(uint16_t addr, uint8_t byte) {
if (addr <= kChrEnd) {
chr_mem_.at(addr) = byte;
}
const auto [index, addr_mod] =
translate_nametable_addr(addr, header().mirroring());
nametables_[index][addr_mod] = byte;
}
 
std::pair<int, uint16_t> Mapper2::translate_nametable_addr(uint16_t addr,
Mirroring m) const {
// TODO(johnor): This logic is identical to mapper 0 (Nrom).
// Refactor and move outside the mapper.
 
// Nametables
// Range Size Desc
// $2000-$23FF $0400 Nametable 0
// $2400-$27FF $0400 Nametable 1
// $2800-$2BFF $0400 Nametable 2
// $2C00-$2FFF $0400 Nametable 3
// $3000-$3EFF $0F00 Mirrors of $2000-$2EFF
 
// Ignore top 4 bits to handle mirroring
addr &= 0x0FFFu;
if (m == Mirroring::Horizontal) {
// Nametable 0 and 1 should be the same
if (addr <= 0x03FF) {
return {0, addr % 0x0400};
}
if (addr >= 0x0400 && addr <= 0x07FF) {
return {0, addr % 0x0400};
}
if (addr >= 0x0800 && addr <= 0x0BFF) {
return {1, addr % 0x0400};
}
if (addr >= 0x0C00 && addr <= 0x0FFF) {
return {1, addr % 0x0400};
}
} else if (header().mirroring() == Mirroring::Vertical) {
// Nametable 0 and 3 should be the same
if (addr <= 0x03FF) {
return {0, addr % 0x0400};
}
if (addr >= 0x0400 && addr <= 0x07FF) {
return {1, addr % 0x0400};
}
if (addr >= 0x0800 && addr <= 0x0BFF) {
return {0, addr % 0x0400};
}
if (addr >= 0x0C00 && addr <= 0x0FFF) {
return {1, addr % 0x0400};
}
}
throw std::runtime_error("Unknown address: " + std::to_string(addr));
}
 
} // namespace n_e_s::core
 
filename was Deleted added: 354, removed: 37, total 317
@@ -0,0 +1,47 @@
#pragma once
 
#include "nes/core/irom.h"
 
#include <array>
#include <cstdint>
#include <utility>
#include <vector>
 
namespace n_e_s::core {
 
class Mapper2 : public IRom {
public:
Mapper2(const INesHeader &h,
std::vector<uint8_t> prg_rom,
std::vector<uint8_t> chr_mem);
 
[[nodiscard]] bool is_cpu_address_in_range(uint16_t addr) const override;
uint8_t cpu_read_byte(uint16_t addr) const override;
void cpu_write_byte(uint16_t addr, uint8_t byte) override;
 
[[nodiscard]] bool is_ppu_address_in_range(uint16_t addr) const override;
uint8_t ppu_read_byte(uint16_t addr) const override;
void ppu_write_byte(uint16_t addr, uint8_t byte) override;
 
private:
std::pair<int, uint16_t> translate_nametable_addr(uint16_t addr,
Mirroring m) const;
 
uint8_t select_bank_low_{0u};
uint8_t select_bank_hi_{0u};
std::vector<uint8_t> prg_rom_; // const?
std::vector<uint8_t> chr_mem_;
 
std::array<std::array<uint8_t, 0x0400>, 2> nametables_;
 
constexpr static uint16_t kChrEnd{0x1FFF};
constexpr static uint16_t kNametableStart{0x2000};
constexpr static uint16_t kNametableEnd{0x3EFF};
 
constexpr static uint16_t kSwitchablePrgRomStart{0x8000};
constexpr static uint16_t kSwitchablePrgRomEnd{0xBFFF};
constexpr static uint16_t kLastBankPrgRomStart{0xC000};
constexpr static uint16_t kLastBankPrgRomEnd{0xFFFF};
};
 
} // namespace n_e_s::core
 
core/src/rom/nrom.cpp added: 354, removed: 37, total 317
@@ -40,13 +40,11 @@ uint8_t Nrom::cpu_read_byte(uint16_t addr) const {
}
 
void Nrom::cpu_write_byte(uint16_t addr, uint8_t byte) {
// Only ram is writable
if (addr <= kPrgRamEnd) {
addr -= kPrgRamStart;
prg_ram_[addr % prg_ram_.size()] = byte;
}
 
addr -= kPrgRomStart;
prg_rom_[addr % prg_rom_.size()] = byte;
}
 
bool Nrom::is_ppu_address_in_range(uint16_t addr) const {
 
core/src/rom_factory.cpp added: 354, removed: 37, total 317
@@ -1,9 +1,11 @@
#include "nes/core/rom_factory.h"
 
#include "rom/mapper_2.h"
#include "rom/nrom.h"
 
#include <fmt/format.h>
#include <cassert>
#include <cstddef>
#include <cstring>
#include <istream>
#include <limits>
@@ -70,13 +72,24 @@ std::unique_ptr<IRom> RomFactory::from_bytes(std::istream &bytestream) {
std::vector<uint8_t> prg_rom(bytes.begin() + sizeof(INesHeader),
bytes.begin() + sizeof(INesHeader) + prg_rom_byte_count);
 
std::vector<uint8_t> chr_rom(
bytes.begin() + sizeof(INesHeader) + prg_rom_byte_count,
bytes.begin() + sizeof(INesHeader) + prg_rom_byte_count +
chr_rom_byte_count);
std::vector<uint8_t> chr_memory;
if (h.chr_rom_size == 0) {
// No chr rom, game uses chr ram
// Allocate ram with enough size:
// https://www.nesdev.org/wiki/CHR_ROM_vs._CHR_RAM
chr_memory.resize(static_cast<std::size_t>(8u * 1024u));
} else {
chr_memory = std::vector<uint8_t>(
bytes.begin() + sizeof(INesHeader) + prg_rom_byte_count,
bytes.begin() + sizeof(INesHeader) + prg_rom_byte_count +
chr_rom_byte_count);
}
 
if (mapper == 0) {
return std::make_unique<Nrom>(h, prg_rom, chr_rom);
return std::make_unique<Nrom>(h, prg_rom, chr_memory);
}
if (mapper == 2) {
return std::make_unique<Mapper2>(h, prg_rom, chr_memory);
}
 
throw std::logic_error(fmt::format("Unsupported mapper: {}", mapper));
 
core/test/src/test_rom.cpp added: 354, removed: 37, total 317
@@ -11,14 +11,20 @@ using namespace n_e_s::core;
 
namespace {
 
std::string ines_header_bytes(const uint8_t mapper,
enum class Mapper {
Nrom = 0,
Mapper2 = 2,
};
 
std::string ines_header_bytes(const uint8_t mapper_id,
const uint8_t prg_rom_size,
const uint8_t chr_rom_size,
const uint8_t prg_ram_size) {
INesHeader header{};
header.flags_6 = static_cast<uint8_t>(mapper & static_cast<uint8_t>(0x0Fu))
<< 4u;
header.flags_7 = static_cast<uint8_t>(mapper & static_cast<uint8_t>(0xF0u));
header.flags_6 =
static_cast<uint8_t>(mapper_id & static_cast<uint8_t>(0x0Fu)) << 4u;
header.flags_7 =
static_cast<uint8_t>(mapper_id & static_cast<uint8_t>(0xF0u));
header.prg_rom_size = prg_rom_size;
header.chr_rom_size = chr_rom_size;
header.prg_ram_size = prg_ram_size;
@@ -32,8 +38,11 @@ std::string ines_header_bytes(const uint8_t mapper,
return bytes;
}
 
std::string nrom_bytes(const uint8_t prg_rom_size, const uint8_t chr_rom_size) {
std::string bytes{ines_header_bytes(0, prg_rom_size, chr_rom_size, 0)};
std::string nrom_bytes(const uint8_t prg_rom_size,
const uint8_t chr_rom_size,
const Mapper mapper) {
std::string bytes{ines_header_bytes(
static_cast<uint8_t>(mapper), prg_rom_size, chr_rom_size, 0)};
 
bytes.append(prg_rom_size * 16 * 1024, 0);
bytes.append(chr_rom_size * 8 * 1024, 0);
@@ -41,6 +50,16 @@ std::string nrom_bytes(const uint8_t prg_rom_size, const uint8_t chr_rom_size) {
return bytes;
}
 
void set_prg_rom_byte(const int prg_rom_banks,
std::string *bytes,
const uint32_t addr,
const uint8_t data) {
const uint32_t bytes_offset = addr + sizeof(INesHeader);
const uint32_t prg_rom_size = prg_rom_banks * 16 * 1024;
EXPECT_LT(addr, prg_rom_size);
std::memcpy(&bytes->operator[](bytes_offset), &data, sizeof(data));
}
 
TEST(RomFactory, doesnt_parse_streams_with_too_few_bytes) {
std::string bytes(15, 0);
std::stringstream ss(bytes);
@@ -63,19 +82,19 @@ TEST(RomFactory, fails_if_mapper_not_supported) {
}
 
TEST(Nrom, creation_works_with_correct_rom_sizes) {
std::string bytes{nrom_bytes(1, 1)};
std::string bytes{nrom_bytes(1, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom{RomFactory::from_bytes(ss)};
EXPECT_NE(nullptr, nrom);
 
bytes = nrom_bytes(2, 1);
bytes = nrom_bytes(2, 1, Mapper::Nrom);
ss = std::stringstream(bytes);
nrom = RomFactory::from_bytes(ss);
EXPECT_NE(nullptr, nrom);
}
 
TEST(Nrom, has_the_correct_cpu_address_space) {
std::string bytes{nrom_bytes(1, 1)};
std::string bytes{nrom_bytes(1, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom{RomFactory::from_bytes(ss)};
EXPECT_TRUE(nrom->is_cpu_address_in_range(0x6000));
@@ -83,7 +102,7 @@ TEST(Nrom, has_the_correct_cpu_address_space) {
}
 
TEST(Nrom, has_the_correct_ppu_address_space) {
std::string bytes{nrom_bytes(1, 1)};
std::string bytes{nrom_bytes(1, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom{RomFactory::from_bytes(ss)};
EXPECT_TRUE(nrom->is_ppu_address_in_range(0x0000));
@@ -91,30 +110,28 @@ TEST(Nrom, has_the_correct_ppu_address_space) {
}
 
TEST(Nrom, creation_fails_with_bad_rom_sizes) {
std::string bytes{nrom_bytes(0, 1)};
std::string bytes{nrom_bytes(0, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
EXPECT_THROW(auto tmp = RomFactory::from_bytes(ss), std::invalid_argument);
 
bytes = nrom_bytes(1, 1);
bytes = nrom_bytes(1, 1, Mapper::Nrom);
bytes.pop_back();
ss = std::stringstream(bytes);
EXPECT_THROW(auto tmp = RomFactory::from_bytes(ss), std::invalid_argument);
 
bytes = nrom_bytes(3, 1);
bytes = nrom_bytes(3, 1, Mapper::Nrom);
ss = std::stringstream(bytes);
EXPECT_THROW(auto tmp = RomFactory::from_bytes(ss), std::invalid_argument);
 
bytes = nrom_bytes(1, 0);
ss = std::stringstream(bytes);
EXPECT_THROW(auto tmp = RomFactory::from_bytes(ss), std::invalid_argument);
 
bytes = nrom_bytes(1, 2);
bytes = nrom_bytes(1, 2, Mapper::Nrom);
ss = std::stringstream(bytes);
EXPECT_THROW(auto tmp = RomFactory::from_bytes(ss), std::invalid_argument);
}
 
////////////////////////////////////////////////////////////////
// Nrom tests
TEST(Nrom, write_and_read_byte_ppu_bus) {
std::string bytes{nrom_bytes(1, 1)};
std::string bytes{nrom_bytes(1, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom = RomFactory::from_bytes(ss);
 
@@ -122,20 +139,133 @@ TEST(Nrom, write_and_read_byte_ppu_bus) {
EXPECT_EQ(0x89, nrom->ppu_read_byte(0x0100));
}
 
TEST(Nrom, write_and_read_byte_cpu_bus) {
std::string bytes{nrom_bytes(1, 1)};
TEST(Nrom, write_and_read_prg_ram) {
std::string bytes{nrom_bytes(1, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom = RomFactory::from_bytes(ss);
 
// $6000-$7FFF: prg ram
nrom->cpu_write_byte(0x6000, 0x0F);
EXPECT_EQ(0x0F, nrom->cpu_read_byte(0x6000));
// Prg rom should not be affected.
for (uint16_t addr = 0x8000; addr < 0xFFFF; ++addr) {
EXPECT_EQ(0x00, nrom->cpu_read_byte(addr));
}
}
 
nrom->cpu_write_byte(0x8000, 0xAB);
TEST(Nrom, prg_rom_should_not_be_writable) {
constexpr int kPrgRomBanks = 2;
std::string bytes{nrom_bytes(kPrgRomBanks, 1, Mapper::Nrom)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom = RomFactory::from_bytes(ss);
 
nrom->cpu_write_byte(0x8000, 0x11);
EXPECT_EQ(0x00, nrom->cpu_read_byte(0x8000));
nrom->cpu_write_byte(0xFFFF, 0x12);
EXPECT_EQ(0x00, nrom->cpu_read_byte(0xFFFF));
}
 
TEST(Nrom, write_and_read_byte_cpu_bus_16k_prg_rom) {
constexpr int kPrgRomBanks = 1;
std::string bytes{nrom_bytes(kPrgRomBanks, 1, Mapper::Nrom)};
set_prg_rom_byte(1, &bytes, 0x0000, 0xAB); // Mapped to 0x8000
set_prg_rom_byte(1, &bytes, 0x3FFF, 0x10); // Mapped to 0xBFFF
 
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom = RomFactory::from_bytes(ss);
 
// 16 K prg rom: $C000-$FFFF should mirror $8000-$BFFF
EXPECT_EQ(0xAB, nrom->cpu_read_byte(0x8000));
EXPECT_EQ(0xAB, nrom->cpu_read_byte(0xC000));
nrom->cpu_write_byte(0xBFFF, 0x10);
 
EXPECT_EQ(0x10, nrom->cpu_read_byte(0xBFFF));
EXPECT_EQ(0x10, nrom->cpu_read_byte(0xFFFF));
}
 
TEST(Nrom, write_and_read_byte_cpu_bus_32k_prg_rom) {
constexpr int kPrgRomBanks = 2;
std::string bytes{nrom_bytes(kPrgRomBanks, 1, Mapper::Nrom)};
set_prg_rom_byte(2, &bytes, 0x0000, 0xAB); // Mapped to 0x8000
set_prg_rom_byte(2, &bytes, 0x3FFF, 0x10); // Mapped to 0xBFFF
set_prg_rom_byte(2, &bytes, 0x4000, 0x42); // Mapped to 0xC000
set_prg_rom_byte(2, &bytes, 0x7FFF, 0x78); // Mapped to 0xFFFF
 
std::stringstream ss(bytes);
std::unique_ptr<IRom> nrom = RomFactory::from_bytes(ss);
 
// 32 K prg rom:
// CPU $8000-$BFFF: First 16 KB of ROM.
// CPU $C000-$FFFF: Last 16 KB of ROM.
EXPECT_EQ(0xAB, nrom->cpu_read_byte(0x8000));
EXPECT_EQ(0x10, nrom->cpu_read_byte(0xBFFF));
EXPECT_EQ(0x42, nrom->cpu_read_byte(0xC000));
EXPECT_EQ(0x78, nrom->cpu_read_byte(0xFFFF));
}
 
////////////////////////////////////////////////////////////////
// Mapper 2 tests
TEST(Mapper2, write_and_read_byte_ppu_bus) {
std::string bytes{nrom_bytes(1, 1, Mapper::Mapper2)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> rom = RomFactory::from_bytes(ss);
 
rom->ppu_write_byte(0x0100, 0x89);
EXPECT_EQ(0x89, rom->ppu_read_byte(0x0100));
}
 
TEST(Mapper2, write_should_not_modify_anything) {
constexpr int kPrgRomBanks = 2;
std::string bytes{nrom_bytes(kPrgRomBanks, 1, Mapper::Mapper2)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> rom = RomFactory::from_bytes(ss);
 
rom->cpu_write_byte(0x9000, 0xF1);
for (uint16_t addr = 0x8000; addr < 0xFFFF; ++addr) {
EXPECT_EQ(0x00, rom->cpu_read_byte(addr));
}
}
 
TEST(Mapper2, prg_rom_should_not_be_writable) {
constexpr int kPrgRomBanks = 2;
std::string bytes{nrom_bytes(kPrgRomBanks, 1, Mapper::Mapper2)};
std::stringstream ss(bytes);
std::unique_ptr<IRom> rom = RomFactory::from_bytes(ss);
 
rom->cpu_write_byte(0x8000, 0x11);
EXPECT_EQ(0x00, rom->cpu_read_byte(0x8000));
rom->cpu_write_byte(0xFFFF, 0x12);
EXPECT_EQ(0x00, rom->cpu_read_byte(0xFFFF));
}
 
TEST(Mapper2, write_and_read_byte_cpu_bus) {
constexpr int kPrgRomBanks = 8;
std::string bytes{nrom_bytes(kPrgRomBanks, 1, Mapper::Mapper2)};
// First byte in first bank
set_prg_rom_byte(kPrgRomBanks, &bytes, 0u * 0x4000u, 0x01);
// First byte in second bank
set_prg_rom_byte(kPrgRomBanks, &bytes, 1u * 0x4000u, 0x02);
// First byte in last bank
set_prg_rom_byte(kPrgRomBanks, &bytes, 7u * 0x4000u, 0x07);
// Second byte in last bank
set_prg_rom_byte(kPrgRomBanks, &bytes, 7u * 0x4000u + 1u, 0xAB);
 
std::stringstream ss(bytes);
std::unique_ptr<IRom> rom = RomFactory::from_bytes(ss);
 
// $8000-$BFFF: 16 KB switchable PRG ROM bank
EXPECT_EQ(0x01, rom->cpu_read_byte(0x8000));
 
// $C000-$FFFF: 16 KB PRG ROM bank, fixed to the last bank
EXPECT_EQ(0x07, rom->cpu_read_byte(0xC000));
EXPECT_EQ(0xAB, rom->cpu_read_byte(0xC001));
 
// Write to rom to switch bank
rom->cpu_write_byte(0x8000, 0x01);
EXPECT_EQ(0x02, rom->cpu_read_byte(0x8000));
 
// Last bank should not be affected
EXPECT_EQ(0x07, rom->cpu_read_byte(0xC000));
EXPECT_EQ(0xAB, rom->cpu_read_byte(0xC001));
}
 
} // namespace