Refactor out the chip8 emulator.

master
Zed A. Shaw 2 days ago
parent 03eb8afc2a
commit 0296d500e4
  1. 4
      Makefile
  2. 4
      README.md
  3. 1
      meson.build
  4. 455
      src/chip8.cpp
  5. 120
      src/chip8.hpp
  6. 28
      src/main.cpp

@ -15,13 +15,13 @@ debug_build:
meson compile -j 4 -C builddir meson compile -j 4 -C builddir
run: build run: build
./builddir/chip8.exe ./builddir/chip8.exe 1 1 ./roms/test_opcode.ch8
debug: build debug: build
gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe
debug_run: build debug_run: build
gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/chip8.exe gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/chip8.exe 1 1 ./roms/test_opcode.ch8
clean: clean:
meson compile --clean -C builddir meson compile --clean -C builddir

@ -5,3 +5,7 @@ Just a little fun project. I based the first version on this really great blog
Other resources from trapexit at: Other resources from trapexit at:
https://github.com/trapexit/chip-8_documentation https://github.com/trapexit/chip-8_documentation
Also interesting:
https://github.com/aicheye/crustty?tab=readme-ov-file

@ -82,6 +82,7 @@ dependencies += [
sources = [ sources = [
'src/dbc.cpp', 'src/dbc.cpp',
'src/chip8.cpp',
] ]
executable('chip8', executable('chip8',

@ -0,0 +1,455 @@
#include "chip8.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
Chip8::Chip8() :
randGen(std::chrono::system_clock::now().time_since_epoch().count())
{
pc = START_ADDRESS;
for(size_t i = 0; i < FONTSET_SIZE; ++i) {
memory[FONTSET_START_ADDRESS + i] = fontset[i];
}
randByte = std::uniform_int_distribution<uint8_t>(0, 255U);
// Set up function pointer table
table[0x0] = &Chip8::Table0;
table[0x1] = &Chip8::OP_1nnn;
table[0x2] = &Chip8::OP_2nnn;
table[0x3] = &Chip8::OP_3xkk;
table[0x4] = &Chip8::OP_4xkk;
table[0x5] = &Chip8::OP_5xy0;
table[0x6] = &Chip8::OP_6xkk;
table[0x7] = &Chip8::OP_7xkk;
table[0x8] = &Chip8::Table8;
table[0x9] = &Chip8::OP_9xy0;
table[0xA] = &Chip8::OP_Annn;
table[0xB] = &Chip8::OP_Bnnn;
table[0xC] = &Chip8::OP_Cxkk;
table[0xD] = &Chip8::OP_Dxyn;
table[0xE] = &Chip8::TableE;
table[0xF] = &Chip8::TableF;
for (size_t i = 0; i <= 0xE; i++)
{
table0[i] = &Chip8::OP_NULL;
table8[i] = &Chip8::OP_NULL;
tableE[i] = &Chip8::OP_NULL;
}
table0[0x0] = &Chip8::OP_00E0;
table0[0xE] = &Chip8::OP_00EE;
table8[0x0] = &Chip8::OP_8xy0;
table8[0x1] = &Chip8::OP_8xy1;
table8[0x2] = &Chip8::OP_8xy2;
table8[0x3] = &Chip8::OP_8xy3;
table8[0x4] = &Chip8::OP_8xy4;
table8[0x5] = &Chip8::OP_8xy5;
table8[0x6] = &Chip8::OP_8xy6;
table8[0x7] = &Chip8::OP_8xy7;
table8[0xE] = &Chip8::OP_8xyE;
tableE[0x1] = &Chip8::OP_ExA1;
tableE[0xE] = &Chip8::OP_Ex9E;
for (size_t i = 0; i <= 0x65; i++)
{
tableF[i] = &Chip8::OP_NULL;
}
tableF[0x07] = &Chip8::OP_Fx07;
tableF[0x0A] = &Chip8::OP_Fx0A;
tableF[0x15] = &Chip8::OP_Fx15;
tableF[0x18] = &Chip8::OP_Fx18;
tableF[0x1E] = &Chip8::OP_Fx1E;
tableF[0x29] = &Chip8::OP_Fx29;
tableF[0x33] = &Chip8::OP_Fx33;
tableF[0x55] = &Chip8::OP_Fx55;
tableF[0x65] = &Chip8::OP_Fx65;
}
void Chip8::LoadROM(const std::string& filename) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
dbc::check(file.is_open(), $F("Can't open {}", filename));
size_t size = file.tellg();
char* buffer = new char[size];
file.seekg(0, std::ios::beg);
file.read(buffer, size);
file.close();
for(size_t i = 0; i < size; ++i) {
memory[START_ADDRESS + i] = buffer[i];
}
delete[] buffer;
}
void Chip8::Cycle() {
// Fetch
opcode = (memory[pc] << 8u) | memory[pc + 1];
// Increment the PC before we execute anything
pc += 2;
// decode and Execute
((*this).*(table[(opcode & 0xF000u) >> 12u]))();
// Decrement the delay timer if it's been set
if(delayTimer > 0) {
--delayTimer;
}
// decrement the sound timer if it's been set
if(soundTimer > 0) {
--soundTimer;
}
}
void Chip8::OP_00E0() {
memset(video, 0, sizeof(video));
}
void Chip8::OP_00EE() {
--sp;
pc = stack[sp];
}
void Chip8::OP_1nnn() {
uint16_t address = opcode & 0x0FFFu;
pc = address;
}
void Chip8::OP_2nnn() {
uint16_t address = opcode & 0x0FFFu;
stack[sp] = pc;
++sp;
pc = address;
}
void Chip8::OP_3xkk()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t byte = opcode & 0x00FFu;
if(registers[Vx] == byte) {
pc += 2;
}
}
void Chip8::OP_4xkk()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t byte = opcode & 0x00FFu;
if(registers[Vx] != byte) {
pc += 2;
}
}
void Chip8::OP_5xy0()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
if(registers[Vx] == registers[Vy]) {
pc += 2;
}
}
void Chip8::OP_6xkk() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t byte = opcode & 0x00FFu;
registers[Vx] = byte;
}
void Chip8::OP_7xkk() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t byte = opcode & 0x00FFu;
registers[Vx] += byte;
}
void Chip8::OP_8xy0() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
registers[Vx] = registers[Vy];
}
void Chip8::OP_8xy1()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
registers[Vx] |= registers[Vy];
}
void Chip8::OP_8xy2()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
registers[Vx] &= registers[Vy];
}
void Chip8::OP_8xy3()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
registers[Vx] ^= registers[Vy];
}
void Chip8::OP_8xy4() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
uint16_t sum = registers[Vx] + registers[Vy];
registers[0xF] = sum > 255U ? 1 : 0;
registers[Vx] = sum & 0xFFu;
}
void Chip8::OP_8xy5()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
registers[0xF] = registers[Vx] > registers[Vy] ? 1 : 0;
registers[Vx] -= registers[Vy];
}
void Chip8::OP_8xy6()
{
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
// Save LSB in VF
registers[0xF] = (registers[Vx] & 0x1u);
registers[Vx] >>= 1;
}
void Chip8::OP_8xy7() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
registers[0xF] = registers[Vy] > registers[Vx] ? 1 : 0;
registers[Vx] = registers[Vy] - registers[Vx];
}
void Chip8::OP_8xyE() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
// Save MSB in VF
registers[0xF] = (registers[Vx] & 0x80u) >> 7u;
registers[Vx] <<= 1;
}
void Chip8::OP_9xy0() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
if(registers[Vx] != registers[Vy]) {
pc += 2;
}
}
void Chip8::OP_Annn() {
uint16_t address = opcode & 0x0FFFu;
index = address;
}
void Chip8::OP_Bnnn() {
uint16_t address = opcode & 0x0FFFu;
pc = registers[0] + address;
}
void Chip8::OP_Cxkk() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint16_t byte = opcode & 0x0FFFu;
registers[Vx] = randByte(randGen) & byte;
}
void Chip8::OP_Dxyn() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t Vy = (opcode & 0x00F0u) >> 4u;
uint8_t height = opcode & 0x00Fu;
// wrap if going beyond the screen boundaries
uint8_t xPos = registers[Vx] % VIDEO_WIDTH;
uint8_t yPos = registers[Vy] % VIDEO_HEIGHT;
registers[0xF] = 0;
for(size_t row = 0; row < height; ++row) {
uint8_t spriteByte = memory[index + row];
for(size_t col = 0; col < 8; ++col) {
uint8_t spritePixel = spriteByte & (0x80u >> col);
uint32_t* screenPixel = &video[(yPos + row) * VIDEO_WIDTH + (xPos + col)];
// Sprite pixel is on
if(spritePixel) {
// Screen pixel also on - collision
if(*screenPixel == 0xFFFFFFFF) {
registers[0xF] = 1;
}
*screenPixel ^= 0xFFFFFFFF;
}
}
}
}
void Chip8::OP_Ex9E() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t key = registers[Vx];
if(keypad[key]) {
pc += 2;
}
}
void Chip8::OP_ExA1() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t key = registers[Vx];
if(!keypad[key]) {
pc += 2;
}
}
void Chip8::OP_Fx07() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
registers[Vx] = delayTimer;
}
void Chip8::OP_Fx0A() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
if (keypad[0])
{
registers[Vx] = 0;
}
else if (keypad[1])
{
registers[Vx] = 1;
}
else if (keypad[2])
{
registers[Vx] = 2;
}
else if (keypad[3])
{
registers[Vx] = 3;
}
else if (keypad[4])
{
registers[Vx] = 4;
}
else if (keypad[5])
{
registers[Vx] = 5;
}
else if (keypad[6])
{
registers[Vx] = 6;
}
else if (keypad[7])
{
registers[Vx] = 7;
}
else if (keypad[8])
{
registers[Vx] = 8;
}
else if (keypad[9])
{
registers[Vx] = 9;
}
else if (keypad[10])
{
registers[Vx] = 10;
}
else if (keypad[11])
{
registers[Vx] = 11;
}
else if (keypad[12])
{
registers[Vx] = 12;
}
else if (keypad[13])
{
registers[Vx] = 13;
}
else if (keypad[14])
{
registers[Vx] = 14;
}
else if (keypad[15])
{
registers[Vx] = 15;
}
else
{
pc -= 2;
}
}
void Chip8::OP_Fx15() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
delayTimer = registers[Vx];
}
void Chip8::OP_Fx18() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
soundTimer = registers[Vx];
}
void Chip8::OP_Fx1E() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
index += registers[Vx];
}
void Chip8::OP_Fx29() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t digit = registers[Vx];
index = FONTSET_START_ADDRESS + (5 * digit);
}
// WTF? binary coded decimal
void Chip8::OP_Fx33() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
uint8_t value = registers[Vx];
memory[index + 2] = value % 10;
value /= 10;
memory[index + 1] = value % 10;
value /= 10;
memory[index] = value % 10;
}
void Chip8::OP_Fx55() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
for(uint8_t i = 0; i <= Vx; ++i) {
memory[index + i] = registers[i];
}
}
void Chip8::OP_Fx65() {
uint8_t Vx = (opcode & 0x0F00u) >> 8u;
for(uint8_t i = 0; i <= Vx; ++i) {
registers[i] = memory[index + i];
}
}

