feat: initial version

This commit is contained in:
Zoëy Noort 2025-05-05 18:50:56 +02:00
parent 0a67fb21d1
commit ee790e99d6
12 changed files with 798 additions and 0 deletions

34
hdrs/chip8.h Normal file
View file

@ -0,0 +1,34 @@
#ifndef CHIP8_H
# define CHIP8_H
#include <stdint.h>
#include "./memory.h"
#include "./graphics.h"
#include "./input.h"
#define FRAME_TIME_MS 1000 / 60
typedef struct s_chip8
{
t_memory memory;
t_display display;
t_keyboard keyboard;
uint8_t v[16];
uint16_t i;
uint16_t pc;
uint8_t delay_timer;
uint8_t sound_timer;
uint16_t stack[16];
uint8_t sp;
int running;
} t_chip8;
int chip8_load_rom(t_chip8 *emu, const char *path);
void chip8_init(t_chip8 *emu);
void chip8_run(t_chip8 *emu);
#endif

23
hdrs/cpu.h Normal file
View file

@ -0,0 +1,23 @@
#ifndef CPU_H
# define CPU_H
#include "./chip8.h"
#include <stdint.h>
typedef struct s_instruction
{
uint16_t opcode;
uint8_t type;
uint8_t x;
uint8_t y;
uint8_t n;
uint8_t nn;
uint16_t nnn;
} t_instruction;
uint16_t cpu_fetch(t_chip8 *emu);
t_instruction cpu_decode(uint16_t opcode);
void cpu_execute(t_chip8 *emu, t_instruction instruction);
#endif

27
hdrs/font.h Normal file
View file

@ -0,0 +1,27 @@
#ifndef FONT_H
#define FONT_H
#define FONTSET_SIZE 80
#define FONTSET_START (MEMORY_SIZE - FONTSET_SIZE)
static const unsigned char FONTSET[FONTSET_SIZE] = {
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
};
#endif

29
hdrs/graphics.h Normal file
View file

@ -0,0 +1,29 @@
#ifndef GRAPHICS_H
#define GRAPHICS_H
#include <SDL2/SDL.h>
#include <stdint.h>
struct s_chip8;
typedef struct s_chip8 t_chip8;
#define SCREEN_WIDTH 64
#define SCREEN_HEIGHT 32
#define SCALE 12
typedef struct s_display
{
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
uint32_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];
} t_display;
int graphics_init(t_display *display);
void graphics_render(t_chip8 *emu);
void graphics_cleanup(t_display *display);
void graphics_draw_sprite(t_chip8 *emu, uint8_t x, uint8_t y, uint8_t n);
int graphics_display_ready();
#endif

26
hdrs/input.h Normal file
View file

@ -0,0 +1,26 @@
#ifndef INPUT_H
#define INPUT_H
#include <stdint.h>
#include <SDL2/SDL.h>
#define KEY_COUNT 16
struct s_chip8;
typedef struct s_chip8 t_chip8;
typedef struct s_keyboard
{
uint8_t keyboard[KEY_COUNT];
uint8_t waiting_for_key;
uint8_t key_pressed;
uint8_t key_register;
} t_keyboard;
void input_update(SDL_Event *event, uint8_t *keyboard);
uint8_t map_sdl_to_chip8(SDL_Keycode key);
uint8_t get_pressed_key(t_chip8 *emu);
#endif // INPUT_H

22
hdrs/memory.h Normal file
View file

@ -0,0 +1,22 @@
#ifndef MEMORY_H
# define MEMORY_H
#include <stdint.h>
#include <stdio.h>
#include "graphics.h"
#define MEMORY_SIZE 4096
typedef struct s_memory
{
uint8_t memory[MEMORY_SIZE];
uint8_t screen[SCREEN_WIDTH * SCREEN_HEIGHT];
} t_memory;
void memory_init(t_memory *mem);
int memory_load_rom(t_memory *mem, FILE *fp);
void memory_load_fontset(t_memory *mem);
void memory_clear_screen(t_memory *mem);
void memory_dump(t_memory *mem, const char *path);
#endif

119
srcs/chip8.c Normal file
View file

