diff options
| author | gbc dev <gbc@localhost> | 2026-07-04 22:05:31 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-04 22:05:31 +0200 |
| commit | 0d0f4f1d23851c0c34e518834f7d316e61dec1f3 (patch) | |
| tree | e46296c247617ee0ead1fb93e9d0dc475a07f9b9 | |
| download | sl0pboy-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-- | .gitignore | 7 | ||||
| -rw-r--r-- | Makefile | 27 | ||||
| -rw-r--r-- | src/cart.c | 253 | ||||
| -rw-r--r-- | src/cart.h | 53 | ||||
| -rw-r--r-- | src/gb.h | 127 | ||||
| -rw-r--r-- | src/types.h | 16 |
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 |
