commit 91fb30ddc269c1b247c724c302814bb72b2b7ed2 Author: Zoe <62722391+juls0730@users.noreply.github.com> Date: Thu Feb 6 00:24:52 2025 -0600 Initial commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..bb29a07 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ + +BasedOnStyle: LLVM +IndentWidth: 4 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c9a5f2b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Set the default behavior for all files. +* text=auto eol=lf + +# Normalized and converts to native line endings on checkout. +*.c text +*.cc text +*.cxx +*.cpp text +*.h text +*.hxx text +*.hpp text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbe74a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +obj/ +bin/ +ram.bin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b3dbff0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3089313 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +CXX := g++ +CXXFLAGS := -Wall -Wextra -std=c++23 -g -Werror -Ilibs `sdl2-config --cflags` +LDFLAGS := `sdl2-config --libs` + +BIN_DIR := bin + +all: voidEmu disassembler + +run: all + ./bin/voidEmu $(FILE) + +disassembler: $(wildcard disassembler/*.cpp) | $(BIN_DIR) + $(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ + +voidEmu: $(wildcard src/*.cpp) | $(BIN_DIR) + $(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ $(LDFLAGS) + +$(BIN_DIR) $(OBJ_DIR): + mkdir -p $@ + +clean: + rm -rf $(OBJ_DIR) $(BIN_DIR) + +.PHONY: all clean run diff --git a/README.md b/README.md new file mode 100644 index 0000000..88df69b --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# VoidEmu + +Current state: This project is a Chip8 emulator implemented according to [Cowgod's Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.2). + +This is a simple emulator for the Game Boy. It is written in C++ and uses SDL for rendering. + +## 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. + +## References + +* [Cowgod's Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM) + +# License + +This is free and unencumbered software released into the public domain, much to the detriment of my "heirs and successors" \ No newline at end of file diff --git a/disassembler/.clangd b/disassembler/.clangd new file mode 100644 index 0000000..bb28cf2 --- /dev/null +++ b/disassembler/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Add: + - -I../lib diff --git a/disassembler/main.cpp b/disassembler/main.cpp new file mode 100644 index 0000000..721b288 --- /dev/null +++ b/disassembler/main.cpp @@ -0,0 +1,8 @@ +#include "reader.hpp" + +#include + +int main() { + printf("Hello World!\n"); + return 0; +} \ No newline at end of file diff --git a/libs/reader.hpp b/libs/reader.hpp new file mode 100644 index 0000000..1cf694c --- /dev/null +++ b/libs/reader.hpp @@ -0,0 +1,389 @@ +#include +#include +#include +#include +#include + +struct byte_reg { + uint8_t reg; + uint16_t byte; +}; + +struct reg_reg { + uint8_t x; + uint8_t y; +}; + +struct reg_reg_nibble { + uint8_t x; + uint8_t y; + uint8_t nibble; +}; + +// this definitely wont be confusing or difficult to implement at all, I am +// perfect at writing good code and documentation and I would never write +// something that is stupidly thought out or overly complicated +enum instruction { + // operand is a pointer to a uint8_t + EXIT = 0, + // operand is a pointer to a uint16_t + SYS, + // no operand + CLS, + // no operand + RET, + // operand is a pointer to a uint16_t + JP, + // operand is a pointer to a uint16_t + CALL, + // operand is a pointer to byte_reg + SKIP_INSTRUCTION_BYTE, + // operand is a pointer to byte_reg + SKIP_INSTRUCTION_NE_BYTE, + // operand is a pointer to reg_reg + SKIP_INSTRUCTION_REG, + // operand is a pointer to reg_byte + LOAD_BYTE, + // operand is a pointer to reg_byte + ADD_BYTE, + // operand is a pointer to reg_reg + LOAD_REG, + // operand is a pointer to reg_reg + OR_REG, + // operand is a pointer to reg_reg + AND_REG, + // operand is a pointer to reg_reg + XOR_REG, + // operand is a pointer to reg_reg + ADD_REG, + // operand is a pointer to reg_reg + SUB_REG, + // operand is a pointer to reg_reg + SHR_REG, + // operand is a pointer to reg_reg + SUBN_REG, + // operand is a pointer to reg_reg + SHL_REG, + // operand is a pointer to reg_reg + SKIP_INSTRUCTION_NE_REG, + // operand is a pointer to a uint16_t + LOAD_I_BYTE, + // operand is a pointer to a uint16_t + JP_V0_BYTE, + // operand is a pointer to a reg_byte + RND, + // operand is a pointer to a reg_reg_nibble + DRW, + // operand is a pointer to a uint8_t + SKIP_PRESSED_REG, + // operand is a pointer to a uint8_t + SKIP_NOT_PRESSED_REG, + // operand is a pointer to a uint8_t + LD_REG_DT, + // operand is a pointer to a uint8_t + LD_REG_K, + // operand is a pointer to a uint8_t + LD_DT_REG, + // operand is a pointer to a uint8_t + LD_ST_REG, + // operand is a pointer to a uint8_t + ADD_I_REG, + // operand is a pointer to a uint8_t + LD_F_REG, + // operand is a pointer to a uint8_t + LD_B_REG, + // operand is a pointer to a uint8_t + LD_PTR_I_REG, + // operand is a pointer to a uint8_t + LD_REG_PTR_I, + UNKNOWN_INSTRUCTION, +}; + +class Bytecode { + public: + enum instruction instruction_type; + // should be interpreted by a reader depending on the instruction type + union { + uint8_t byte; + uint16_t word; + struct byte_reg byte_reg; + struct reg_reg reg_reg; + struct reg_reg_nibble reg_reg_nibble; + } operand; +}; + +inline Bytecode parse(uint16_t opcode) { + struct Bytecode bytecode; + + switch (opcode & 0xF000) { + case 0x0000: { + if ((opcode & 0x00F0) == 0x0010) { + // EXIT N 0x001N + // Specific to emulators, not part of the original chip-8 + bytecode.instruction_type = EXIT; + break; + } + + switch (opcode & 0x00FF) { + case 0x00E0: { + // CLS 0x00E0 + // clears the screen + bytecode.instruction_type = CLS; + break; + } + case 0x00EE: { + // RET 0x00EE + bytecode.instruction_type = RET; + break; + } + default: + // SYS NNN + //? NOTE: This is an outdated opcode, but it's still + //? important for completeness. It's not clear what the + //? difference is between it and the JMP NNN 0x1NNN opcode. + bytecode.instruction_type = SYS; + break; + } + + break; + } + case 0x1000: { + bytecode.instruction_type = JP; + break; + } + case 0x2000: { + bytecode.instruction_type = CALL; + break; + } + case 0x3000: { + bytecode.instruction_type = SKIP_INSTRUCTION_BYTE; + break; + } + case 0x4000: { + bytecode.instruction_type = SKIP_INSTRUCTION_NE_BYTE; + break; + } + case 0x5000: { + bytecode.instruction_type = SKIP_INSTRUCTION_REG; + break; + } + case 0x6000: { + bytecode.instruction_type = LOAD_BYTE; + break; + } + case 0x700: { + bytecode.instruction_type = ADD_BYTE; + break; + } + case 0x8000: { + switch (opcode & 0x000F) { + case 0x0000: { + bytecode.instruction_type = LOAD_REG; + break; + } + case 0x0001: { + bytecode.instruction_type = OR_REG; + break; + } + case 0x0002: { + bytecode.instruction_type = AND_REG; + break; + } + case 0x0003: { + bytecode.instruction_type = XOR_REG; + break; + } + case 0x0004: { + bytecode.instruction_type = ADD_REG; + break; + } + case 0x0005: { + bytecode.instruction_type = SUB_REG; + break; + } + case 0x0006: { + // Set VX equal to VX bitshifted right 1. VF is set to the least + // significant bit of VX prior to the shift. Originally this opcode + // meant set VX equal to VY bitshifted right 1 but emulators and + // software seem to ignore VY now. Note: This instruction was + // originally undocumented but functional due to how the 8XXX + // instructions were implemented on teh COSMAC VIP. + bytecode.instruction_type = SHR_REG; + break; + } + case 0x0007: { + bytecode.instruction_type = SUBN_REG; + break; + } + case 0x000E: { + bytecode.instruction_type = SHL_REG; + break; + } + default: { + bytecode.instruction_type = UNKNOWN_INSTRUCTION; + break; + } + } + break; + } + case 0x9000: { + bytecode.instruction_type = SKIP_INSTRUCTION_NE_REG; + break; + } + case 0xA000: { + bytecode.instruction_type = LOAD_I_BYTE; + break; + } + case 0xB000: { + bytecode.instruction_type = JP_V0_BYTE; + break; + } + case 0xC000: { + bytecode.instruction_type = RND; + break; + } + case 0xD000: { + bytecode.instruction_type = DRW; + break; + } + case 0xE000: { + switch (opcode & 0x00FF) { + case 0x009E: { + bytecode.instruction_type = SKIP_PRESSED_REG; + break; + } + case 0x00A1: { + bytecode.instruction_type = SKIP_NOT_PRESSED_REG; + break; + } + default: { + bytecode.instruction_type = UNKNOWN_INSTRUCTION; + break; + } + } + break; + } + case 0xF000: { + switch (opcode & 0x00FF) { + case 0x0007: { + bytecode.instruction_type = LD_REG_DT; + break; + } + case 0x000A: { + bytecode.instruction_type = LD_REG_K; + break; + } + case 0x0015: { + bytecode.instruction_type = LD_DT_REG; + break; + } + case 0x0018: { + bytecode.instruction_type = LD_ST_REG; + break; + } + case 0x001E: { + bytecode.instruction_type = ADD_I_REG; + break; + } + case 0x0029: { + bytecode.instruction_type = LD_F_REG; + break; + } + case 0x0033: { + bytecode.instruction_type = LD_B_REG; + break; + } + case 0x0055: { + bytecode.instruction_type = LD_PTR_I_REG; + break; + } + case 0x0065: { + bytecode.instruction_type = LD_REG_PTR_I; + break; + } + default: { + bytecode.instruction_type = UNKNOWN_INSTRUCTION; + break; + } + } + break; + } + default: { + bytecode.instruction_type = UNKNOWN_INSTRUCTION; + break; + } + } + + switch (bytecode.instruction_type) { + case UNKNOWN_INSTRUCTION: + case RET: + case CLS: { + // no operand + break; + } + case EXIT: { + bytecode.operand.byte = opcode & 0x000F; + break; + } + case SKIP_PRESSED_REG: + case SKIP_NOT_PRESSED_REG: + case LD_REG_DT: + case LD_REG_K: + case LD_DT_REG: + case LD_ST_REG: + case ADD_I_REG: + case LD_F_REG: + case LD_B_REG: + case LD_PTR_I_REG: + case LD_REG_PTR_I: { + bytecode.operand.byte = (uint8_t)((opcode & 0x0F00) >> 8); + break; + } + case SYS: + case JP: + case CALL: + case LOAD_I_BYTE: + case JP_V0_BYTE: { + bytecode.operand.word = (uint16_t)(opcode & 0x0FFF); + break; + } + case SKIP_INSTRUCTION_BYTE: + case SKIP_INSTRUCTION_NE_BYTE: + case LOAD_BYTE: + case ADD_BYTE: + case RND: { + bytecode.operand.byte_reg = (struct byte_reg){ + .reg = (uint8_t)((opcode & 0x0F00) >> 8), + .byte = (uint16_t)(opcode & 0x00FF), + }; + break; + } + case SKIP_INSTRUCTION_REG: + case LOAD_REG: + case OR_REG: + case AND_REG: + case XOR_REG: + case ADD_REG: + case SUB_REG: + case SHR_REG: + case SUBN_REG: + case SHL_REG: + case SKIP_INSTRUCTION_NE_REG: { + bytecode.operand.reg_reg = (struct reg_reg){ + .x = (uint8_t)((opcode & 0x0F00) >> 8), + .y = (uint8_t)((opcode & 0x00F0) >> 4), + }; + break; + } + case DRW: { + bytecode.operand.reg_reg_nibble = (struct reg_reg_nibble){ + .x = (uint8_t)((opcode & 0x0F00) >> 8), + .y = (uint8_t)((opcode & 0x00F0) >> 4), + .nibble = (uint8_t)(opcode & 0x000F), + }; + break; + } + } + + return bytecode; +} diff --git a/src/.clangd b/src/.clangd new file mode 100644 index 0000000..0aa9308 --- /dev/null +++ b/src/.clangd @@ -0,0 +1,4 @@ +CompileFlags: + Add: + - -I../libs + - -ISDL2 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ab58a9e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,656 @@ +#include +#include +#include + +#define BYTECODE_READER_IMPLEMENTATION +#include "reader.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int SCREEN_WIDTH = 64; +const int SCREEN_HEIGHT = 32; +const int SCALE = 5; +static 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; + +void draw(SDL_Renderer *renderer, SDL_Texture *texture, + bool framebuffer[SCREEN_HEIGHT][SCREEN_WIDTH]) { + + printf("Drawing...\n"); + + uint32_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT]; + for (int i = 0; i < SCREEN_HEIGHT; i++) { + for (int j = 0; j < SCREEN_WIDTH; j++) { + pixels[i * SCREEN_WIDTH + j] = + framebuffer[i][j] ? FG_COLOR : BG_COLOR; + } + } + + SDL_UpdateTexture(texture, nullptr, pixels, + SCREEN_WIDTH * sizeof(uint32_t)); + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, texture, nullptr, nullptr); + SDL_RenderPresent(renderer); +} + +static uint8_t FONT[0x10][0x05] = { + {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 +}; + +class Chip8 { + public: + Chip8(char *rom_path) { + int rom_fd = open(rom_path, O_RDONLY); + if (rom_fd < 0) { + printf("Failed to open file: %s\n", rom_path); + exit(1); + } + + pc = 0x200; + ram = (uint8_t *)malloc(RAM_SIZE); + if (ram == NULL) { + printf("Failed to allocate ram!"); + exit(1); + } + + memcpy(ram, FONT, sizeof(FONT)); + + int file_size = lseek(rom_fd, 0, SEEK_END); + (void)lseek(rom_fd, 0, SEEK_SET); + + printf("Reading file: %s for %d bytes\n", rom_path, file_size); + + int err = read(rom_fd, ram + 0x200, file_size); + if (err < 0) { + printf("Failed to read file: %s\n", rom_path); + exit(1); + } + + if (err != file_size) { + printf("Failed to read file: %s\n", rom_path); + exit(1); + } + + close(rom_fd); + + stack = (uint16_t *)malloc(sizeof(uint16_t) * 16); + if (stack == NULL) { + printf("Failed to allocate stack!"); + exit(1); + } + } + + ~Chip8() { + free(ram); + free(stack); + } + + int run(); + void view_ram(); + void dump_ram(); + + int is_protected(size_t addr) { return addr < 0x200; } + + int read_mem(size_t addr) { + if (is_protected(addr)) { + printf("Attempted to read from protected address: 0x%04x\n", + (unsigned int)addr); + dump_ram(); + exit(1); + } + return this->ram[addr]; + } + + void write_mem(size_t addr, uint8_t val) { + if (is_protected(addr)) { + printf("Attempted to write to protected address: 0x%04x\n", + (unsigned int)addr); + dump_ram(); + exit(1); + } + this->ram[addr] = val; + } + + void set_sound_timer(uint8_t val) { + // enable buzzer + this->sound_timer = val; + } + + void set_pixel(int x, int y, uint8_t val) { + assert(x >= 0 && x < SCREEN_WIDTH); + assert(y >= 0 && y < SCREEN_HEIGHT); + this->fb[y][x] = val; + } + + uint8_t get_pixel(int x, int y) { + assert(x >= 0 && x < SCREEN_WIDTH); + assert(y >= 0 && y < SCREEN_HEIGHT); + return this->fb[y][x]; + } + + private: + uint8_t *ram; + bool fb[SCREEN_HEIGHT][SCREEN_WIDTH]; + uint16_t pc; + uint16_t *stack; + uint8_t sp; + uint8_t v[16]; + uint16_t i; + uint8_t delay; + uint8_t sound_timer; + bool compat; +}; + +int Chip8::run() { + using namespace std::chrono; + + int exit = 0; + constexpr auto cycle_time = milliseconds(TARGET_MS_PER_CYCLE); + constexpr auto timer_interval = milliseconds(TARGET_MS_PER_FRAME); + auto last_timer_update = high_resolution_clock::now(); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("Failed to initialize SDL: %s\n", SDL_GetError()); + return 1; + } + + SDL_Window *window = SDL_CreateWindow( + "CHIP-8 Emulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL_WINDOW_SHOWN); + SDL_Renderer *renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_STREAMING, + SCREEN_WIDTH, SCREEN_HEIGHT); + + bool running = true; + SDL_Event event; + + while (running) { + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + running = false; + } + } + + auto start_time = high_resolution_clock::now(); + + uint16_t op = (read_mem(pc) << 8) | read_mem(pc + 1); + pc += 2; + printf("PC: 0x%04x OP: 0x%04x\n", pc, op); + Bytecode bytecode = parse(op); + + printf("OPCODE: 0x%04x INSTRUCTION_TYPE: %d\n", op, + bytecode.instruction_type); + + switch (bytecode.instruction_type) { + case EXIT: { + // From Peter Miller's chip8run. Exit emulator with a return value + // of N. + exit = bytecode.operand.byte; + running = false; + break; + } + case SYS: { + // Jump to a machine code routine at nnn. + // This instruction is only used on the old computers on which + // Chip-8 was originally implemented. It is ignored by modern + // interpreters. + uint16_t addr = bytecode.operand.word & 0x0FFF; + assert(addr < RAM_SIZE && addr % 0x2 == 0); + + pc = bytecode.operand.word & 0x0FFF; + break; + } + case CLS: { + // Clear the screen. + // fprintf(stderr, "CLS not implemented\n"); + memset(this->fb, 0, SCREEN_WIDTH * SCREEN_HEIGHT); + break; + } + case RET: { + // Return from a subroutine. + // The interpreter sets the program counter to the address at the + // top of the stack, then subtracts 1 from the stack pointer. + + pc = stack[--sp]; + break; + } + case JP: { + // Jump to location nnn. + // The interpreter sets the program counter to nnn. + + uint16_t addr = bytecode.operand.word & 0x0FFF; + assert(addr < RAM_SIZE && addr % 0x2 == 0); + + pc = bytecode.operand.word & 0x0FFF; + break; + } + case CALL: { + // Call subroutine at nnn. + // 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; + pc = bytecode.operand.word & 0x0FFF; + break; + } + case SKIP_INSTRUCTION_BYTE: { + // Skip next instruction if Vx = kk. + // The interpreter compares register Vx to kk, and if they are + // equal, increments the program counter by 2. + if (this->v[bytecode.operand.byte_reg.reg] == + bytecode.operand.byte_reg.byte) { + pc += 2; + } + + break; + } + case SKIP_INSTRUCTION_NE_BYTE: { + // Skip next instruction if Vx != kk. + // The interpreter compares register Vx to kk, and if they are not + // equal, increments the program counter by 2. + if (this->v[bytecode.operand.byte_reg.reg] != + bytecode.operand.byte_reg.byte) { + pc += 2; + } + + break; + } + case SKIP_INSTRUCTION_REG: { + // Skip next instruction if Vx = Vy. + // The interpreter compares register Vx to register Vy, and if they + // are equal, increments the program counter by 2. + if (this->v[bytecode.operand.reg_reg.x] == + this->v[bytecode.operand.reg_reg.y]) { + pc += 2; + } + break; + } + case SKIP_INSTRUCTION_NE_REG: { + // Skip next instruction if Vx != Vy. + // The values of Vx and Vy are compared, and if they are not equal, + // the program counter is increased by 2. + if (this->v[bytecode.operand.reg_reg.x] != + this->v[bytecode.operand.reg_reg.y]) { + pc += 2; + } + break; + } + case LOAD_BYTE: { + // Set Vx = kk. + // The interpreter puts the value kk into register Vx. + this->v[bytecode.operand.byte_reg.reg] = + bytecode.operand.byte_reg.byte; + break; + } + case ADD_BYTE: { + // Set Vx = Vx + kk. + // Adds the value kk to the value of register Vx, then stores the + // result in Vx. + this->v[bytecode.operand.byte_reg.reg] += + bytecode.operand.byte_reg.byte; + break; + } + case LOAD_REG: { + // Set Vx = Vy. + // Stores the value of register Vy in register Vx. + this->v[bytecode.operand.reg_reg.x] = + this->v[bytecode.operand.reg_reg.y]; + break; + } + case ADD_REG: { + // Set Vx = Vx + Vy, set VF = carry. + // The values of Vx and Vy are added together. If the result is + // greater than 8 bits (i.e., > 255,) VF is set to 1, otherwise 0. + // Only the lowest 8 bits of the result are kept, and 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 > 0xFF; + + break; + } + case SUB_REG: { + // 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; + break; + } + case OR_REG: { + // Set Vx = Vx OR Vy. + // Performs a bitwise OR on the values of Vx and Vy, then stores the + // result in Vx. A bitwise OR compares the corrseponding bits from + // two values, and if either bit is 1, then the same bit in the + // result is also 1. Otherwise, it is 0. + this->v[bytecode.operand.reg_reg.x] = + this->v[bytecode.operand.reg_reg.x] | + this->v[bytecode.operand.reg_reg.y]; + break; + } + case AND_REG: { + // Set Vx = Vx AND Vy. + // Performs a bitwise AND on the values of Vx and Vy, then stores + // the result in Vx. A bitwise AND compares the corrseponding bits + // from two values, and if both bits are 1, then the same bit in the + // result is also 1. Otherwise, it is 0. + this->v[bytecode.operand.reg_reg.x] = + this->v[bytecode.operand.reg_reg.x] & + this->v[bytecode.operand.reg_reg.y]; + break; + } + case XOR_REG: { + // Set Vx = Vx XOR Vy. + // Performs a bitwise exclusive OR on the values of Vx and Vy, then + // stores the result in Vx. An exclusive OR compares the + // corrseponding bits from two values, and if the bits are not both + // the same, then the corresponding bit in the result is set to 1. + // Otherwise, it is 0. + this->v[bytecode.operand.reg_reg.x] = + this->v[bytecode.operand.reg_reg.x] ^ + this->v[bytecode.operand.reg_reg.y]; + break; + } + case SHR_REG: { + // 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; + 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; + 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; + break; + } + case LOAD_I_BYTE: { + // Set I = nnn. + // The value of register I is set to nnn. + this->i = bytecode.operand.word & 0x0FFF; + break; + } + case JP_V0_BYTE: { + // Jump to location nnn + V0. + // The program counter is set to nnn plus the value of V0. + pc = (bytecode.operand.word & 0x0FFF) + this->v[0]; + break; + } + case RND: { + // Set Vx = random byte AND kk. + // The interpreter generates a random number from 0 to 255, which is + // then ANDed with the value kk. The results are stored in Vx. See + // instruction 8xy2 for more information on AND. + this->v[bytecode.operand.byte_reg.reg] = + static_cast(rand()) & bytecode.operand.byte_reg.byte; + break; + } + case DRW: { + // Display n-byte sprite starting at memory location I + // at (Vx, Vy), set VF = collision. + // The interpreter reads n bytes from memory, starting at the + // address stored in I. These bytes are then displayed as sprites on + // screen at coordinates (Vx, Vy). Sprites are XORed onto the + // existing screen. If this causes any pixels to be erased, VF is + // set to 1, otherwise it is set to 0. If the sprite is positioned + // so part of it is outside the coordinates of the display, it wraps + // 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++) { + uint8_t sprite = read_mem(this->i + i); + + for (int j = 0; j < 8; j++) { + bool source = (sprite >> (7 - j)) & 0x1; + int x = (this->v[bytecode.operand.reg_reg_nibble.x] + j) % + 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; + } + + if (get_pixel(x, y)) { + set_pixel(x, y, 0); + this->v[0x0F] = 1; + } else { + set_pixel(x, y, 1); + } + } + } + break; + } + case SKIP_PRESSED_REG: { + // Skip next instruction if key with the value of Vx is + // pressed. + // Checks the keyboard, and if the key corresponding to the value of + // Vx is currently in the down position, PC is increased by 2. + fprintf(stderr, "SKIP_PRESSED_REG not implemented\n"); + break; + } + case SKIP_NOT_PRESSED_REG: { + // Skip next instruction if key with the value of Vx is + // not pressed. + // Checks the keyboard, and if the key corresponding to the value of + // Vx is currently in the up position, PC is increased by 2. + fprintf(stderr, "SKIP_NOT_PRESSED_REG not implemented\n"); + break; + } + case LD_REG_DT: { + // Set Vx = delay timer value. + // The value of DT is placed into Vx. + this->v[bytecode.operand.reg_reg.x] = this->delay; + break; + } + case LD_REG_K: { + // Wait for a key press, store the value of the key in + // Vx. + // All execution stops until a key is pressed, then the value of + // that key is stored in Vx. + fprintf(stderr, "LD_REG_K not implemented\n"); + break; + } + case LD_DT_REG: { + // Set delay timer = Vx. + // DT is set equal to the value of Vx. + this->delay = this->v[bytecode.operand.reg_reg.x]; + break; + } + case LD_ST_REG: { + // Set sound timer = Vx. + // ST is set equal to the value of Vx. + set_sound_timer(bytecode.operand.byte); + break; + } + case ADD_I_REG: { + // Set I = I + Vx. + // The values of I and Vx are added, and the results are stored in + // I. + this->i += this->v[bytecode.operand.byte]; + break; + } + case LD_F_REG: { + // Set I = location of sprite for digit Vx. + // The value of I is set to the location for the hexadecimal sprite + // 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->i = (uint16_t)(bytecode.operand.byte * 5); + break; + } + case LD_B_REG: { + // Store BCD representation of Vx in memory locations I, + // I+1, and I+2. + // The interpreter takes the decimal value of Vx, and places the + // 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] = + (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; + break; + } + case LD_PTR_I_REG: { + // Store registers V0 through Vx in memory starting at + // 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]; + } + break; + } + case LD_REG_PTR_I: { + // Read registers V0 through Vx from memory starting at + // 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]; + } + break; + } + case UNKNOWN_INSTRUCTION: { + fprintf(stderr, "Unknown instruction type: %d\n", + bytecode.instruction_type); + exit = 1; + running = false; + break; + } + } + + printf("Emulating...\n"); + + auto now = high_resolution_clock::now(); + if (duration_cast(now - last_timer_update) >= + timer_interval) { + printf("Updating...\n"); + if (delay > 0) + --delay; + if (sound_timer > 0) + --sound_timer; + draw(renderer, texture, this->fb); + last_timer_update = now; + } + + auto elapsed_time = duration_cast( + high_resolution_clock::now() - start_time); + if (elapsed_time < cycle_time) { + std::this_thread::sleep_for(cycle_time - elapsed_time); + } + } + + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + return exit; +} + +void Chip8::view_ram() { + printf("Hex dump:\n"); + for (size_t i = 0; i < RAM_SIZE / 16; i++) { + size_t j = 0; + for (; j < 16; j++) { + printf("%02x ", this->ram[i * 16 + j]); + } + printf(" |"); + j = 0; + for (; j < 16; j++) { + if (this->ram[i * 16 + j] >= 32 && this->ram[i * 16 + j] <= 126) { + printf("%c", this->ram[i * 16 + j]); + } else { + printf("."); + } + } + printf("|\n"); + } +} + +void Chip8::dump_ram() { + (void)remove("ram.bin"); + + FILE *fp = fopen("ram.bin", "wb"); + if (fp == NULL) { + printf("Failed to open file\n"); + exit(1); + } + + fwrite(this->ram, RAM_SIZE, 1, fp); + fclose(fp); +} + +int main(int argc, char **argv) { + signal(SIGINT, exit); + + if (argc < 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + Chip8 chip8 = Chip8(argv[1]); + chip8.view_ram(); + int ret = chip8.run(); + + return ret; +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..3645178 --- /dev/null +++ b/test.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +roms=($(find tests/ -type f)) + +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" + fi +done diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..a78e667 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +extern/** \ No newline at end of file diff --git a/tests/exit.ch8 b/tests/exit.ch8 new file mode 100644 index 0000000..081deac Binary files /dev/null and b/tests/exit.ch8 differ diff --git a/tests/jmp.ch8 b/tests/jmp.ch8 new file mode 100644 index 0000000..1db0a05 Binary files /dev/null and b/tests/jmp.ch8 differ