aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-04 22:05:31 +0200
committergbc dev <gbc@localhost>2026-07-04 22:05:31 +0200
commit0d0f4f1d23851c0c34e518834f7d316e61dec1f3 (patch)
treee46296c247617ee0ead1fb93e9d0dc475a07f9b9
downloadsl0pboy-0d0f4f1d23851c0c34e518834f7d316e61dec1f3.tar.gz
sl0pboy-0d0f4f1d23851c0c34e518834f7d316e61dec1f3.tar.xz
sl0pboy-0d0f4f1d23851c0c34e518834f7d316e61dec1f3.zip
scaffolding: build system, cartridge loader with MBC1/2/3/5, core types
-rw-r--r--.gitignore7
-rw-r--r--Makefile27
-rw-r--r--src/cart.c253
-rw-r--r--src/cart.h53
-rw-r--r--src/gb.h127
-rw-r--r--src/types.h16
6 files changed, 483 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..26ad2a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/build/
+*.o
+*.gb
+*.gbc
+*.sav
+roms/
+compile_commands.json
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0d79c1f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+CC := gcc
+CFLAGS := -std=c11 -O2 -Wall -Wextra -Wno-unused-parameter -g
+LDFLAGS :=
+SRCDIR := src
+BUILD := build
+SRCS := $(wildcard $(SRCDIR)/*.c)
+OBJS := $(patsubst $(SRCDIR)/%.c,$(BUILD)/%.o,$(SRCS))
+BIN := $(BUILD)/gbc
+
+.PHONY: all clean run
+
+all: $(BIN)
+
+$(BIN): $(OBJS)
+ $(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS)
+
+$(BUILD)/%.o: $(SRCDIR)/%.c | $(BUILD)
+ $(CC) $(CFLAGS) -c $< -o $@
+
+$(BUILD):
+ mkdir -p $(BUILD)
+
+clean:
+ rm -rf $(BUILD)
+
+run: $(BIN)
+ $(BIN) $(ROM)
diff --git a/src/cart.c b/src/cart.c
new file mode 100644
index 0000000..95cd279
--- /dev/null
+++ b/src/cart.c
@@ -0,0 +1,253 @@
+#include "cart.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static const char *mbc_name(MbcType m) {
+ switch (m) {
+ case MBC_NONE: return "ROM-only";
+ case MBC_1: return "MBC1";
+ case MBC_2: return "MBC2";
+ case MBC_3: return "MBC3";
+ case MBC_5: return "MBC5";
+ }
+ return "?";
+}
+
+// Decode cartridge type byte (0x0147) into MBC + feature flags.
+static void decode_type(Cart *c, u8 t) {
+ c->mbc = MBC_NONE;
+ c->has_ram = false;
+ c->has_battery = false;
+ c->has_rtc = false;
+ switch (t) {
+ case 0x00: c->mbc = MBC_NONE; break;
+ case 0x01: c->mbc = MBC_1; break;
+ case 0x02: c->mbc = MBC_1; c->has_ram = true; break;
+ case 0x03: c->mbc = MBC_1; c->has_ram = true; c->has_battery = true; break;
+ case 0x05: c->mbc = MBC_2; break;
+ case 0x06: c->mbc = MBC_2; c->has_battery = true; break;
+ case 0x08: c->mbc = MBC_NONE; c->has_ram = true; break;
+ case 0x09: c->mbc = MBC_NONE; c->has_ram = true; c->has_battery = true; break;
+ case 0x0F: c->mbc = MBC_3; c->has_rtc = true; c->has_battery = true; break;
+ case 0x10: c->mbc = MBC_3; c->has_rtc = true; c->has_ram = true; c->has_battery = true; break;
+ case 0x11: c->mbc = MBC_3; break;
+ case 0x12: c->mbc = MBC_3; c->has_ram = true; break;
+ case 0x13: c->mbc = MBC_3; c->has_ram = true; c->has_battery = true; break;
+ case 0x19: c->mbc = MBC_5; break;
+ case 0x1A: c->mbc = MBC_5; c->has_ram = true; break;
+ case 0x1B: c->mbc = MBC_5; c->has_ram = true; c->has_battery = true; break;
+ case 0x1C: c->mbc = MBC_5; break;
+ case 0x1D: c->mbc = MBC_5; c->has_ram = true; break;
+ case 0x1E: c->mbc = MBC_5; c->has_ram = true; c->has_battery = true; break;
+ default:
+ fprintf(stderr, "warning: unknown cart type 0x%02X, assuming MBC1\n", t);
+ c->mbc = MBC_1; c->has_ram = true;
+ break;
+ }
+}
+
+static size_t ram_size_bytes(u8 code) {
+ switch (code) {
+ case 0x00: return 0;
+ case 0x01: return 2 * 1024; // unofficial
+ case 0x02: return 8 * 1024;
+ case 0x03: return 32 * 1024;
+ case 0x04: return 128 * 1024;
+ case 0x05: return 64 * 1024;
+ default: return 0;
+ }
+}
+
+int cart_load(Cart *c, const char *path) {
+ memset(c, 0, sizeof(*c));
+ FILE *f = fopen(path, "rb");
+ if (!f) { perror("fopen rom"); return -1; }
+ fseek(f, 0, SEEK_END);
+ long sz = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ if (sz < 0x150) { fprintf(stderr, "rom too small\n"); fclose(f); return -1; }
+
+ c->rom = malloc(sz);
+ if (fread(c->rom, 1, sz, f) != (size_t)sz) { fclose(f); return -1; }
+ fclose(f);
+ c->rom_size = sz;
+
+ memcpy(c->title, c->rom + 0x134, 16);
+ c->title[16] = 0;
+ for (int i = 0; i < 16; i++)
+ if ((u8)c->title[i] < 0x20 || (u8)c->title[i] > 0x7e) c->title[i] = 0;
+
+ u8 cgb_flag = c->rom[0x143];
+ c->cgb = (cgb_flag == 0x80 || cgb_flag == 0xC0);
+
+ decode_type(c, c->rom[0x147]);
+
+ c->rom_banks = 2 << c->rom[0x148];
+ if ((size_t)c->rom_banks * 0x4000 > c->rom_size) {
+ // trust the file size
+ c->rom_banks = c->rom_size / 0x4000;
+ if (c->rom_banks < 2) c->rom_banks = 2;
+ }
+
+ c->ram_size = ram_size_bytes(c->rom[0x149]);
+ if (c->mbc == MBC_2) c->ram_size = 512; // built-in 512x4 bit
+ if (c->has_ram && c->ram_size == 0) c->ram_size = 8 * 1024;
+ if (c->ram_size) {
+ c->ram = calloc(1, c->ram_size);
+ c->ram_banks = c->ram_size / 0x2000;
+ if (c->ram_banks < 1) c->ram_banks = 1;
+ }
+
+ c->rom_bank = 1;
+ c->rom_bank_hi = 0;
+ c->ram_bank = 0;
+
+ // battery save path: rom path with .sav
+ snprintf(c->save_path, sizeof(c->save_path), "%s.sav", path);
+ if (c->has_battery && c->ram) {
+ FILE *sf = fopen(c->save_path, "rb");
+ if (sf) {
+ fread(c->ram, 1, c->ram_size, sf);
+ fclose(sf);
+ }
+ }
+
+ fprintf(stderr, "Loaded '%s' | %s | %d ROM banks (%zuKB) | %zuKB RAM | %s%s\n",
+ c->title, mbc_name(c->mbc), c->rom_banks, c->rom_size / 1024,
+ c->ram_size / 1024, c->cgb ? "CGB " : "DMG ",
+ c->has_battery ? "battery" : "");
+ return 0;
+}
+
+void cart_save(Cart *c) {
+ if (!c->has_battery || !c->ram) return;
+ FILE *sf = fopen(c->save_path, "wb");
+ if (sf) {
+ fwrite(c->ram, 1, c->ram_size, sf);
+ fclose(sf);
+ }
+}
+
+void cart_free(Cart *c) {
+ cart_save(c);
+ free(c->rom);
+ free(c->ram);
+ c->rom = c->ram = NULL;
+}
+
+// ---- Bank math helpers ----
+
+static u8 read_rom(Cart *c, u32 bank, u16 off) {
+ if (c->rom_banks) bank %= (u32)c->rom_banks;
+ size_t idx = (size_t)bank * 0x4000 + off;
+ if (idx >= c->rom_size) return 0xFF;
+ return c->rom[idx];
+}
+
+u8 cart_read(Cart *c, u16 addr) {
+ if (addr < 0x4000) {
+ // bank 0 region (MBC1 mode1 can remap, handle simply)
+ if (c->mbc == MBC_1 && c->bank_mode == 1) {
+ u32 bank = (c->rom_bank_hi << 5);
+ return read_rom(c, bank, addr);
+ }
+ return read_rom(c, 0, addr);
+ }
+ if (addr < 0x8000) {
+ u32 bank;
+ switch (c->mbc) {
+ case MBC_1: {
+ bank = (c->rom_bank & 0x1F) | (c->rom_bank_hi << 5);
+ if ((bank & 0x1F) == 0) bank |= 1;
+ break;
+ }
+ case MBC_2:
+ bank = c->rom_bank & 0x0F;
+ if (bank == 0) bank = 1;
+ break;
+ case MBC_3:
+ bank = c->rom_bank & 0x7F;
+ if (bank == 0) bank = 1;
+ break;
+ case MBC_5:
+ bank = c->rom_bank | (c->rom_bank_hi << 8);
+ break;
+ default:
+ bank = 1;
+ break;
+ }
+ return read_rom(c, bank, addr - 0x4000);
+ }
+ if (addr >= 0xA000 && addr < 0xC000) {
+ if (!c->ram_enabled) return 0xFF;
+ if (c->mbc == MBC_2) {
+ return 0xF0 | (c->ram[(addr - 0xA000) & 0x1FF] & 0x0F);
+ }
+ if (c->mbc == MBC_3 && c->ram_bank >= 0x08) {
+ // RTC register read
+ if (c->ram_bank <= 0x0C) return c->rtc_reg[c->ram_bank - 0x08];
+ return 0xFF;
+ }
+ if (!c->ram) return 0xFF;
+ u32 bank = c->ram_bank;
+ if (c->mbc == MBC_1 && c->bank_mode == 0) bank = 0;
+ if (c->ram_banks) bank %= (u32)c->ram_banks;
+ size_t idx = bank * 0x2000 + (addr - 0xA000);
+ if (idx >= c->ram_size) return 0xFF;
+ return c->ram[idx];
+ }
+ return 0xFF;
+}
+
+void cart_write(Cart *c, u16 addr, u8 val) {
+ switch (c->mbc) {
+ case MBC_NONE:
+ break;
+ case MBC_1:
+ if (addr < 0x2000) c->ram_enabled = (val & 0x0F) == 0x0A;
+ else if (addr < 0x4000) c->rom_bank = val & 0x1F;
+ else if (addr < 0x6000) c->rom_bank_hi = val & 0x03;
+ else if (addr < 0x8000) c->bank_mode = val & 1;
+ break;
+ case MBC_2:
+ if (addr < 0x4000) {
+ if (addr & 0x0100) c->rom_bank = val & 0x0F;
+ else c->ram_enabled = (val & 0x0F) == 0x0A;
+ }
+ break;
+ case MBC_3:
+ if (addr < 0x2000) c->ram_enabled = (val & 0x0F) == 0x0A;
+ else if (addr < 0x4000) c->rom_bank = val & 0x7F;
+ else if (addr < 0x6000) c->ram_bank = val; // 0-3 ram, 8-C rtc
+ else if (addr < 0x8000) {
+ if (c->rtc_latch == 0 && val == 1) { /* latch clock (stub) */ }
+ c->rtc_latch = val;
+ }
+ break;
+ case MBC_5:
+ if (addr < 0x2000) c->ram_enabled = (val & 0x0F) == 0x0A;
+ else if (addr < 0x3000) c->rom_bank = val;
+ else if (addr < 0x4000) c->rom_bank_hi = val & 1;
+ else if (addr < 0x6000) c->ram_bank = val & 0x0F;
+ break;
+ }
+
+ if (addr >= 0xA000 && addr < 0xC000) {
+ if (!c->ram_enabled) return;
+ if (c->mbc == MBC_2) {
+ c->ram[(addr - 0xA000) & 0x1FF] = val & 0x0F;
+ return;
+ }
+ if (c->mbc == MBC_3 && c->ram_bank >= 0x08) {
+ if (c->ram_bank <= 0x0C) c->rtc_reg[c->ram_bank - 0x08] = val;
+ return;
+ }
+ if (!c->ram) return;
+ u32 bank = c->ram_bank;
+ if (c->mbc == MBC_1 && c->bank_mode == 0) bank = 0;
+ if (c->ram_banks) bank %= (u32)c->ram_banks;
+ size_t idx = bank * 0x2000 + (addr - 0xA000);
+ if (idx < c->ram_size) c->ram[idx] = val;
+ }
+}
diff --git a/src/cart.h b/src/cart.h
new file mode 100644
index 0000000..ec864bb
--- /dev/null
+++ b/src/cart.h
@@ -0,0 +1,53 @@
+#ifndef GBC_CART_H
+#define GBC_CART_H
+
+#include "types.h"
+
+typedef enum {
+ MBC_NONE,
+ MBC_1,
+ MBC_2,
+ MBC_3,
+ MBC_5,
+} MbcType;
+
+typedef struct {
+ u8 *rom; // full ROM image
+ size_t rom_size;
+ int rom_banks; // number of 16KB banks
+
+ u8 *ram; // external cartridge RAM
+ size_t ram_size;
+ int ram_banks; // number of 8KB banks
+
+ MbcType mbc;
+ bool has_ram;
+ bool has_battery;
+ bool has_rtc;
+ bool cgb; // CGB flag set in header
+
+ char title[17];
+
+ // MBC state
+ bool ram_enabled;
+ u8 rom_bank; // low bank register
+ u8 rom_bank_hi; // high bits (mbc1 mode / mbc5)
+ u8 ram_bank;
+ u8 bank_mode; // MBC1 banking mode select
+
+ // RTC (MBC3)
+ u8 rtc_reg[5]; // S, M, H, DL, DH
+ u8 rtc_latch;
+
+ char save_path[512];
+} Cart;
+
+// Load a ROM file into cart. Returns 0 on success.
+int cart_load(Cart *c, const char *path);
+void cart_free(Cart *c);
+void cart_save(Cart *c);
+
+u8 cart_read(Cart *c, u16 addr);
+void cart_write(Cart *c, u16 addr, u8 val);
+
+#endif
diff --git a/src/gb.h b/src/gb.h
new file mode 100644
index 0000000..806daba
--- /dev/null
+++ b/src/gb.h
@@ -0,0 +1,127 @@
+#ifndef GBC_GB_H
+#define GBC_GB_H
+
+#include "types.h"
+#include "cart.h"
+
+// Interrupt bit flags (IE/IF at 0xFFFF / 0xFF0F)
+enum {
+ INT_VBLANK = 0x01,
+ INT_LCDSTAT= 0x02,
+ INT_TIMER = 0x04,
+ INT_SERIAL = 0x08,
+ INT_JOYPAD = 0x10,
+};
+
+// Joypad button bits (as used by our input state; 1 = pressed)
+enum {
+ BTN_A = 0x01,
+ BTN_B = 0x02,
+ BTN_SELECT = 0x04,
+ BTN_START = 0x08,
+ BTN_RIGHT = 0x10,
+ BTN_LEFT = 0x20,
+ BTN_UP = 0x40,
+ BTN_DOWN = 0x80,
+};
+
+#define SCREEN_W 160
+#define SCREEN_H 144
+
+typedef struct GB GB;
+
+// ---- CPU (Sharp SM83) ----
+typedef struct {
+ union { struct { u8 f, a; }; u16 af; };
+ union { struct { u8 c, b; }; u16 bc; };
+ union { struct { u8 e, d; }; u16 de; };
+ union { struct { u8 l, h; }; u16 hl; };
+ u16 sp, pc;
+ bool ime; // interrupt master enable
+ bool ime_pending; // EI delayed enable
+ bool halted;
+ bool halt_bug;
+ bool stopped;
+} CPU;
+
+// ---- PPU ----
+typedef struct {
+ u8 vram[0x4000]; // 2 banks (CGB)
+ u8 oam[0xA0];
+ u8 vram_bank; // CGB
+
+ // registers
+ u8 lcdc, stat, scy, scx, ly, lyc, wy, wx, bgp, obp0, obp1;
+ u8 vbk; // vram bank select reg (bit0)
+
+ // CGB palettes
+ u8 bcps, ocps;
+ u8 bg_pal[64]; // 8 palettes * 4 colors * 2 bytes
+ u8 obj_pal[64];
+
+ int mode; // 0 hblank,1 vblank,2 oam,3 draw
+ int dots; // dot counter within current line
+ bool frame_ready;
+ u8 wly; // internal window line counter
+
+ // framebuffer: RGB888
+ u8 fb[SCREEN_H][SCREEN_W][3];
+} PPU;
+
+// ---- Timer ----
+typedef struct {
+ u16 div; // internal 16-bit counter; DIV is upper 8 bits
+ u8 tima, tma, tac;
+ bool overflow_pending;
+ int reload_delay;
+} Timer;
+
+struct GB {
+ CPU cpu;
+ PPU ppu;
+ Timer timer;
+ Cart cart;
+
+ u8 wram[0x8000]; // 8 banks of 4KB (CGB); DMG uses 2
+ u8 hram[0x7F];
+ u8 wram_bank; // svbk (1-7)
+
+ u8 ie; // 0xFFFF
+ u8 iff; // 0xFF0F interrupt flags
+ u8 joyp_sel; // 0xFF00 selection bits
+ u8 buttons; // our pressed state (1=pressed)
+
+ // serial
+ u8 sb, sc;
+
+ // DMA
+ u8 dma;
+
+ // CGB
+ bool cgb_mode; // running in CGB mode
+ u8 key1; // speed switch
+ bool double_speed;
+
+ // HDMA / GDMA (CGB)
+ u16 hdma_src, hdma_dst;
+ u8 hdma_len;
+ bool hdma_active;
+
+ u64 cycles; // total T-cycles elapsed
+ bool serial_log; // print serial output to stderr (blargg tests)
+};
+
+void gb_init(GB *gb);
+void gb_reset(GB *gb);
+// Run one full CPU instruction (advancing all subsystems). Returns T-cycles.
+int gb_step(GB *gb);
+void gb_run_frame(GB *gb);
+
+// bus access
+u8 gb_read(GB *gb, u16 addr);
+void gb_write(GB *gb, u16 addr, u8 val);
+void gb_tick(GB *gb, int tcycles); // advance ppu/timer/dma by t-cycles
+
+void gb_request_interrupt(GB *gb, u8 flag);
+
+#endif
diff --git a/src/types.h b/src/types.h
new file mode 100644
index 0000000..ceed8a0
--- /dev/null
+++ b/src/types.h
@@ -0,0 +1,16 @@
+#ifndef GBC_TYPES_H
+#define GBC_TYPES_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+
+#endif