Skip to content

Instantly share code, notes, and snippets.

@darkerbit
Created April 25, 2023 13:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darkerbit/7de4b00e65cb64ad930f3075604c5fd0 to your computer and use it in GitHub Desktop.
Save darkerbit/7de4b00e65cb64ad930f3075604c5fd0 to your computer and use it in GitHub Desktop.
Single-file CHIP-8 emulator
/* 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