@ -0,0 +1,119 @@
#include "chip8.h"
#include "memory.h"
#include "cpu.h"
#include "input.h"
#include "libft.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <SDL2/SDL.h>
int chip8_load_rom(t_chip8 *emu, const char *path)
{
FILE *fp = fopen(path, "rb");
if (fp == NULL)
{
return 1;
}
if (memory_load_rom(&emu->memory, fp) == 1)
{
fclose(fp);
return (1);
}
fclose(fp);
return (0);
}
void chip8_init(t_chip8 *emu)
{
ft_memset(emu->keyboard.keyboard, 0, sizeof(emu->keyboard.keyboard));
memory_init(&emu->memory);
memory_load_fontset(&emu->memory);
memset(emu->v, 0, sizeof(emu->v));
memset(emu->stack, 0, sizeof(emu->stack));
emu->i = 0;
emu->pc = 0x200;
emu->delay_timer = 0;
emu->sound_timer = 0;
emu->sp = 0;
if (graphics_init(&emu->display) != 0)
{
printf("Failed to initialize graphics!\n");
exit(1);
}
if (graphics_display_ready() != 1)
{
printf("Graphics display failed to initialize properly!\n");
exit(1);
}
emu->running = 1;
}
void chip8_run(t_chip8 *emu)
{
uint32_t frame_start, frame_end, frame_duration;
SDL_Event event;
while (emu->running)
{
frame_start = SDL_GetTicks();
while (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
{
emu->running = 0;
break;
}
if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP)
{
input_update(&event, emu->keyboard.keyboard);
}
}
if (emu->keyboard.waiting_for_key)
{
uint8_t key = get_pressed_key(emu);
if (key != 0xFF)
{
emu->v[emu->keyboard.key_register] = key;
emu->keyboard.waiting_for_key = 0;
}
}
else
{
for (int i = 0; i < 10; i++)
{
uint16_t opcode = cpu_fetch(emu);
t_instruction instruction = cpu_decode(opcode);
cpu_execute(emu, instruction);
}
}
if (emu->delay_timer > 0)
emu->delay_timer--;
if (emu->sound_timer > 0)
emu->sound_timer--;
graphics_render(emu);
frame_end = SDL_GetTicks();
frame_duration = frame_end - frame_start;
if (frame_duration < FRAME_TIME_MS)
{
SDL_Delay(FRAME_TIME_MS - frame_duration);
}
}
}

257
srcs/cpu.c Normal file
View file

