srctree

Johan Norberg parent 7cd5b7aa ef2ef1e6
Add support for mapper 2

core/CMakeLists.txt added: 256, removed: 7, total 249
@@ -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: 256, removed: 7, total 249
@@ -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: 256, removed: 7, total 249
@@ -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_factory.cpp added: 256, removed: 7, total 249
@@ -1,5 +1,6 @@
#include "nes/core/rom_factory.h"
 
#include "rom/mapper_2.h"
#include "rom/nrom.h"
 
#include <fmt/format.h>
@@ -87,6 +88,9 @@ std::unique_ptr<IRom> RomFactory::from_bytes(std::istream &bytestream) {
if (mapper == 0) {
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: 256, removed: 7, total 249
@@ -13,6 +13,7 @@ namespace {
 
enum class Mapper {
Nrom = 0,
Mapper2 = 2,
};
 
std::string ines_header_bytes(const uint8_t mapper_id,
@@ -51,10 +52,10 @@ std::string nrom_bytes(const uint8_t prg_rom_size,
 
void set_prg_rom_byte(const int prg_rom_banks,
std::string *bytes,
const uint16_t addr,
const uint32_t addr,
const uint8_t data) {
const uint32_t bytes_offset = addr + sizeof(INesHeader);
const uint16_t prg_rom_size = prg_rom_banks * 16 * 1024;
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));
}
@@ -127,6 +128,8 @@ TEST(Nrom, creation_fails_with_bad_rom_sizes) {
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, Mapper::Nrom)};
std::stringstream ss(bytes);
@@ -199,4 +202,70 @@ TEST(Nrom, write_and_read_byte_cpu_bus_32k_prg_rom) {
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