@ -0,0 +1,120 @@
#pragma once
#define _USE_MATH_DEFINES
#include <math.h>
#include <cstdint>
#include <fstream>
#include <chrono>
#include <random>
const unsigned int START_ADDRESS = 0x200;
const unsigned int FONTSET_SIZE = 80;
const unsigned int FONTSET_START_ADDRESS = 0x50;
const unsigned int VIDEO_WIDTH = 64;
const unsigned int VIDEO_HEIGHT = 32;
class Chip8 {
public:
uint8_t registers[16]{};
uint8_t memory[4096]{};
uint16_t index{};
uint16_t pc{};
uint16_t stack[16]{};
uint8_t sp{};
uint8_t delayTimer{};
uint8_t soundTimer{};
uint8_t keypad[16]{};
uint32_t video[64 * 32]{};
uint16_t opcode;
uint8_t fontset[FONTSET_SIZE] =
{
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
0x20, 0x60, 0x20, 0x20, 0x70, // 1
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
0xF0, 0x80, 0xF0, 0x80, 0x80 // F
};
std::default_random_engine randGen;
std::uniform_int_distribution<uint8_t> randByte;
typedef void (Chip8::*Chip8Func)();
Chip8Func table[0xF + 1];
Chip8Func table0[0xE + 1];
Chip8Func table8[0xE + 1];
Chip8Func tableE[0xE + 1];
Chip8Func tableF[0x65 + 1];
Chip8();
void LoadROM(const std::string& filename);
// opcodes
void OP_NULL() {}
void OP_00E0(); // CLS
void OP_00EE(); // RET
void OP_1nnn(); // JP addr
void OP_2nnn(); // CALL addr
void OP_3xkk(); // SE Vx, byte
void OP_4xkk(); // SNE Vx, byte
void OP_5xy0(); // SE Vx, Vy
void OP_6xkk(); // LD Vx, byte
void OP_7xkk(); // ADD Vx, byte
void OP_8xy0(); // LD Vx, Vy
void OP_8xy1(); // OR Vx, Vy
void OP_8xy2(); // AND Vx, Vy
void OP_8xy3(); // XOR Vx, Vy
void OP_8xy4(); // ADD Vx, Vy
void OP_8xy5(); // SUB Vx, Vy
void OP_8xy6(); // SHR Vx
void OP_8xy7(); // SUBN Vx, Vy
void OP_8xyE(); // SHL Vx {, Vy}
void OP_9xy0(); // SNE Vx, Vy
void OP_Annn(); // LD Index, addr
void OP_Bnnn(); // JP V0, addr
void OP_Cxkk(); // RND Vx, byte
void OP_Dxyn(); // SKP Vx
void OP_Ex9E(); // SKP Vx
void OP_ExA1(); // SKNP Vx
void OP_Fx07(); // LD Vx, DT delay timer
void OP_Fx0A(); // LD VX, K
void OP_Fx15(); // LD DT, Vx
void OP_Fx18(); // LD ST, Vx sound timer
void OP_Fx1E(); // ADD I, Vx
void OP_Fx29(); // LD F, Vx font
void OP_Fx33(); // LD B, Vx bcd == binary coded decimal
void OP_Fx55(); // LD [I], Vx fill ram up to Vx register
void OP_Fx65();
void Table0()
{
((*this).*(table0[opcode & 0x000Fu]))();
}
void Table8()
{
((*this).*(table8[opcode & 0x000Fu]))();
}
void TableE()
{
((*this).*(tableE[opcode & 0x000Fu]))();
}
void TableF()
{
((*this).*(tableF[opcode & 0x00FFu]))();
}
void Cycle();
};