@ -0,0 +1,257 @@
#include "input.h"
#include "cpu.h"
#include "font.h"
#include <stdio.h>
#include <stdint.h>
#define OPCODE_SIZE 2
uint16_t cpu_fetch(t_chip8 *emu)
{
// Check if there is enough space in memory to fetch a 2-byte opcode
if (emu->pc + 1 >= MEMORY_SIZE)
{
printf("Error, program counter out of bounds! (pc = 0x%04X)\n", emu->pc);
graphics_cleanup(&emu->display);
emu->running = 0;
return (1);
}
uint16_t opcode = (emu->memory.memory[emu->pc] << 8) | emu->memory.memory[emu->pc + 1];
return opcode;
}
t_instruction cpu_decode(uint16_t opcode)
{
t_instruction instruction;
instruction.opcode = opcode;
instruction.type = (opcode & 0xF000) >> 12;
instruction.x = (opcode & 0x0F00) >> 8;
instruction.y = (opcode & 0x00F0) >> 4;
instruction.n = opcode & 0x000F;
instruction.nn = opcode & 0x00FF;
instruction.nnn = opcode & 0x0FFF;
return instruction;
}
void cpu_execute(t_chip8 *emu, t_instruction instruction)
{
uint8_t x = instruction.x;
uint8_t y = instruction.y;
uint8_t n = instruction.n;
uint8_t nn = instruction.nn;
uint16_t nnn = instruction.nnn;
switch (instruction.type)
{
// 0x0NNN: SYS addr (ignored, do nothing)
case 0x0:
// 0x00E0: CLS (Clear screen)
if (instruction.opcode == 0x00E0)
memory_clear_screen(&emu->memory);
// 0x00EE: RET (Return from subroutine)
else if (instruction.opcode == 0x00EE) {
if (emu->sp == 0)
{
printf("Stack underflow on RET!\n");
emu->running = 0;
return;
}
emu->pc = emu->stack[--emu->sp];
}
break;
// 0x1NNN: JP addr (Jump to address NNN)
case 0x1:
emu->pc = nnn;
return; // Do not increment PC after jump
// 0x2NNN: CALL addr (Call subroutine at NNN)
case 0x2:
if (emu->sp >= 16)
{
printf("Stack overflow!\n");
emu->running = 0;
return;
}
emu->stack[emu->sp++] = emu->pc;
emu->pc = nnn;
return; // Do not increment PC after call
// 0x3XNN: SE Vx, byte (Skip if Vx == NN)
case 0x3:
if (emu->v[x] == nn)
emu->pc += OPCODE_SIZE;
break;
// 0x4XNN: SNE Vx, byte (Skip if Vx != NN)
case 0x4:
if (emu->v[x] != nn)
emu->pc += OPCODE_SIZE;
break;
// 0x5XY0: SE Vx, Vy (Skip if Vx == Vy)
case 0x5:
if (n == 0 && emu->v[x] == emu->v[y])
emu->pc += OPCODE_SIZE;
break;
// 0x6XNN: LD Vx, byte (Load NN into Vx)
case 0x6:
emu->v[x] = nn;
break;
// 0x7XNN: ADD Vx, byte (Add NN to Vx)
case 0x7:
emu->v[x] += nn;
break;
// 0x8XYN: Arithmetic opcodes (switch on last nibble, 'n')
case 0x8:
switch (n)
{
// 0x8XY0: LD Vx, Vy (Set Vx = Vy)
case 0x0:
emu->v[x] = emu->v[y];
break;
// 0x8XY1: OR Vx, Vy (Set Vx = Vx OR Vy)
case 0x1:
emu->v[x] |= emu->v[y];
break;
// 0x8XY2: AND Vx, Vy (Set Vx = Vx AND Vy)
case 0x2:
emu->v[x] &= emu->v[y];
break;
// 0x8XY3: XOR Vx, Vy (Set Vx = Vx XOR Vy)
case 0x3:
emu->v[x] ^= emu->v[y];
break;
// 0x8XY4: ADD Vx, Vy (Set Vx = Vx + Vy, set VF = carry)
case 0x4:
emu->v[0xF] = (emu->v[x] + emu->v[y]) > 0xFF;
emu->v[x] += emu->v[y];
break;
// 0x8XY5: SUB Vx, Vy (Set Vx = Vx - Vy, set VF = NOT borrow)
case 0x5:
emu->v[0xF] = emu->v[x] >= emu->v[y];
emu->v[x] -= emu->v[y];
break;
// 0x8XY6: SHR Vx, Vy (Set Vx = Vx >> 1, set VF = LSB before shift)
case 0x6:
emu->v[0xF] = emu->v[x] & 1;
emu->v[x] >>= 1;
break;
// 0x8XY7: SUBN Vx, Vy (Set Vx = Vy - Vx, set VF = NOT borrow)
case 0x7:
emu->v[0xF] = emu->v[y] >= emu->v[x];
emu->v[x] = emu->v[y] - emu->v[x];
break;
// 0x8XYE: SHL Vx, Vy (Set Vx = Vx << 1, set VF = MSB before shift)
case 0xE:
emu->v[0xF] = (emu->v[x] >> 7) & 1;
emu->v[x] <<= 1;
break;
default:
printf("Unknown arithmetic opcode: 0x%04X\n", instruction.opcode);
break;
}
break;
// 0x9XY0: SNE Vx, Vy (Skip if Vx != Vy)
case 0x9:
if (n == 0 && emu->v[x] != emu->v[y])
emu->pc += OPCODE_SIZE;
break;
// 0x0ANNN: LD I, addr (Set I = NNN)
case 0xA:
emu->i = nnn;
break;
// 0xBNNN: JP V0, addr (Jump to NNN + V0)
case 0xB:
emu->pc = nnn + emu->v[0];
return; // Do not increment PC after jump
// 0xCXNN: RND Vx, byte (Set Vx = random byte AND NN)
case 0xC:
emu->v[x] = (rand() & 0xFF) & nn;
break;
// 0xDXYN: DRW Vx, Vy, nibble (Draw sprite at (Vx, Vy), N rows)
// N=0 means 16 rows (wrapping)
case 0xD:
graphics_draw_sprite(emu, emu->v[x] % 64, emu->v[y] % 32, n);
break;
// 0xEX9E: SKP Vx (Skip if key Vx is pressed)
// 0xEXA1: SKNP Vx (Skip if key Vx is not pressed)
case 0xE:
if (nn == 0x9E)
{
if (emu->keyboard.keyboard[emu->v[x] & 0xF])
emu->pc += OPCODE_SIZE;
}
else if (nn == 0xA1)
{
if (!emu->keyboard.keyboard[emu->v[x] & 0xF])
emu->pc += OPCODE_SIZE;
}
break;
// 0xFXNN: Various instructions (switch on NN)
case 0xF:
switch (nn)
{
// 0xFX07: LD Vx, DT (Set Vx = delay timer)
case 0x07:
emu->v[x] = emu->delay_timer;
break;
// 0xFX0A: LD Vx, K (Wait for key press, store in Vx)
case 0x0A:
emu->keyboard.waiting_for_key = 1;
emu->keyboard.key_register = x;
break;
// 0xFX15: LD DT, Vx (Set delay timer = Vx)
case 0x15:
emu->delay_timer = emu->v[x];
break;
// 0xFX18: LD ST, Vx (Set sound timer = Vx)
case 0x18:
emu->sound_timer = emu->v[x];
break;
// 0xFX1E: ADD I, Vx (Set I = I + Vx)
case 0x1E:
emu->i += emu->v[x];
break;
// 0xFX29: LD F, Vx (Set I = font sprite address for character in Vx)
case 0x29:
// emu->i = 0x50 + 5 * (emu->v[x] & 0xF);
emu->i = FONTSET_START + 5 * (emu->v[x] & 0xF);
break;
// 0xFX33: LD B, Vx (Store BCD representation of Vx at I, I+1, I+2)
case 0x33:
emu->memory.memory[emu->i] = (emu->v[x] / 100) % 10;
emu->memory.memory[emu->i + 1] = (emu->v[x] / 10) % 10;
emu->memory.memory[emu->i + 2] = emu->v[x] % 10;
break;
// 0xFX55: LD [I], Vx (Store V0-Vx in memory starting at I)
case 0x55:
for (int i = 0; i <= x; i++)
emu->memory.memory[emu->i + i] = emu->v[i];
break;
// 0xFX65: LD Vx, [I] (Load V0-Vx from memory starting at I)
case 0x65:
for (int i = 0; i <= x; i++)
emu->v[i] = emu->memory.memory[emu->i + i];
break;
default:
printf("Unknown 0xF opcode: 0x%04X\n", instruction.opcode);
break;
}
break;
}
emu->pc += OPCODE_SIZE;
}

