aboutsummaryrefslogtreecommitdiffstats
path: root/src/cart.c
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 /src/cart.c
downloadsl0pboy-0d0f4f1d23851c0c34e518834f7d316e61dec1f3.tar.gz
sl0pboy-0d0f4f1d23851c0c34e518834f7d316e61dec1f3.tar.xz
sl0pboy-0d0f4f1d23851c0c34e518834f7d316e61dec1f3.zip
scaffolding: build system, cartridge loader with MBC1/2/3/5, core types
Diffstat (limited to 'src/cart.c')
-rw-r--r--src/cart.c253
1 files changed, 253 insertions, 0 deletions
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;
+ }
+}