From d64f63b1653bbaedd3a3f9e6419a15e19fd5f22d Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:55:12 +0000 Subject: [PATCH] hi-res mode --- Makefile | 1 + README.md | 5 +- src/main.cpp | 578 ++++++++++++++++++++++++++++++--------------------- test.sh | 12 ++ 4 files changed, 361 insertions(+), 235 deletions(-) diff --git a/Makefile b/Makefile index e514139..ac64c41 100644 --- a/Makefile +++ b/Makefile @@ -20,5 +20,6 @@ $(BIN_DIR): clean: rm -rf $(BIN_DIR) + rm -rf tests/extern .PHONY: all clean run diff --git a/README.md b/README.md index f9586da..bcdeeb4 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ This is a simple emulator for the Game Boy. It is written in C++ and uses SDL fo ## TODO -- [ ] Implement sound -- [ ] Implement keyboard input +- [X] Implement sound +- [X] Implement keyboard input +- [ ] Fix games like Tetris and pong - [ ] Implement better e2e testing for visuals and other things - [ ] Implement a disassembler - [ ] Get better debugging diff --git a/src/main.cpp b/src/main.cpp index 9052cc8..a284046 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,13 @@ #include #include +#include #include +#include #include #include +#include #include +#include #include #include @@ -20,21 +24,27 @@ #include #include -const int SCREEN_WIDTH = 64; -const int SCREEN_HEIGHT = 32; 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_FRAMERATE = 60; const int TARGET_MS_PER_FRAME = 1000 / TARGET_FRAMERATE; const int TARGET_MS_PER_TICK = 1000 / 60; +static int SCREEN_WIDTH = 64; +static int SCREEN_HEIGHT = 32; static int SCALE = 10; static int BG_COLOR = 0x081820; static int FG_COLOR = 0x88c070; -const int SAMPLE_RATE = 44100; // 44.1kHz sample rate -const int FREQUENCY = 440; // A4 tone (you can change this) -const int AMPLITUDE = 28000; // Volume level +// Spcae Invaders by David Winter uses misaligned addresses, so we might not +// always want to align pc +#define ALIGN_PC true +// the SYS instruction technically shouldnt be used, so it can be compiled out. +#define SYS_INSTRUCTION false + +const int SAMPLE_RATE = 44100; +const int FREQUENCY = 440; // A4 +const int AMPLITUDE = 28000; const int SAMPLES_PER_CYCLE = SAMPLE_RATE / FREQUENCY; void audioCallback(void *userdata, Uint8 *stream, int len) { @@ -82,19 +92,54 @@ static uint8_t FONT[0x10][0x05] = { {0xF0, 0x80, 0xF0, 0x80, 0x80}, // F }; +void clear_framebuffer(bool **fb, int width, int height) { + // since the pixel array is just one big array, we can just memset the + // first element of the array and that will clear the entire pixel array + memset(fb[0], 0, width * height); +} + +bool **allocate_framebuffer(int width, int height) { + // allocate the frambuffer, which is a pointer to an array of booleans, the + // pointer to the arrays should be height elements long. The boolean arrays + // should be wdith elements long. + bool **fb = (bool **)malloc(height * sizeof(bool *)); + if (fb == NULL) { + return NULL; + } + + bool *pixel_array = (bool *)malloc(width * height * sizeof(bool)); + if (pixel_array == NULL) { + return NULL; + } + + for (int y = 0; y < height; y++) { + fb[y] = pixel_array + (y * width); + } + + clear_framebuffer(fb, width, height); + + return fb; +} + +void free_framebuffer(bool **fb) { + // since the pixel array is just one big array, we can just free the + // first element of the array and that will free the entire pixel array + free(fb[0]); + free(fb); +} + 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); + fprintf(stderr, "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!"); + fprintf(stderr, "Failed to allocate ram!"); exit(1); } @@ -107,12 +152,12 @@ class Chip8 { int err = read(rom_fd, ram + 0x200, file_size); if (err < 0) { - printf("Failed to read file: %s\n", rom_path); + fprintf(stderr, "Failed to read file: %s\n", rom_path); exit(1); } if (err != file_size) { - printf("Failed to read file: %s\n", rom_path); + fprintf(stderr, "Failed to read file: %s\n", rom_path); exit(1); } @@ -120,7 +165,15 @@ class Chip8 { stack = (uint16_t *)malloc(sizeof(uint16_t) * 16); if (stack == NULL) { - printf("Failed to allocate stack!"); + fprintf(stderr, "Failed to allocate stack!"); + exit(1); + } + + fb = allocate_framebuffer(SCREEN_WIDTH, SCREEN_HEIGHT); + fb_length = SCREEN_HEIGHT * SCREEN_WIDTH; + + if (fb == NULL) { + fprintf(stderr, "Failed to allocate framebuffer!\n"); exit(1); } } @@ -128,6 +181,7 @@ class Chip8 { ~Chip8() { free(ram); free(stack); + free_framebuffer(fb); } int run(); @@ -135,86 +189,79 @@ class Chip8 { void dump_ram(); void view_stack(); - // allow the font to be read - bool is_protected(size_t addr) { - if (addr <= sizeof(FONT)) - return false; - return addr < 0x1FF; - } - - // 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 + // 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)) { - printf("Attempted to read from protected address: 0x%04x\n", - (unsigned int)addr); - dump_ram(); - exit(1); - } - return this->ram[addr]; - } + int read_mem(size_t addr) { 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 write_mem(size_t addr, uint8_t val) { this->ram[addr] = val; } - void set_sound_timer(uint8_t val) { - // enable buzzer - printf("The sound timer is set to %d\n", val); - this->sound_timer = val; - } + void set_sound_timer(uint8_t val) { this->sound_timer = val; } void set_pixel(int x, int y, uint8_t val) { + assert(fb != NULL); assert(x >= 0 && x < SCREEN_WIDTH); assert(y >= 0 && y < SCREEN_HEIGHT); + assert(this->fb[y] != NULL); this->fb[y][x] = val; } uint8_t get_pixel(int x, int y) { + assert(fb != NULL); assert(x >= 0 && x < SCREEN_WIDTH); assert(y >= 0 && y < SCREEN_HEIGHT); + assert(this->fb[y] != NULL); return this->fb[y][x]; } - bool fb[SCREEN_HEIGHT][SCREEN_WIDTH] = {}; - std::atomic_uint8_t delay; - std::atomic_uint8_t sound_timer; - std::mutex key_mutex; - std::condition_variable key_cv; - std::atomic key_pressed = false; - std::atomic last_key = 0; + size_t fb_length = 0; + bool **fb = nullptr; + std::atomic_uint8_t delay = 0; + std::atomic_uint8_t sound_timer = 0; + std::mutex key_mutex = {}; + std::condition_variable key_cv = {}; + bool key_pressed_map[0x10] = {}; + std::atomic last_key = 0xFF; private: - uint8_t *ram; - uint16_t pc; - uint16_t *stack; - uint8_t sp; - uint8_t v[16]; - uint16_t i; + uint8_t *ram = nullptr; + uint16_t pc = 0x200; + uint16_t *stack = nullptr; + uint8_t sp = 0; + uint8_t v[16] = {0}; + uint16_t i = 0; // bool compat; }; -void draw(SDL_Renderer *renderer, SDL_Texture *texture, Chip8 *chip8) { - printf("Drawing...\n"); +size_t pixels_length = 0; +uint32_t *pixels_array = nullptr; + +void draw(SDL_Renderer *renderer, SDL_Texture *texture, Chip8 *chip8) { + if (pixels_array == NULL || + pixels_length != (size_t)(SCREEN_HEIGHT * SCREEN_WIDTH)) { + if (pixels_array != NULL) { + free(pixels_array); + } + + pixels_array = (uint32_t *)malloc(sizeof(uint32_t) * + (SCREEN_HEIGHT * SCREEN_WIDTH)); + if (pixels_array == NULL) { + fprintf(stderr, "Failed to allocated pixels buffer!"); + exit(1); + } + pixels_length = (size_t)(SCREEN_HEIGHT * SCREEN_WIDTH); + } - 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] = + pixels_array[i * SCREEN_WIDTH + j] = chip8->fb[i][j] ? FG_COLOR : BG_COLOR; } } - SDL_UpdateTexture(texture, nullptr, pixels, + SDL_UpdateTexture(texture, nullptr, pixels_array, SCREEN_WIDTH * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, nullptr, nullptr); @@ -243,18 +290,58 @@ void timer_thread(Chip8 *chip8) { } void render_thread(Chip8 *chip8) { - SDL_Window *window = SDL_CreateWindow( + SDL_Window *sdl_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); + SDL_Renderer *sdl_renderer = + SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_ACCELERATED); + SDL_Texture *render_texture = SDL_CreateTexture( + sdl_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, + SCREEN_WIDTH, SCREEN_HEIGHT); + + int width, height; + SDL_GetWindowSize(sdl_window, &width, &height); + + width /= SCALE; + height /= SCALE; while (true) { - printf("Rendering...\n"); - draw(renderer, texture, chip8); + // check if we need to resize the window + if (width != SCREEN_WIDTH || height != SCREEN_HEIGHT) { + // resize the framebuffer + bool **new_fb = allocate_framebuffer(SCREEN_WIDTH, SCREEN_HEIGHT); + + if (new_fb == NULL) { + fprintf(stderr, "Failed to allocate framebuffer!\n"); + exit(1); + } + + // we need to copy the old framebuffer to the new one, but, it + // is not guranteed that the old framebuffer is smaller than the + // new framebuffer, so we need to make sure we only read at most + // the smaller framebuffer. + size_t smaller_fb_size = width * height; + if (width > SCREEN_WIDTH || height > SCREEN_HEIGHT) { + smaller_fb_size = SCREEN_WIDTH * SCREEN_HEIGHT; + } + memcpy(new_fb[0], chip8->fb[0], smaller_fb_size); + + free_framebuffer(chip8->fb); + chip8->fb_length = SCREEN_HEIGHT * SCREEN_WIDTH; + chip8->fb = new_fb; + + // resize the SDL window + SDL_SetWindowSize(sdl_window, SCREEN_WIDTH * SCALE, + SCREEN_HEIGHT * SCALE); + SDL_DestroyTexture(render_texture); + render_texture = SDL_CreateTexture( + sdl_renderer, SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT); + width = SCREEN_WIDTH; + height = SCREEN_HEIGHT; + } + + draw(sdl_renderer, render_texture, chip8); std::this_thread::sleep_for( std::chrono::milliseconds(TARGET_MS_PER_FRAME)); // 60Hz } @@ -267,23 +354,65 @@ void input_thread(Chip8 *chip8, std::atomic_bool &running) { if (event.type == SDL_QUIT) running = false; if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { - printf("Key pressed! %c\n", event.key.keysym.sym); std::unique_lock lock(chip8->key_mutex); - chip8->key_pressed = (event.type == SDL_KEYDOWN); - // check if the key is a valid CHIP-8, either a number or a, b, - // c, d, e, or f - if ((event.key.keysym.sym < 0x30 || - event.key.keysym.sym > 0x39) && // 0-9 - (event.key.keysym.sym < 0x61 || - event.key.keysym.sym > 0x66)) - continue; - uint8_t key = event.key.keysym.sym - 0x30; - if (key > 0x09) { - key = key - 0x27; + // map the key to a CHIP-8 key in a numpad way, but not on + // the numpad since I dont have a numpad + uint8_t key; + switch (event.key.keysym.sym) { + case SDLK_1: + key = 0x01; + break; + case SDLK_2: + key = 0x02; + break; + case SDLK_3: + key = 0x03; + break; + case SDLK_q: + key = 0x04; + break; + case SDLK_w: + key = 0x05; + break; + case SDLK_e: + key = 0x06; + break; + case SDLK_a: + key = 0x07; + break; + case SDLK_s: + key = 0x08; + break; + case SDLK_d: + key = 0x09; + break; + case SDLK_x: + key = 0x00; + break; + case SDLK_z: + key = 0x0A; + break; + case SDLK_c: + key = 0x0B; + break; + case SDLK_4: + key = 0x0C; + break; + case SDLK_r: + key = 0x0D; + break; + case SDLK_f: + key = 0x0E; + break; + case SDLK_v: + key = 0x0F; + break; + default: + continue; } - printf("Key: %x\n", key); - chip8->last_key = key; // Map this to CHIP-8 keys + chip8->key_pressed_map[key] = event.type == SDL_KEYDOWN; + chip8->last_key = key; chip8->key_cv.notify_one(); lock.unlock(); } @@ -298,7 +427,7 @@ int Chip8::run() { constexpr auto cycle_time = milliseconds(TARGET_MS_PER_CYCLE); if (SDL_Init(SDL_INIT_VIDEO) < 0) { - printf("Failed to initialize SDL: %s\n", SDL_GetError()); + fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError()); return 1; } @@ -316,12 +445,13 @@ int Chip8::run() { 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); + fprintf(stderr, "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); + pc += 2; Bytecode bytecode = parse(op); printf("OPCODE: 0x%04x INSTRUCTION_TYPE: %d\n", op, @@ -329,33 +459,42 @@ int Chip8::run() { switch (bytecode.instruction_type) { case EXIT: { - // From Peter Miller's chip8run. Exit emulator with a return value - // of N. + // From Peter Miller's chip8run. Exit emulator with a return + // value of N. ret = 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. +// 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. +#if SYS_INSTRUCTION uint16_t addr = bytecode.operand.word & 0x0FFF; - assert(addr < RAM_SIZE && addr % 0x2 == 0); + assert(addr < RAM_SIZE); +#if ALIGN_PC + assert(addr % 0x2 == 0); +#endif // ALIGN_PC pc = bytecode.operand.word & 0x0FFF; +#else + (void)bytecode; + fprintf(stderr, "SYS instruction not enabled\n"); + exit(1); +#endif // SYS_INSTRUCTION break; } case CLS: { // Clear the screen. - // fprintf(stderr, "CLS not implemented\n"); - memset(this->fb, 0, SCREEN_WIDTH * SCREEN_HEIGHT); + clear_framebuffer(this->fb, 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. + // 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 @@ -366,8 +505,21 @@ int Chip8::run() { // Jump to location nnn. // The interpreter sets the program counter to nnn. + if ((pc - 2) == 0x200 && bytecode.operand.word == 0x260) { + printf("Entering Hi-Res mode\n"); + // This is the Hi-Res enable opcode (0x1260), we need to + // change the resolution to 64x64 and jump to 0x2C0 instead + // of 0x260. + bytecode.operand.word = 0x2C0; + // resize_framebuffer(this, 64, 64); + SCREEN_HEIGHT = 64; + } + uint16_t addr = bytecode.operand.word & 0x0FFF; - assert(addr < RAM_SIZE && addr % 0x2 == 0); + assert(addr < RAM_SIZE); +#if ALIGN_PC + assert(addr % 0x2 == 0); +#endif pc = bytecode.operand.word & 0x0FFF; break; @@ -375,10 +527,11 @@ int Chip8::run() { 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. + // current PC on the top of the stack. The PC is then set to + // nnn. - // sp++ increments the stack pointer and returns the value before - // the addition + // sp++ increments the stack pointer and returns the value + // before the addition stack[sp++] = pc; pc = bytecode.operand.word & 0x0FFF; break; @@ -396,8 +549,8 @@ int Chip8::run() { } 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. + // 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; @@ -407,8 +560,8 @@ int Chip8::run() { } 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. + // 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; @@ -417,8 +570,8 @@ int Chip8::run() { } 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. + // 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; @@ -434,8 +587,8 @@ int Chip8::run() { } case ADD_BYTE: { // Set Vx = Vx + kk. - // Adds the value kk to the value of register Vx, then stores the - // result in Vx. + // 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; @@ -450,8 +603,9 @@ int Chip8::run() { 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. + // 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; @@ -469,11 +623,6 @@ 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. - 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]; @@ -488,23 +637,19 @@ int Chip8::run() { } 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: { // 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. + // 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]; @@ -512,10 +657,11 @@ int Chip8::run() { } 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. + // 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]; @@ -523,11 +669,11 @@ int Chip8::run() { } 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. + // 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]; @@ -550,11 +696,6 @@ int Chip8::run() { // 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. - 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]; @@ -569,15 +710,11 @@ int Chip8::run() { } 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: { @@ -607,25 +744,26 @@ int Chip8::run() { } 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. + // 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. + // 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. + // 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. this->v[0x0F] = 0; for (int i = 0; i < bytecode.operand.reg_reg_nibble.nibble; i++) { @@ -641,45 +779,39 @@ int Chip8::run() { continue; } - if (get_pixel(x, y)) { - set_pixel(x, y, 0); + if (this->get_pixel(x, y)) { + this->set_pixel(x, y, 0); this->v[0x0F] = 1; } else { - set_pixel(x, y, 1); + this->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"); - printf("Last key: %x\n", this->last_key.load()); - printf("Key: %x\n", bytecode.operand.byte); + // 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. uint8_t key = this->v[bytecode.operand.byte]; - if (this->last_key.load() == key && this->key_pressed.load()) { + if (this->key_pressed_map[key]) { pc += 2; } 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"); - printf("Last key: %x\n", this->last_key.load()); - printf("Key: %x\n", bytecode.operand.byte); + // 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"); uint8_t key = this->v[bytecode.operand.byte]; - if (this->last_key.load() != key) { - pc += 2; - } - if (this->last_key.load() == key && !this->key_pressed.load()) { + if (!this->key_pressed_map[key]) { pc += 2; } break; @@ -687,24 +819,22 @@ int Chip8::run() { 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; + this->v[bytecode.operand.byte] = this->delay; break; } case LD_REG_K: { - // Wait for a key press, store the value of the key in - // Vx. + // 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. std::unique_lock lock(this->key_mutex); this->key_cv.wait(lock, [&]() { this->v[bytecode.operand.byte] = this->last_key.load(); - return this->key_pressed.load(); + return this->key_pressed_map[this->last_key.load()]; }); lock.unlock(); - while (this->key_pressed.load()) { - printf("Waiting for key to be released %x %x\n", - this->last_key.load(), this->key_pressed.load()); + while (this->key_pressed_map[this->last_key.load()]) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } break; @@ -718,71 +848,65 @@ int Chip8::run() { case LD_ST_REG: { // Set sound timer = Vx. // ST is set equal to the value of Vx. - printf("Sound timer set to %d\n", this->v[bytecode.operand.byte]); set_sound_timer(this->v[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. + // 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. + // 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 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); + this->i = (uint16_t)(this->v[bytecode.operand.byte] * 5); break; } case LD_B_REG: { - // Store BCD representation of Vx in memory locations I, - // I+1, and I+2. + // 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? - 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); - write_mem(this->i + 2, - (uint8_t)(this->v[bytecode.operand.reg_reg.x] % 10) & + write_mem(this->i, + (uint8_t)((this->v[bytecode.operand.byte] / 100) & 0x0F)); + write_mem(this->i + 1, + (uint8_t)((this->v[bytecode.operand.byte] % 100) / 10) & 0x0F); + write_mem(this->i + 2, + (uint8_t)(this->v[bytecode.operand.byte] % 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. + // 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.byte; i++) { write_mem(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. + // 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.byte; i++) { this->v[i] = read_mem(this->i + i); } break; } case UNKNOWN_INSTRUCTION: { - fprintf(stderr, "Unknown instruction type: %d\n", - bytecode.instruction_type); + fprintf(stderr, "Unknown instruction: %04x\n", op); exit(1); } } @@ -798,11 +922,6 @@ int Chip8::run() { render.detach(); input.detach(); - // SDL_DestroyTexture(texture); - // SDL_DestroyRenderer(renderer); - // SDL_DestroyWindow(window); - // SDL_Quit(); - return ret; } @@ -849,22 +968,13 @@ void Chip8::view_stack() { printf("\n"); } -void destroy(int _) { - (void)_; - - if (!SDL_WasInit(SDL_INIT_VIDEO)) { - exit(0); - } - - SDL_Event event; - event.type = SDL_QUIT; - SDL_PushEvent(&event); - - exit(0); +void signal_handler(int signum) { + (void)signum; + exit(1); } int main(int argc, char **argv) { - signal(SIGINT, destroy); + signal(SIGINT, signal_handler); if (argc < 2) { printf("Usage: %s [options]\n", argv[0]); return 1; @@ -942,8 +1052,10 @@ int main(int argc, char **argv) { } } + srand(time(0)); + Chip8 chip8 = Chip8(file_name); - chip8.view_ram(); + chip8.dump_ram(); int ret = chip8.run(); return ret; diff --git a/test.sh b/test.sh index d97b439..6695284 100755 --- a/test.sh +++ b/test.sh @@ -13,6 +13,18 @@ extern_roms=( "https://github.com/Timendus/chip8-test-suite/raw/main/bin/4-flags.ch8" "https://github.com/Timendus/chip8-test-suite/raw/main/bin/6-keypad.ch8" "https://github.com/Timendus/chip8-test-suite/raw/main/bin/7-beep.ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/games/Pong%20(1%20player).ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/games/Tetris%20%5BFran%20Dachille,%201991%5D.ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/games/Lunar%20Lander%20(Udo%20Pernisz,%201979).ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/games/Breakout%20%5BCarmelo%20Cortez,%201979%5D.ch8" + # Space Invaders uses misaligned addresses, so for a typical testing suite, + # where we force aligned instructions, we dont want to test this since + # it would fail for obvious reasons + # "https://github.com/kripod/chip8-roms/raw/refs/heads/master/games/Space%20Invaders%20%5BDavid%20Winter%5D%20(alt).ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/hires/Hires%20Maze%20%5BDavid%20Winter,%20199x%5D.ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/hires/Hires%20Stars%20%5BSergey%20Naydenov,%202010%5D.ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/programs/Random%20Number%20Test%20%5BMatthew%20Mikolay,%202010%5D.ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/hires/Hires%20Sierpinski%20%5BSergey%20Naydenov,%202010%5D.ch8" ) for rom in "${extern_roms[@]}"; do