Created
April 25, 2023 13:39
-
-
Save darkerbit/7de4b00e65cb64ad930f3075604c5fd0 to your computer and use it in GitHub Desktop.
Single-file CHIP-8 emulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* sumic8.c: single-file chip-8 vm */ | |
/* Copyright (c) 2023 darkerbit | |
* | |
* This software is provided 'as-is', without any express or implied | |
* warranty. In no event will the authors be held liable for any damages | |
* arising from the use of this software. | |
* | |
* Permission is granted to anyone to use this software for any purpose, | |
* including commercial applications, and to alter it and redistribute it | |
* freely, subject to the following restrictions: | |
* | |
* 1. The origin of this software must not be misrepresented; you must not | |
* claim that you wrote the original software. If you use this software | |
* in a product, an acknowledgment in the product documentation would be | |
* appreciated but is not required. | |
* 2. Altered source versions must be plainly marked as such, and must not be | |
* misrepresented as being the original software. | |
* 3. This notice may not be removed or altered from any source distribution. | |
*/ | |
/* How big should the window be (1 is original resolution) */ | |
#define WINDOW_SIZE_MULT 15 | |
/* How many cycles to run per frame */ | |
#define CYCLES_PER_FRAME 100 | |
/* Real CHIP-8 can only draw one sprite per frame, uncomment to turn on behaviour */ | |
#define DISPLAY_WAIT | |
#include <stdbool.h> | |
#include <stdio.h> | |
#include <stdint.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <SDL2/SDL.h> | |
/* Global VM state */ | |
static struct | |
{ | |
/* Main memory */ | |
uint8_t *mem; | |
/* Framebuffer */ | |
uint8_t *fb; | |
/* Program counter */ | |
uint16_t pc; | |
/* Stack */ | |
uint8_t sp; | |
uint16_t stack[16]; | |
/* Registers */ | |
uint8_t r[16]; | |
uint16_t i; | |
/* Timers */ | |
uint8_t dly; | |
uint8_t snd; | |
bool refresh_wait; | |
/* Input */ | |
bool keys[16]; | |
uint8_t wait_reg; | |
} vm = { 0 }; | |
/* Forward declare for font data */ | |
static const uint8_t font[5*16]; | |
/* Initialize VM */ | |
static void vm_init() | |
{ | |
/* Allocate memory */ | |
vm.mem = calloc(0x1000, 1); | |
vm.fb = calloc(8*32, 1); | |
// Copy font data | |
memcpy(vm.mem, font, 5 * 16); | |
// Not waiting for inputs | |
vm.wait_reg = 0xFF; | |
} | |
/* Load a ROM file into memory */ | |
static bool vm_load(const char *path) | |
{ | |
FILE *f = fopen(path, "rb"); | |
if (f == NULL) | |
{ | |
perror("failed to load ROM"); | |
return false; | |
} | |
fread(vm.mem + 0x200, 1, 0x1000-0x200, f); | |
fclose(f); | |
// Set program counter | |
vm.pc = 0x200; | |
return true; | |
} | |
/* Draws a 8xN sprite from address, at X, Y */ | |
static bool vm_draw(uint16_t addr, uint8_t x, uint8_t y, uint8_t n) | |
{ | |
bool col = false; | |
uint8_t i = ((x & 0x3F) >> 3) | ((y & 0x1F) << 3); | |
uint8_t shift = x & 0x7; | |
for (uint8_t j = 0; j < n && (y & 0x1F) + j < 32; j++) | |
{ | |
uint8_t row = vm.mem[(addr + j) & 0xFFF]; | |
col = col || (row >> shift) & vm.fb[i]; | |
vm.fb[i] ^= (row >> shift); | |
if ((i & 0x7) < 7) | |
{ | |
col = col || (row << (8-shift)) & vm.fb[i+1]; | |
vm.fb[i+1] ^= (row << (8 - shift)); | |
} | |
i += 8; | |
} | |
return col; | |
} | |
/* ALU instructions */ | |
static uint8_t vm_alu(uint8_t s, uint8_t x, uint8_t y, bool *flag) | |
{ | |
switch (s) | |
{ | |
case 0x0: | |
return y; | |
case 0x1: | |
return x | y; | |
case 0x2: | |
return x & y; | |
case 0x3: | |
return x ^ y; | |
case 0x4: | |
*flag = ((uint16_t) x + y) > 0xFF; | |
return x + y; | |
case 0x5: | |
*flag = x >= y; | |
return x - y; | |
case 0x6: | |
*flag = y & 1; | |
return y >> 1; | |
case 0x7: | |
*flag = y >= x; | |
return y - x; | |
case 0xE: | |
*flag = (y & 0x80) >> 7; | |
return y << 1; | |
default: | |
break; | |
} | |
fprintf(stderr, "unknown alu instruction %X\n", s); | |
return 0; | |
} | |
/* Does a single VM step */ | |
static void vm_step() | |
{ | |
// Fetch next instruction | |
uint8_t b1 = vm.mem[vm.pc++ & 0xFFF]; | |
uint8_t b2 = vm.mem[vm.pc++ & 0xFFF]; | |
// Decode instruction | |
uint8_t s = (b1 & 0xF0) >> 4; | |
uint8_t x = (b1 & 0xF); | |
uint8_t y = (b2 & 0xF0) >> 4; | |
uint8_t n = (b2 & 0xF); | |
uint8_t nn = b2; | |
uint16_t nnn = (x << 8) | nn; | |
switch (s) | |
{ | |
case 0x0: | |
/* Selector 0: Machine language instructions */ | |
switch (nnn) | |
{ | |
case 0x0E0: | |
/* 00E0: Clear screen */ | |
memset(vm.fb, 0, 8*32); | |
return; | |
case 0x0EE: | |
/* 00EE: Return from subroutine */ | |
vm.pc = vm.stack[(--vm.sp) & 0xF]; | |
return; | |
default: | |
break; | |
} | |
break; | |
case 0x1: | |
/* 1NNN: Jump */ | |
vm.pc = nnn; | |
return; | |
case 0x2: | |
/* 2NNN: Jump to subroutine */ | |
vm.stack[(vm.sp++) & 0xF] = vm.pc; | |
vm.pc = nnn; | |
return; | |
case 0x3: | |
/* 3XNN: Skip next instruction if VX = NN */ | |
if (vm.r[x] == nn) vm.pc += 2; | |
return; | |
case 0x4: | |
/* 4XNN: Skip next instruction if VX != NN */ | |
if (vm.r[x] != nn) vm.pc += 2; | |
return; | |
case 0x5: | |
/* 5XY0: Skip if VX = VY */ | |
if (vm.r[x] == vm.r[y]) vm.pc += 2; | |
return; | |
case 0x6: | |
/* 6XNN: Set register */ | |
vm.r[x] = nn; | |
return; | |
case 0x7: | |
/* 7XNN: Add value to register */ | |
vm.r[x] += nn; | |
return; | |
case 0x8: | |
/* Selector 8: ALU instructions */ | |
{ | |
bool flag = false; | |
vm.r[x] = vm_alu(n, vm.r[x], vm.r[y], &flag); | |
vm.r[0xF] = flag; | |
} | |
return; | |
case 0x9: | |
/* 9XY0: Skip next instruction if VX != VY */ | |
if (vm.r[x] != vm.r[y]) vm.pc += 2; | |
return; | |
case 0xA: | |
/* AXNN: Set index register */ | |
vm.i = nnn; | |
return; | |
case 0xB: | |
/* BNNN: Jump to V0 + NNN */ | |
vm.pc = vm.r[0] + nnn; | |
return; | |
case 0xC: | |
/* CXNN: Set X to rand() & NN */ | |
vm.r[x] = rand() & nn; | |
return; | |
case 0xD: | |
/* DXYN: Draw */ | |
vm.r[0xF] = vm_draw(vm.i, vm.r[x], vm.r[y], n); | |
#ifdef DISPLAY_WAIT | |
vm.refresh_wait = true; | |
#endif | |
return; | |
case 0xE: | |
/* Selector E: Input instructions */ | |
switch (nn) | |
{ | |
case 0x9E: | |
/* EX9E: Skip if VX is pressed */ | |
if (vm.r[x] <= 0xF && vm.keys[vm.r[x]]) vm.pc += 2; | |
return; | |
case 0xA1: | |
/* EXA1: Skip if VX not pressed */ | |
if (vm.r[x] <= 0xF && !vm.keys[vm.r[x]]) vm.pc += 2; | |
return; | |
default: | |
break; | |
} | |
break; | |
case 0xF: | |
/* Selector F: Misc single-register instructions */ | |
switch (nn) | |
{ | |
case 0x07: | |
/* FX07: Set VX to delay register */ | |
vm.r[x] = vm.dly; | |
return; | |
case 0x0A: | |
/* FX0A: Wait for keypress */ | |
vm.wait_reg = x; | |
return; | |
case 0x15: | |
/* FX15: Set delay to VX */ | |
vm.dly = vm.r[x]; | |
return; | |
case 0x18: | |
/* FX18: Set sound timer to VX */ | |
vm.snd = vm.r[x]; | |
return; | |
case 0x1E: | |
/* Add VX to I */ | |
vm.i += vm.r[x]; | |
return; | |
case 0x29: | |
/* FX29: Set I to hexadecimal digit VX */ | |
vm.i = vm.r[x] * 5; | |
return; | |
case 0x33: | |
/* FX33: Store BCD equivalent of VX at I */ | |
vm.mem[vm.i & 0xFFF] = (vm.r[x] / 100); | |
vm.mem[(vm.i + 1) & 0xFFF] = (vm.r[x] / 10) % 10; | |
vm.mem[(vm.i + 2) & 0xFFF] = (vm.r[x]) % 10; | |
return; | |
case 0x55: | |
/* FX55: Store values V0..VX at I */ | |
{ | |
for (uint8_t i = 0; i <= x; i++) | |
{ | |
vm.mem[(vm.i++) & 0xFFF] = vm.r[i]; | |
} | |
} | |
return; | |
case 0x65: | |
/* FX55: Load values V0..VX from I */ | |
{ | |
for (uint8_t i = 0; i <= x; i++) | |
{ | |
vm.r[i] = vm.mem[(vm.i++) & 0xFFF]; | |
} | |
} | |
return; | |
default: | |
break; | |
} | |
break; | |
default: | |
break; | |
} | |
fprintf(stderr, "unrecognised opcode %02X%02X\n", b1, b2); | |
} | |
/* Runs the VM */ | |
static void vm_tick() | |
{ | |
vm.refresh_wait = false; | |
// Tick timers | |
if (vm.dly > 0) vm.dly--; | |
if (vm.snd > 0) vm.snd--; | |
for (int i = 0; i < CYCLES_PER_FRAME && vm.wait_reg > 0xF && !vm.refresh_wait; i++) | |
{ | |
vm_step(); | |
} | |
} | |
/* Quit VM */ | |
static void vm_quit() | |
{ | |
free(vm.fb); | |
free(vm.mem); | |
} | |
/* Global video state */ | |
static struct | |
{ | |
SDL_Window *window; | |
SDL_Renderer *renderer; | |
} video = { 0 }; | |
/* Initialize video */ | |
static bool video_init() | |
{ | |
if (SDL_Init(SDL_INIT_VIDEO) < 0) | |
{ | |
fprintf(stderr, "failed to initialize SDL: %s\n", SDL_GetError()); | |
return false; | |
} | |
video.window = SDL_CreateWindow("sumic8", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 64 * WINDOW_SIZE_MULT, 32 * WINDOW_SIZE_MULT, 0); | |
if (video.window == NULL) | |
{ | |
fprintf(stderr, "failed to create window: %s\n", SDL_GetError()); | |
SDL_Quit(); | |
return false; | |
} | |
video.renderer = SDL_CreateRenderer(video.window, -1, SDL_RENDERER_PRESENTVSYNC); | |
if (video.renderer == NULL) | |
{ | |
fprintf(stderr, "failed to create renderer: %s\n", SDL_GetError()); | |
SDL_DestroyWindow(video.window); | |
SDL_Quit(); | |
return false; | |
} | |
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, 0); | |
SDL_RenderSetLogicalSize(video.renderer, 64, 32); | |
return true; | |
} | |
/* Render framebuffer to screen */ | |
static void video_render() | |
{ | |
// Clear screen | |
SDL_SetRenderDrawColor(video.renderer, vm.snd > 0 ? 64 : 0, 0, 0, SDL_ALPHA_OPAQUE); | |
SDL_RenderClear(video.renderer); | |
// Draw screen | |
SDL_SetRenderDrawColor(video.renderer, 255, 255, 255, SDL_ALPHA_OPAQUE); | |
for (uint8_t y = 0; y < 32; y++) | |
{ | |
for (uint8_t i = 0; i < 8; i++) | |
{ | |
uint8_t row = vm.fb[y << 3 | i]; | |
for (uint8_t x = 0; x < 8; x++) | |
{ | |
if (row & (1 << (7 - x))) | |
{ | |
SDL_RenderDrawPoint(video.renderer, i << 3 | x, y); | |
} | |
} | |
} | |
} | |
SDL_RenderPresent(video.renderer); | |
} | |
/* Quit video */ | |
static void video_quit() | |
{ | |
SDL_DestroyRenderer(video.renderer); | |
SDL_DestroyWindow(video.window); | |
SDL_Quit(); | |
} | |
/* Scancodes for input */ | |
static const SDL_Scancode scancodes[16] = { | |
SDL_SCANCODE_X, | |
SDL_SCANCODE_1, | |
SDL_SCANCODE_2, | |
SDL_SCANCODE_3, | |
SDL_SCANCODE_Q, | |
SDL_SCANCODE_W, | |
SDL_SCANCODE_E, | |
SDL_SCANCODE_A, | |
SDL_SCANCODE_S, | |
SDL_SCANCODE_D, | |
SDL_SCANCODE_Z, | |
SDL_SCANCODE_C, | |
SDL_SCANCODE_4, | |
SDL_SCANCODE_R, | |
SDL_SCANCODE_F, | |
SDL_SCANCODE_V | |
}; | |
static void key_down(SDL_Scancode code) | |
{ | |
for (uint8_t i = 0; i < 16; i++) | |
{ | |
if (code == scancodes[i]) | |
{ | |
if (vm.wait_reg <= 0xF) | |
{ | |
vm.r[vm.wait_reg] = i; | |
vm.wait_reg = 0xFF; | |
vm.keys[i] = false; | |
break; | |
} | |
else | |
{ | |
vm.keys[i] = true; | |
break; | |
} | |
} | |
} | |
} | |
static void key_up(SDL_Scancode code) | |
{ | |
for (uint8_t i = 0; i < 16; i++) | |
{ | |
if (code == scancodes[i]) | |
{ | |
vm.keys[i] = false; | |
break; | |
} | |
} | |
} | |
static const char *usage = "sumic8: sumire's chip-8 vm\n" \ | |
"usage: %s <rom file>\n"; | |
int main(int argc, char *argv[]) | |
{ | |
if (argc < 2) | |
{ | |
printf(usage, argv[0]); | |
return 0; | |
} | |
else if (argc > 3) | |
{ | |
fprintf(stderr, usage, argv[0]); | |
return 1; | |
} | |
vm_init(); | |
if (!vm_load(argv[1])) | |
{ | |
vm_quit(); | |
return 1; | |
} | |
if (!video_init()) | |
{ | |
vm_quit(); | |
return 1; | |
} | |
// Main loop | |
while (1) | |
{ | |
// Measure time at start of frame | |
uint64_t timer = SDL_GetTicks64(); | |
// Poll events | |
SDL_Event e; | |
while (SDL_PollEvent(&e)) | |
{ | |
switch (e.type) | |
{ | |
case SDL_QUIT: | |
goto exit; | |
case SDL_KEYDOWN: | |
key_down(e.key.keysym.scancode); | |
break; | |
case SDL_KEYUP: | |
key_up(e.key.keysym.scancode); | |
break; | |
default: | |
break; | |
} | |
} | |
// Run the VM | |
vm_tick(); | |
// Render | |
video_render(); | |
// Sync to 60hz ish | |
uint64_t timeout = timer + 17; | |
if (SDL_GetTicks64() < timeout) | |
{ | |
do | |
{ | |
SDL_Delay(1); | |
} while (SDL_GetTicks64() < timeout); | |
} | |
else | |
{ | |
fprintf(stderr, "can't keep up!\n"); | |
} | |
} | |
exit: | |
video_quit(); | |
vm_quit(); | |
} | |
/* Built-in font */ | |
static const uint8_t font[5*16] = { | |
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 | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment