Fix handling of several instructions

We are now handling all instructions we have implmeented, and their respective flags, correctly and we now pass test 3 and 4 from Timendus' CHIP-8 test suite!
This commit is contained in:
Zoe
2025-02-06 20:04:56 +00:00
parent 781ca3c8e9
commit 8a682699f6
9 changed files with 317 additions and 67 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
obj/
bin/
ram.bin
out.txt

View File

@@ -15,10 +15,10 @@ disassembler: $(wildcard disassembler/*.cpp) | $(BIN_DIR)
voidEmu: $(wildcard src/*.cpp) | $(BIN_DIR)
$(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ $(LDFLAGS)
$(BIN_DIR) $(OBJ_DIR):
$(BIN_DIR):
mkdir -p $@
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR)
rm -rf $(BIN_DIR)
.PHONY: all clean run

View File

@@ -4,6 +4,14 @@ Current state: This project is a Chip8 emulator implemented according to [Cowgod
This is a simple emulator for the Game Boy. It is written in C++ and uses SDL for rendering.
## TODO
- [ ] Implement sound
- [ ] Implement keyboard input
- [ ] Implement better e2e testing for visuals and other things
- [ ] Implement a disassembler
- [ ] Get better debugging
## Why
I wanted to learn how to use C++ and SDL, and I recently saw a youtube video listing out reasons for writing a Game Boy emulator. The name is VoidEmu, because I gurantee I will be staring into the void trying to make this thing work.
@@ -15,4 +23,4 @@ I wanted to learn how to use C++ and SDL, and I recently saw a youtube video lis
# License
This is free and unencumbered software released into the public domain, much to the detriment of my "heirs and successors"
This is free and unencumbered software released into the public domain, much to the detriment of my "heirs and successors". Unlicense everything

View File

@@ -1,3 +1,8 @@
CompileFlags:
Add:
- -I../lib
- -I../libs
- -Wall
- -Wextra
- -std=c++23
- -g
- -Werror

View File

@@ -1,3 +1,4 @@
#define BYTECODE_READER_IMPLEMENTATION
#include "reader.hpp"
#include <cstdio>

View File

@@ -113,7 +113,7 @@ class Bytecode {
};
inline Bytecode parse(uint16_t opcode) {
struct Bytecode bytecode;
Bytecode bytecode;
switch (opcode & 0xF000) {
case 0x0000: {

View File

@@ -2,3 +2,8 @@ CompileFlags:
Add:
- -I../libs
- -ISDL2
- -Wall
- -Wextra
- -std=c++23
- -g
- -Werror

View File

@@ -17,13 +17,14 @@
const int SCREEN_WIDTH = 64;
const int SCREEN_HEIGHT = 32;
const int SCALE = 5;
static size_t RAM_SIZE = 0x1000;
const size_t RAM_SIZE = 0x1000;
const int TARGET_CYCLES_PER_SECOND = 500;
const int TARGET_MS_PER_CYCLE = 1000 / TARGET_CYCLES_PER_SECOND;
const int TARGET_MS_PER_FRAME = 1000 / 60;
const int BG_COLOR = 0x081820;
const int FG_COLOR = 0x88c070;
const int TARGET_FRAMERATE = 60;
const int TARGET_MS_PER_FRAME = 1000 / TARGET_FRAMERATE;
static int SCALE = 10;
static int BG_COLOR = 0x081820;
static int FG_COLOR = 0x88c070;
void draw(SDL_Renderer *renderer, SDL_Texture *texture,
bool framebuffer[SCREEN_HEIGHT][SCREEN_WIDTH]) {
@@ -115,8 +116,20 @@ class Chip8 {
int run();
void view_ram();
void dump_ram();
void view_stack();
void dump_fb(int);
int is_protected(size_t addr) { return addr < 0x200; }
// allow the font to be read
bool is_protected(size_t addr) {
if (addr <= sizeof(FONT))
return false;
return addr < 0x200;
}
// only allow addresses in the program space to be executed, addresses not
// protected, but in the reserved space, for example, the font, should not
// be executable
bool is_executable(size_t addr) { return addr > 0x1FF; }
int read_mem(size_t addr) {
if (is_protected(addr)) {
@@ -165,7 +178,7 @@ class Chip8 {
uint16_t i;
uint8_t delay;
uint8_t sound_timer;
bool compat;
// bool compat;
};
int Chip8::run() {
@@ -203,6 +216,11 @@ int Chip8::run() {
auto start_time = high_resolution_clock::now();
uint16_t op = (read_mem(pc) << 8) | read_mem(pc + 1);
if (!is_executable(pc)) {
printf("Attempted to execute protected memory at 0x%04x\n", pc);
view_ram();
return 1;
}
pc += 2;
printf("PC: 0x%04x OP: 0x%04x\n", pc, op);
Bytecode bytecode = parse(op);
@@ -240,6 +258,8 @@ int Chip8::run() {
// The interpreter sets the program counter to the address at the
// top of the stack, then subtracts 1 from the stack pointer.
// --sp subtracts 1 from the stack pointer and returns the value
// after the subraction
pc = stack[--sp];
break;
}
@@ -258,7 +278,9 @@ int Chip8::run() {
// The interpreter increments the stack pointer, then puts the
// current PC on the top of the stack. The PC is then set to nnn.
stack[++sp] = pc;
// sp++ increments the stack pointer and returns the value before
// the addition
stack[sp++] = pc;
pc = bytecode.operand.word & 0x0FFF;
break;
}
@@ -334,7 +356,13 @@ int Chip8::run() {
int result = this->v[bytecode.operand.reg_reg.x] +
this->v[bytecode.operand.reg_reg.y];
this->v[bytecode.operand.reg_reg.x] = result & 0xFF;
this->v[0xF] = result > 0xFF;
if (result > 0xFF) {
printf("Overflowed!\n");
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
break;
}
@@ -342,10 +370,34 @@ int Chip8::run() {
// Set Vx = Vx - Vy, set VF = NOT borrow.
// If Vx > Vy, then VF is set to 1, otherwise 0. Then Vy is
// subtracted from Vx, and the results stored in Vx.
int result = this->v[bytecode.operand.reg_reg.x] -
this->v[bytecode.operand.reg_reg.y];
this->v[bytecode.operand.reg_reg.x] = result & 0xFF;
this->v[0xF] = result < 0;
printf("Subtracting %d - %d (v%x - v%x)\n",
this->v[bytecode.operand.reg_reg.x],
this->v[bytecode.operand.reg_reg.y],
bytecode.operand.reg_reg.x, bytecode.operand.reg_reg.y);
bool borrow = this->v[bytecode.operand.reg_reg.x] >=
this->v[bytecode.operand.reg_reg.y];
this->v[bytecode.operand.reg_reg.x] =
(this->v[bytecode.operand.reg_reg.x] -
this->v[bytecode.operand.reg_reg.y]) &
0xFF;
if (borrow) {
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
if (borrow) {
printf("Overflowed!\n");
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
printf("Resulting in %d (v%x) with %s\n",
this->v[bytecode.operand.reg_reg.x],
bytecode.operand.reg_reg.x, borrow ? "borrow" : "no borrow");
break;
}
case OR_REG: {
@@ -386,28 +438,60 @@ int Chip8::run() {
// Set Vx = Vx SHR 1.
// If the least-significant bit of Vx is 1, then VF is set to 1,
// otherwise 0. Then Vx is divided by 2.
this->v[0xF] = this->v[bytecode.operand.reg_reg.x] & 1;
this->v[bytecode.operand.reg_reg.x] =
this->v[bytecode.operand.reg_reg.x] >> 1;
bool carry = this->v[bytecode.operand.reg_reg.x] & 0x01;
this->v[bytecode.operand.reg_reg.x] >>= 1;
if (carry) {
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
break;
}
case SUBN_REG: {
// Set Vx = Vy - Vx, set VF = NOT borrow.
// If Vy > Vx, then VF is set to 1, otherwise 0. Then Vx is
// subtracted from Vy, and the results stored in Vx.
int result = this->v[bytecode.operand.reg_reg.y] -
this->v[bytecode.operand.reg_reg.x];
this->v[bytecode.operand.reg_reg.x] = result & 0xFF;
this->v[0xF] = result < 0;
printf("Subtracting %d - %d (v%x - v%x)\n",
this->v[bytecode.operand.reg_reg.y],
this->v[bytecode.operand.reg_reg.x],
bytecode.operand.reg_reg.y, bytecode.operand.reg_reg.x);
bool borrow = this->v[bytecode.operand.reg_reg.y] >=
this->v[bytecode.operand.reg_reg.x];
this->v[bytecode.operand.reg_reg.x] =
(this->v[bytecode.operand.reg_reg.y] -
this->v[bytecode.operand.reg_reg.x]) &
0xFF;
if (borrow) {
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
if (borrow) {
printf("Overflowed!\n");
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
printf("Resulting in %d (v%x) with %s\n",
this->v[bytecode.operand.reg_reg.x],
bytecode.operand.reg_reg.x, borrow ? "borrow" : "no borrow");
break;
}
case SHL_REG: {
// Set Vx = Vx SHL 1.
// If the most-significant bit of Vx is 1, then VF is set to 1,
// otherwise to 0. Then Vx is multiplied by 2.
this->v[0xF] = this->v[bytecode.operand.reg_reg.x] >> 7;
this->v[bytecode.operand.reg_reg.x] =
this->v[bytecode.operand.reg_reg.x] << 1;
bool carry = (this->v[bytecode.operand.reg_reg.x] >> 7) & 0x01;
this->v[bytecode.operand.reg_reg.x] <<= 1;
if (carry) {
this->v[0xF] = 1;
} else {
this->v[0xF] = 0;
}
break;
}
case LOAD_I_BYTE: {
@@ -443,7 +527,6 @@ int Chip8::run() {
// around to the opposite side of the screen. See instruction 8xy3
// for more information on XOR, and section 2.4, Display, for more
// information on the Chip-8 screen and sprites.
// fprintf(stderr, "DRW not implemented\n");
this->v[0x0F] = 0;
for (int i = 0; i < bytecode.operand.reg_reg_nibble.nibble; i++) {
@@ -455,9 +538,6 @@ int Chip8::run() {
SCREEN_WIDTH;
int y = (this->v[bytecode.operand.reg_reg_nibble.y] + i) %
SCREEN_HEIGHT;
printf("Sprite %d, bit %d %d\n", i, j,
sprite & (0x80 >> j));
if (!source) {
continue;
}
@@ -527,9 +607,9 @@ int Chip8::run() {
// corresponding to the value of Vx. See section 2.4, Display, for
// more information on the Chip-8 hexadecimal font.
//? This is the ONLY spot where the emulator is allowed to access
//? 0x0000-0x01FF of the RAM. Since that area of RAM is reserved for
//? the emulator's own use.
//? This is the ONLY spot in 0x0000-0x01FF of the RAM where the
//? emulator is allowed to access. Since that area of RAM is
//? where the font is stored.
this->i = (uint16_t)(bytecode.operand.byte * 5);
break;
}
@@ -540,13 +620,16 @@ int Chip8::run() {
// hundreds digit in memory at location in I, the tens digit at
// location I+1, and the ones digit at location I+2.
// TODO: is this correct?
this->ram[this->i] =
(uint8_t)((this->v[bytecode.operand.reg_reg.x] / 100) & 0x0F);
this->ram[this->i + 1] =
write_mem(
this->i,
(uint8_t)((this->v[bytecode.operand.reg_reg.x] / 100) & 0x0F));
write_mem(
this->i + 1,
(uint8_t)((this->v[bytecode.operand.reg_reg.x] % 100) / 10) &
0x0F;
this->ram[this->i + 2] =
(uint8_t)(this->v[bytecode.operand.reg_reg.x] % 10) & 0x0F;
0x0F);
write_mem(this->i + 2,
(uint8_t)(this->v[bytecode.operand.reg_reg.x] % 10) &
0x0F);
break;
}
case LD_PTR_I_REG: {
@@ -554,8 +637,8 @@ int Chip8::run() {
// location I.
// The interpreter copies the values of registers V0 through Vx into
// memory, starting at the address in I.
for (int i = 0; i < bytecode.operand.reg_reg.x; i++) {
this->ram[this->i + i] = this->v[i];
for (int i = 0; i <= bytecode.operand.byte; i++) {
write_mem(this->i + i, this->v[i]);
}
break;
}
@@ -564,8 +647,8 @@ int Chip8::run() {
// location I.
// The interpreter reads values from memory starting at location I
// into registers V0 through Vx.
for (int i = 0; i < bytecode.operand.reg_reg.x; i++) {
this->v[i] = this->ram[this->i + i];
for (int i = 0; i <= bytecode.operand.byte; i++) {
this->v[i] = read_mem(this->i + i);
}
break;
}
@@ -578,16 +661,21 @@ int Chip8::run() {
}
}
printf("Emulating...\n");
auto now = high_resolution_clock::now();
// 60hz clock
if (duration_cast<milliseconds>(now - last_timer_update) >=
timer_interval) {
printf("Updating...\n");
if (delay > 0)
if (delay > 0) {
--delay;
if (sound_timer > 0)
}
if (sound_timer > 0) {
--sound_timer;
}
// SDL technically has an SDL_Delay function thing, but doing it
// this way allows us to be more flexible and more away from SDL in
// the future if we wanted to.
draw(renderer, texture, this->fb);
last_timer_update = now;
}
@@ -610,6 +698,8 @@ int Chip8::run() {
void Chip8::view_ram() {
printf("Hex dump:\n");
for (size_t i = 0; i < RAM_SIZE / 16; i++) {
printf("%04x: ", (unsigned int)(i * 16));
size_t j = 0;
for (; j < 16; j++) {
printf("%02x ", this->ram[i * 16 + j]);
@@ -640,15 +730,121 @@ void Chip8::dump_ram() {
fclose(fp);
}
int main(int argc, char **argv) {
signal(SIGINT, exit);
void Chip8::view_stack() {
printf("Stack:\n");
for (int i = 0; i < 16; i++) {
printf("%04x ", stack[i]);
}
printf("\n");
}
void Chip8::dump_fb(int _) {
(void)_;
FILE *fp = fopen("fb.dump", "wb");
if (fp == NULL) {
printf("Failed to open file\n");
exit(1);
}
fwrite(fb, SCREEN_HEIGHT * SCREEN_WIDTH, 1, fp);
fclose(fp);
}
void destroy(int _) {
(void)_;
if (!SDL_WasInit(SDL_INIT_VIDEO)) {
exit(0);
}
SDL_Event event;
event.type = SDL_QUIT;
SDL_PushEvent(&event);
exit(0);
}
int main(int argc, char **argv) {
signal(SIGINT, destroy);
if (argc < 2) {
printf("Usage: %s <file>\n", argv[0]);
printf("Usage: %s <file> [options]\n", argv[0]);
return 1;
}
Chip8 chip8 = Chip8(argv[1]);
char *file_name = NULL;
// is this memory safe?
// start from 1 to skip the first argument (the executable)
for (int i = 1; i < argc; i++) {
if (argc < i) {
printf("exceeded argc\n");
}
if (strcmp(argv[i], "--scale") == 0) {
if (argc < i + 1) {
printf("Error: Missing scale value\n");
return 1;
}
long scale = strtol(argv[i + 1], NULL, 10);
if (scale < 1 || scale > 50) {
printf("Error: Invalid scale value\n");
return 1;
}
SCALE = scale;
i++;
continue;
}
if (strcmp(argv[i], "--bg") == 0) {
if (argc < i + 1) {
printf("Error: Missing bg value\n");
return 1;
}
long bg = strtol(argv[i + 1], NULL, 16);
if (bg < 0 || bg > 0xFFFFFF) {
printf("Error: Invalid bg value\n");
return 1;
}
// RSH by 8 to correct for the alpha channel
BG_COLOR = (int)(bg << 8);
i++;
continue;
}
if (strcmp(argv[i], "--fg") == 0) {
if (argc < i + 1) {
printf("Error: Missing fg value\n");
return 1;
}
long fg = strtol(argv[i + 1], NULL, 16);
if (fg < 0 || fg > 0xFFFFFF) {
printf("Error: Invalid fg value\n");
return 1;
}
// RSH by 8 to correct for the alpha channel
FG_COLOR = (int)(fg << 8);
i++;
continue;
}
// if the argument does not start with a dash, assume it is a file
if (argv[i][0] != '-') {
printf("Filename: %s\n", argv[i]);
file_name = argv[i];
continue;
}
}
Chip8 chip8 = Chip8(file_name);
chip8.view_ram();
int ret = chip8.run();

62
test.sh
View File

@@ -1,35 +1,69 @@
#!/bin/bash
TIMEOUT=2
if [ ! -d tests/extern ]; then
mkdir tests/extern
mkdir -p tests/extern
fi
extern_roms=("https://github.com/Timendus/chip8-test-suite/raw/main/bin/1-chip8-logo.ch8" "https://github.com/Timendus/chip8-test-suite/raw/main/bin/2-ibm-logo.ch8")
extern_roms=(
"https://github.com/Timendus/chip8-test-suite/raw/main/bin/1-chip8-logo.ch8"
"https://github.com/Timendus/chip8-test-suite/raw/main/bin/2-ibm-logo.ch8"
"https://github.com/Timendus/chip8-test-suite/raw/main/bin/3-corax+.ch8"
"https://github.com/Timendus/chip8-test-suite/raw/main/bin/4-flags.ch8"
)
for rom in "${extern_roms[@]}"; do
if [ ! -f tests/extern/$(basename $rom) ]; then
file="tests/extern/$(basename "$rom")"
if [ ! -f "$file" ]; then
echo "Downloading $rom"
curl -L $rom -o tests/extern/$(basename $rom)
curl -L "$rom" -o "$file"
fi
done
roms=($(find tests/ -type f -name "*.ch8"))
if [ "$1" == "--download-only" ]; then
exit 0
fi
roms=($(find tests/ -type f -name "*.ch8"))
testOutput=$(mktemp)
if ${VERBOSE:-false}; then
testOutput=2
fi
for rom in "${roms[@]}"; do
echo -n "$rom "
./bin/voidEmu $rom 1>&$testOutput
if [ $? -ne 0 ]; then
echo -en "\x1b[2K\r"
cat $testOutput
echo -e "$rom \x1b[97m[\x1b[1;31mFAIL\x1b[97m]\x1b[0m"
exit 1
else
echo -e "\x1b[97m[\x1b[1;32mPASS\x1b[97m]\x1b[0m"
./bin/voidEmu "$rom" $@ 1>&$testOutput &
pid=$!
SECONDS=0
while kill -0 $pid 2>/dev/null && [ $SECONDS -lt $TIMEOUT ]; do
sleep 0.1
done
if ! kill -0 $pid 2>/dev/null; then
wait $pid
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo -e "$rom \x1b[97m[\x1b[1;32mPASS\x1b[97m]\x1b[0m (Exited Successfully)"
continue
else
echo -e "$rom \x1b[97m[\x1b[1;31mFAIL\x1b[97m]\x1b[0m (Exited with code $exit_code)"
echo "Output:"
cat $testOutput
exit 1
fi
fi
echo "Press Enter to confirm PASS, or Ctrl-C to FAIL..."
read -r
if kill -0 $pid 2>/dev/null; then
kill $pid
wait $pid 2>/dev/null
fi
echo -e "$rom \x1b[97m[\x1b[1;32mPASS\x1b[97m]\x1b[0m"
done