122
srcs/graphics.c Normal file
View file

@ -0,0 +1,122 @@
#include "graphics.h"
#include <SDL2/SDL.h>
#include <stdint.h>
#include <stdio.h>
#include "chip8.h"
int graphics_init(t_display *display)
{
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
fprintf(stderr, "SDL2 could not initiallize! e=%s\n", SDL_GetError());
return (1);
}
display->window = SDL_CreateWindow("c8",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH * SCALE,
SCREEN_HEIGHT * SCALE,
SDL_WINDOW_SHOWN);
if (!display->window)
{
fprintf(stderr, "Window could not be created! e=%s\n", SDL_GetError());
return (1);
}
display->renderer = SDL_CreateRenderer(display->window, -1, SDL_RENDERER_ACCELERATED);
if (!display->renderer)
{
fprintf(stderr, "Renderer could not be created! e=%s\n", SDL_GetError());
return (1);
}
display->texture = SDL_CreateTexture(display->renderer, SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
SCREEN_WIDTH,
SCREEN_HEIGHT);
if (!display->texture)
{
fprintf(stderr, "Texture could not be created! e=%s\n", SDL_GetError());
return (1);
}
return (0);
}
void graphics_render(t_chip8 *emu)
{
for (int i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++)
{
uint8_t pixel = emu->memory.screen[i];
emu->display.pixels[i] = pixel ? 0xFFFFFFFF : 0xFF000000;
}
SDL_UpdateTexture(emu->display.texture, NULL, emu->display.pixels, SCREEN_WIDTH * sizeof(uint32_t));
SDL_RenderClear(emu->display.renderer);
SDL_RenderCopy(emu->display.renderer, emu->display.texture, NULL, NULL);
SDL_RenderPresent(emu->display.renderer);
}
void graphics_cleanup(t_display *display)
{
SDL_DestroyTexture(display->texture);
SDL_DestroyRenderer(display->renderer);
SDL_DestroyWindow(display->window);
SDL_Quit();
}
void graphics_draw_sprite(t_chip8 *emu, uint8_t x, uint8_t y, uint8_t n)
{
uint8_t pixel;
uint16_t sprite_address = emu->i;
uint8_t collision = 0;
for (int row = 0; row < n; row++)
{
pixel = emu->memory.memory[sprite_address + row];
for (int col = 0; col < 8; col++)
{
if ((pixel & (0x80 >> col)) != 0)
{
uint16_t screen_x = (x + col) % SCREEN_WIDTH;
uint16_t screen_y = (y + row) % SCREEN_HEIGHT;
if (emu->memory.screen[screen_y * SCREEN_WIDTH + screen_x] == 1)
{
collision = 1;
}
emu->memory.screen[screen_y * SCREEN_WIDTH + screen_x] ^= 1;
}
}
}
emu->v[0xF] = collision;
}
int graphics_display_ready()
{
SDL_Event event;
while (1)
{
while (SDL_PollEvent(&event) != 0)
{
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SHOWN)
{
return 1;
}
// Check for window close event
if (event.type == SDL_QUIT)
{
return 0;
}
}
SDL_Delay(10);
}
}

