This commit is contained in:
Sebastian Mihai Pantoc
2026-05-12 11:15:47 +02:00
commit 8d93de4be7
27 changed files with 773 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# Ignore Build folder
build/
# ignore .DS_Store on macos
.DS_Store
# ingore .cache folder
.cache
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Linker files
*.ilk
# Debugger Files
*.pdb
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# debug information files
*.dwo
+21
View File
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 4.1.1)
project(chippotto)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG 0f8d062e1084d269d6437b4d1173148e14a7da74# release 3.4
)
FetchContent_MakeAvailable(SDL3)
file(GLOB src CONFIGURE_DEPENDS src/*.cpp)
add_executable(${PROJECT_NAME} ${src})
file(COPY src/assets/audio.wav DESTINATION assets)
target_link_libraries(${PROJECT_NAME} SDL3::SDL3)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
A Chip8 Emulator made in modern C++ and SDL3
Passes all the tests for Timendus' Test Suite.
To install:
1. Create a "build" directory by running $ mkdir build
2. $ cd build
3. $ cmake .. && cmake --build .
4. run by $ ./chippotto <path/to/rom>
Binary file not shown.
+280
View File
@@ -0,0 +1,280 @@
#include "chip8.hpp"
#include "SDL3/SDL_log.h"
#include "utils.hpp"
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <random>
#include <sys/resource.h>
Chip8::Chip8(std::ifstream &file, Screen *screen) {
for (auto i{0}; i < MEMORY_SIZE; ++i) {
mem[i] = {0};
}
file.seekg(0, std::ios::end);
rom_size = file.tellg();
file.seekg(std::ios::beg);
char ch;
while (file.read(&ch, 1)) {
mem[pc++] = static_cast<u8>(ch);
}
pc = {0x200};
for (auto i{0}; i < 0xF; ++i) {
v[i] = 0;
keypad_[i] = 0;
}
sound_ = 0;
delay_ = 0;
halted = {false};
screen_ = {screen};
load_font();
}
Chip8::~Chip8() {}
Display &Chip8::display() { return display_; }
Keypad &Chip8::keypad() { return keypad_; }
u8 &Chip8::sound() { return sound_; }
u8 &Chip8::delay() { return delay_; }
void Chip8::load_font() {
for (auto i{0}; i < 80; ++i) {
mem[i] = font[i];
}
}
u16 Chip8::fetch() {
u16 opcode = (mem[pc] << 8) | mem[pc + 1];
pc += 2;
return opcode;
}
void Chip8::execute(u16 opcode) {
if (opcode == 0x0000) {
return;
}
u8 X = {static_cast<u8>((opcode & 0x0F00) >> 8)};
u8 Y = {static_cast<u8>((opcode & 0x00F0) >> 4)};
u16 NN = {static_cast<u16>((opcode & 0x00FF))};
u16 NNN = {static_cast<u16>((opcode & 0x0FFF))};
switch (opcode & 0xF000) {
case 0x0000:
switch (opcode & 0x000F) {
case 0x0000:
display_.clear();
break;
case 0x000E:
pc = {stack.pop()};
break;
default:
break;
}
break;
case 0x1000:
pc = {NNN};
break;
case 0x2000:
stack.push(pc);
pc = {NNN};
break;
case 0x3000:
if (v[X] == (NN)) {
pc += 2;
}
break;
case 0x4000:
if (v[X] != (NN)) {
pc += 2;
}
break;
case 0x5000:
if (v[X] == v[Y]) {
pc += 2;
}
break;
case 0x6000:
v[X] = {static_cast<u8>(NN)};
break;
case 0x7000:
v[X] += {static_cast<u8>(NN)};
break;
case 0x8000: {
switch (opcode & 0x000F) {
case 0x0000:
v[X] = {v[Y]};
break;
case 0x0001:
v[X] |= {v[Y]};
break;
case 0x0002:
v[X] &= {v[Y]};
break;
case 0x0003:
v[X] ^= {v[Y]};
break;
case 0x0004: {
u8 tmp = {0};
if ((static_cast<int>(v[X]) + static_cast<int>(v[Y])) > 255) {
tmp = {1};
}
v[X] += v[Y];
v[0xF] = tmp;
} break;
case 0x0005: {
u8 tmp = {static_cast<u8>((v[X] < v[Y]) ? 0 : 1)};
v[X] -= v[Y];
v[0xF] = tmp;
} break;
case 0x0006: {
u8 tmp = {static_cast<u8>(v[Y] & 0x01)};
v[X] = v[Y] >> 1;
v[0xF] = tmp;
} break;
case 0x0007:
v[X] = {static_cast<u8>(v[Y] - v[X])};
v[0xF] = (v[Y] < v[X]) ? 0 : 1;
break;
case 0x000E: {
u8 tmp = {static_cast<u8>((v[Y] & 0x80) >> 7)};
v[X] = v[Y] << 1;
v[0xF] = tmp;
} break;
}
} break;
case 0x9000:
if (v[X] != v[Y]) {
pc += 2;
}
break;
case 0xA000:
i = {static_cast<u16>(NNN)};
break;
case 0xB000:
pc = {static_cast<u16>(NNN + v[0])};
break;
case 0xC000: {
std::random_device device;
std::mt19937 rng(device());
std::uniform_int_distribution<> distribution(0, 255);
u8 random{static_cast<u8>(distribution(rng))};
v[X] = random & NN;
} break;
case 0xD000: {
u8 x = v[X] & 63;
u8 y = v[Y] & 31;
for (auto N{0}; N < (opcode & 0x000F); ++N) {
if (y + N > 31) {
break;
}
u8 byte{mem[i + N]};
for (auto k{0}; k < 8; ++k) {
if (x + k > 63) {
break;
}
if (u8 bit{static_cast<u8>(byte & (0x80 >> k))}) {
if (display_(x + k, y + N)) {
v[0xF] = {true};
}
display_(x + k, y + N) ^= 1;
}
}
}
display_.should_draw() = true;
} break;
case 0xE000:
switch (opcode & 0x000F) {
case 0x000E:
if (keypad_[v[X]]) {
pc += 2;
}
break;
case 0x0001:
if (!keypad_[v[X]]) {
pc += 2;
}
break;
}
break;
case 0xF000:
switch (opcode & 0x000F) {
case 0x0003: {
u8 number = v[X];
int l = 2;
while (l >= 0) {
mem[i + l] = {static_cast<u8>(number % 10)};
number /= 10;
--l;
}
} break;
case 0x0005:
switch (opcode & 0x00F0) {
case 0x0010:
delay_ = v[X];
break;
case 0x0050:
for (auto j{0}; j <= X; ++j) {
mem[i + j] = v[j];
}
break;
case 0x0060:
for (auto j{0}; j <= X; ++j) {
v[j] = mem[i + j];
}
break;
}
break;
case 0x0007:
v[X] = delay_;
break;
case 0x0008:
sound_ = v[X];
break;
case 0x0009:
i = (v[X] & 0x000F) * 5;
break;
case 0x000A: {
halted = {true};
int pressed_key;
for (auto i{0}; i < 16; ++i) {
if (keypad_[i]) {
current_key = i;
break;
}
}
if (current_key != -1) {
for (auto i{0}; i < 16; ++i) {
if (!keypad_[current_key]) {
halted = {false};
v[X] = i;
current_key = {-1};
}
}
}
if (halted) {
pc -= 2;
}
break;
}
case 0x000E:
i += v[X];
v[0xF] = (i > NNN) ? 1 : 0;
break;
}
break;
default:
SDL_Log("OPCODE not recognized: 0x%04x", opcode);
}
}
void Chip8::cycle() { execute(fetch()); }
void Chip8::update_timers() {
if (delay_ > 0) {
--delay_;
}
if (sound_ > 0) {
screen_->beep();
--sound_;
}
}
+40
View File
@@ -0,0 +1,40 @@
#include "display.hpp"
#include "keypad.hpp"
#include "screen.hpp"
#include "stack.hpp"
#include "utils.hpp"
#include <SDL3/SDL_log.h>
#include <array>
#include <cstddef>
#include <fstream>
#include <memory>
class Chip8 {
public:
Chip8(std::ifstream &file, Screen *screen);
~Chip8();
void cycle();
void update_timers();
u8 &delay();
u8 &sound();
Display &display();
Keypad &keypad();
private:
u16 fetch();
void execute(u16 opcode);
void load_font();
Screen *screen_;
Display display_;
Keypad keypad_;
u16 pc{0x200};
u16 i;
Stack stack;
u8 delay_;
u8 sound_;
bool halted;
int current_key{-1};
std::array<u8, REGISTER_NUMBER> v;
std::array<u8, MEMORY_SIZE> mem;
std::size_t rom_size;
};
+25
View File
@@ -0,0 +1,25 @@
#include "display.hpp"
#include "utils.hpp"
#include <stdexcept>
Display::Display() {
clear();
should_draw_ = {false};
}
Display::~Display() {}
void Display::draw() {}
bool &Display::operator()(int i, int j) {
if (i < 0 || i > 63 || j < 0 || j > 31) throw std::out_of_range("Porcoddio");
return pixels_[j * 64 + i];
}
bool *Display::pixels() {
return pixels_;
}
bool &Display::should_draw() { return should_draw_; }
void Display::clear() {
for (auto i{0}; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; ++i) {
pixels_[i] = {false};
}
should_draw_ = {true};
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef DISPLAY
#define DISPLAY
#include "utils.hpp"
#include <array>
#include <cstdlib>
#include <ctime>
class Display {
public:
Display();
~Display();
void draw();
bool &operator()(int i, int j);
bool &should_draw();
bool *pixels();
void clear();
private:
bool pixels_[2048];
bool should_draw_;
};
#endif // !DISPLAY
+11
View File
@@ -0,0 +1,11 @@
#include "keypad.hpp"
#include <array>
Keypad::Keypad() {
for (auto i{0}; i < 16; ++i) {
keys[i] = {false};
}
}
Keypad::~Keypad() {}
bool &Keypad::operator[](int i) { return keys[i]; }
+14
View File
@@ -0,0 +1,14 @@
#ifndef KEYPAD
#define KEYPAD
#include <array>
constexpr unsigned int KEYS = 16;
class Keypad {
public:
Keypad();
~Keypad();
bool& operator[](int i);
private:
std::array<bool, KEYS> keys;
};
#endif // !KEYPAD
+51
View File
@@ -0,0 +1,51 @@
#include "SDL3/SDL_stdinc.h"
#include "SDL3/SDL_timer.h"
#include "chip8.hpp"
#include "utils.hpp"
#include <fstream>
#include <ios>
#include <iostream>
#include <ostream>
#include <vector>
int main(int argc, char *argv[]) {
std::vector<std::string> args{convert_args(argc, argv)};
if (args.size() != 2) {
std::cerr << "Incorrect Usage, ./chippotto /path/to/rom" << std::endl;
exit(1);
}
std::ifstream is{args[1], std::ios::binary};
if (!is) {
std::cerr << "Unable to open rom" << std::endl;
return 1;
}
Screen screen(64, 32);
Chip8 chippotto(is, &screen);
bool quit{false};
u32 to_render[2048];
int frame_rate{60};
Uint64 frame_ms = 1000000000 / frame_rate;
while (!quit) {
Uint64 start = SDL_GetTicksNS();
screen.handler(quit, chippotto.keypad());
for (int i = 0; i < 10; ++i)
chippotto.cycle();
if (chippotto.display().should_draw()) {
modify_pixels(chippotto.display().pixels(), to_render);
screen.render(to_render, DISPLAY_WIDTH * sizeof(u32));
chippotto.display().should_draw() = {false};
}
Uint64 end = SDL_GetTicksNS();
Uint64 total = end - start;
chippotto.update_timers();
if (total < frame_ms) {
SDL_DelayNS(frame_ms - total);
}
}
return 0;
}
+97
View File
@@ -0,0 +1,97 @@
#include "screen.hpp"
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_log.h"
#include "SDL3/SDL_properties.h"
#include "SDL3/SDL_render.h"
#include "SDL3/SDL_video.h"
#include "utils.hpp"
#include <array>
#include <cstring>
Screen::Screen(int w, int h) {
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
SDL_Log("SDL not initialized, error: %s", SDL_GetError());
}
SDL_PropertiesID props{SDL_CreateProperties()};
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
"Chippotto");
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, 1280);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, 640);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER,
SDL_WINDOWPOS_CENTERED);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER,
SDL_WINDOWPOS_CENTERED);
if (window = SDL_CreateWindowWithProperties(props); window == nullptr) {
SDL_Log("Unable To create window");
}
if (renderer = SDL_CreateRenderer(window, nullptr); renderer == nullptr) {
SDL_Log("Window not created, error: %s", SDL_GetError());
}
if (texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING, w, h);
texture == nullptr) {
SDL_Log("Failed to create Texture, error: %s", SDL_GetError());
}
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_PIXELART);
SDL_AudioSpec audio_spec;
if (!SDL_LoadWAV("assets/audio.wav", &audio_spec, &buf, &len)) {
SDL_Log("Error Loading the beep sound");
}
stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&audio_spec, nullptr, nullptr);
SDL_ResumeAudioStreamDevice(stream);
}
Screen::~Screen() {
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
void Screen::render(u32 *buffer, int pitch) {
void *pixels;
int r_pitch;
SDL_LockTexture(texture, nullptr, &pixels, &r_pitch);
r_pitch = {pitch};
memcpy(pixels, buffer, DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(u32));
SDL_UnlockTexture(texture);
SDL_RenderClear(renderer);
SDL_RenderTexture(renderer, texture, nullptr, nullptr);
SDL_RenderPresent(renderer);
}
void Screen::beep() {
if (SDL_GetAudioStreamQueued(stream) < (int)len) {
SDL_PutAudioStreamData(stream, buf, len);
}
}
void Screen::handler(bool &quit, Keypad &keypad) {
SDL_Event e;
SDL_zero(e);
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) {
quit = {true};
} else if (e.type == SDL_EVENT_KEY_DOWN) {
for (auto i{0}; i < 16; ++i) {
if (e.key.key == keys[i]) {
keypad[i] = true;
}
}
} else if (e.type == SDL_EVENT_KEY_UP) {
for (auto i{0}; i < 16; ++i) {
if (e.key.key == keys[i]) {
keypad[i] = false;
}
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
#include "SDL3/SDL_audio.h"
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_log.h"
#include "SDL3/SDL_render.h"
#include "SDL3/SDL_stdinc.h"
#include "SDL3/SDL_surface.h"
#include "SDL3/SDL_video.h"
#include "keypad.hpp"
#include "utils.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <array>
class Screen {
public:
Screen(int w, int h);
~Screen();
void render(u32 *buffer, int pitch);
void beep();
void handler(bool &quit, Keypad &keypad);
private:
Uint8 *buf;
Uint32 len;
SDL_AudioStream *stream;
SDL_Window *window;
SDL_Renderer *renderer{nullptr};
SDL_Texture *texture;
};
+25
View File
@@ -0,0 +1,25 @@
#include "stack.hpp"
Stack::Stack() {}
Stack::~Stack() {}
bool Stack::is_empty() {
return v.empty();
}
void Stack::push(u16 element) {
v.push_back(element);
}
u16 Stack::pop() {
u16 top{v.back()};
v.pop_back();
return top;
}
int Stack::size() {
return v.size();
}
u16 Stack::top() {
return v.back();
}
+17
View File
@@ -0,0 +1,17 @@
#include <cstdint>
#include <vector>
using u16 = std::uint16_t;
class Stack {
public:
Stack();
~Stack();
void push(u16 element);
u16 pop();
u16 top();
bool is_empty();
int size();
private:
std::vector<u16> v;
};
+20
View File
@@ -0,0 +1,20 @@
#include "utils.hpp"
#include <SDL3/SDL_log.h>
#include <string>
#include <vector>
std::vector<std::string> convert_args(int argc, char **argv) {
std::vector<std::string> v(argv, argv + argc);
return v;
}
void modify_pixels(bool *src, u32 *dst) {
for (auto i{0}; i < 2048; ++i) {
if (src[i]) {
dst[i] = 0xFFFFFFFF;
} else {
dst[i] = 0x00000000;
}
}
}
+65
View File
@@ -0,0 +1,65 @@
#ifndef UTILS_H
#define UTILS_H
#include <SDL3/SDL_keycode.h>
#include <array>
#include <cstdint>
#include <string>
#include <vector>
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
constexpr unsigned int MEMORY_SIZE = 4096;
constexpr unsigned int REGISTER_NUMBER = 16;
constexpr unsigned int FONT_SIZE = 80;
constexpr unsigned int START_ADDRESS = 0x200;
constexpr unsigned int FONTSET_START_ADDRESS = 0x050;
constexpr unsigned int DISPLAY_WIDTH = 64;
constexpr unsigned int DISPLAY_HEIGHT = 32;
using pixels = std::array<u32, DISPLAY_WIDTH * DISPLAY_HEIGHT>;
constexpr std::array<u8, FONT_SIZE> font = {
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
};
constexpr std::array<u8, 16> keys{
SDLK_X, // 0
SDLK_1, // 1
SDLK_2, // 2
SDLK_3, // 3
SDLK_Q, // 4
SDLK_W, // 5
SDLK_E, // 6
SDLK_A, // 7
SDLK_S, // 8
SDLK_D, // 9
SDLK_Z, // A
SDLK_C, // B
SDLK_4, // C
SDLK_R, // D
SDLK_F, // E
SDLK_V // F
};
std::vector<std::string> convert_args(int argc, char **argv);
u32 *turn_to_pixels(bool *src);
void modify_pixels(bool *src, u32 *dst);
#endif // !UTILS_H