From 0296d500e4242b63894b4ed7676ed6820df6da13 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Sun, 17 May 2026 11:10:21 -0400 Subject: [PATCH] Refactor out the chip8 emulator. --- Makefile | 4 +- README.md | 4 + meson.build | 1 + src/chip8.cpp | 455 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/chip8.hpp | 120 +++++++++++++ src/main.cpp | 28 +++- 6 files changed, 608 insertions(+), 4 deletions(-) create mode 100644 src/chip8.cpp create mode 100644 src/chip8.hpp diff --git a/Makefile b/Makefile index 5616a65..91e73a3 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,13 @@ debug_build: meson compile -j 4 -C builddir run: build - ./builddir/chip8.exe + ./builddir/chip8.exe 1 1 ./roms/test_opcode.ch8 debug: build gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe 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: meson compile --clean -C builddir diff --git a/README.md b/README.md index 4bb2a10..3b36020 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,7 @@ Just a little fun project. I based the first version on this really great blog Other resources from trapexit at: https://github.com/trapexit/chip-8_documentation + +Also interesting: + +https://github.com/aicheye/crustty?tab=readme-ov-file diff --git a/meson.build b/meson.build index 13f9478..40386af 100644 --- a/meson.build +++ b/meson.build @@ -82,6 +82,7 @@ dependencies += [ sources = [ 'src/dbc.cpp', + 'src/chip8.cpp', ] executable('chip8', diff --git a/src/chip8.cpp b/src/chip8.cpp new file mode 100644 index 0000000..2116d5f --- /dev/null +++ b/src/chip8.cpp @@ -0,0 +1,455 @@ +#include "chip8.hpp" +#include "dbc.hpp" +#include + +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(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]; + } +} diff --git a/src/chip8.hpp b/src/chip8.hpp new file mode 100644 index 0000000..0dc37c1 --- /dev/null +++ b/src/chip8.hpp @@ -0,0 +1,120 @@ +#pragma once +#define _USE_MATH_DEFINES +#include +#include +#include +#include +#include + +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 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(); +}; diff --git a/src/main.cpp b/src/main.cpp index 6bf523c..23ae2f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,9 +8,17 @@ #include #include #include "dbc.hpp" +#include "chip8.hpp" -int main() { - fmt::print("Setting up a window for you...\n"); +int main(int argc, char* argv[]) { + 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"); window.setFramerateLimit(60); @@ -20,7 +28,23 @@ int main() { sf::Clock clock; 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()) { + auto currentTime = std::chrono::high_resolution_clock::now(); + float dt = std::chrono::duration(currentTime - lastCycleTime).count(); + + if (dt > cycleDelay) + { + lastCycleTime = currentTime; + chip8.Cycle(); + } + window.clear(); window.display(); }