@ -8,9 +8,17 @@
#include <SFML/Audio.hpp> #include <SFML/Audio.hpp>
#include <SFML/Window/Event.hpp> #include <SFML/Window/Event.hpp>
#include "dbc.hpp" #include "dbc.hpp"
#include "chip8.hpp"
int main() { int main(int argc, char* argv[]) {
fmt::print("Setting up a window for you...\n"); if(argc != 4) {
fmt::print("ERROR!");
return 1;
}
int videoScale = std::stoi(argv[1]);
int cycleDelay = std::stoi(argv[2]);
std::string romFilename{argv[3]};
sf::RenderWindow window(sf::VideoMode({1280, 720}), "Simple Game Demo"); sf::RenderWindow window(sf::VideoMode({1280, 720}), "Simple Game Demo");
window.setFramerateLimit(60); window.setFramerateLimit(60);
@ -20,7 +28,23 @@ int main() {
sf::Clock clock; sf::Clock clock;
sf::Time tick = clock.getElapsedTime(); sf::Time tick = clock.getElapsedTime();
Chip8 chip8;
chip8.LoadROM(romFilename);
int videoPitch = sizeof(chip8.video[0]) * VIDEO_WIDTH;
auto lastCycleTime = std::chrono::high_resolution_clock::now();
while (window.isOpen()) { while (window.isOpen()) {
auto currentTime = std::chrono::high_resolution_clock::now();
float dt = std::chrono::duration<float, std::chrono::milliseconds::period>(currentTime - lastCycleTime).count();
if (dt > cycleDelay)
{
lastCycleTime = currentTime;
chip8.Cycle();
}
window.clear(); window.clear();
window.display(); window.display();
} }

Loading…
Cancel
Save