60
srcs/input.c Normal file
View file

@ -0,0 +1,60 @@
#include "input.h"
#include "chip8.h"
#include <stdio.h>
void input_update(SDL_Event *event, uint8_t *keyboard)
{
uint8_t key = map_sdl_to_chip8(event->key.keysym.sym);
if (key != 0xFF)
{
if (event->type == SDL_KEYDOWN)
{
keyboard[key] = 1;
}
else if (event->type == SDL_KEYUP)
{
keyboard[key] = 0;
}
}
}
uint8_t map_sdl_to_chip8(SDL_Keycode key)
{
// Map to a numpad-like layout:
// 1 2 3 4 → 1 2 3 C
// Q W E R → 4 5 6 D
// A S D F → 7 8 9 E
// Z X C V → A 0 B F
switch (key)
{
case SDLK_1: return 0x1;
case SDLK_2: return 0x2;
case SDLK_3: return 0x3;
case SDLK_4: return 0xC;
case SDLK_q: return 0x4;
case SDLK_w: return 0x5;
case SDLK_e: return 0x6;
case SDLK_r: return 0xD;
case SDLK_a: return 0x7;
case SDLK_s: return 0x8;
case SDLK_d: return 0x9;
case SDLK_f: return 0xE;
case SDLK_z: return 0xA;
case SDLK_x: return 0x0;
case SDLK_c: return 0xB;
case SDLK_v: return 0xF;
default: return 0xFF;
}
}
uint8_t get_pressed_key(t_chip8 *emu)
{
for (int i = 0; i < 16; i++)
{
if (emu->keyboard.keyboard[i])
return i;
}
return 0xFF; // No key pressed
}

25
srcs/main.c Normal file
View file

@ -0,0 +1,25 @@
#include <stdio.h>
#include "../hdrs/chip8.h"
int main(int argc, char **argv)
{
if (argc != 2)
{
fprintf(stderr, "Usage: %s <rom.ch8>\n", argv[0]);
return (1);
}
t_chip8 emu;
chip8_init(&emu);
if (chip8_load_rom(&emu, argv[1]) == 1)
{
fprintf(stderr, "Failed to load rom: %s\n", argv[1]);
return (1);
}
chip8_run(&emu);
return (0);
}

54
srcs/memory.c Normal file
View file

@ -0,0 +1,54 @@
#include "../hdrs/memory.h"
#include <stdio.h>
#include <string.h>
#include "../hdrs/font.h"
#include "libft.h"
void memory_init(t_memory *mem)
{
ft_memset(mem->memory, 0, MEMORY_SIZE);
}
int memory_load_rom(t_memory *mem, FILE *fp)
{
fseek(fp, 0, SEEK_END);
size_t rom_size = ftell(fp);
rewind(fp);
if (rom_size > (sizeof(mem->memory) - 0x200))
{
return (1);
}
if (fread(&mem->memory[0x200], 1, rom_size, fp) != rom_size)
{
return (1);
}
return (0);
}
void memory_clear_screen(t_memory *mem)
{
ft_memset(mem->screen, 0, SCREEN_WIDTH * SCREEN_HEIGHT);
}
void memory_load_fontset(t_memory *mem)
{
ft_memcpy(&mem->memory[MEMORY_SIZE - FONTSET_SIZE], FONTSET, FONTSET_SIZE);
}
void memory_dump(t_memory *mem, const char *path)
{
FILE *fp = fopen(path, "wb");
if (!fp)
{
printf("Failed to open: %s\n", path);
exit(1);
}
fwrite(mem->memory, 1, MEMORY_SIZE, fp);
fclose(fp);
printf("Memory dumped to: %s\n", path);
}