aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-09-22 15:56:52 +0200
committergbc dev <gbc@localhost>2026-09-22 15:56:52 +0200
commitc4848bb0a114a95aaeb282ac105087f2913eb012 (patch)
treef2598990d638ca8cedd5779e1250cb4a1a95d3b1
parentkitty: truecolor graphics output with a layered HUD (diff)
downloadsl0pboy-c4848bb0a114a95aaeb282ac105087f2913eb012.tar.gz
sl0pboy-c4848bb0a114a95aaeb282ac105087f2913eb012.tar.xz
sl0pboy-c4848bb0a114a95aaeb282ac105087f2913eb012.zip
core: make the machine loadable without a filesystem
Groundwork for building the core for targets that have no files, no sockets and no terminal (a browser, next commit). Three changes, no behaviour change for the native binary: - cart_load() splits into cart_init_owned() (everything past the file read) plus cart_load_mem(), which takes a ROM image already in memory. The path version keeps owning the .sav wiring; a memory load attaches no battery file and leaves save_path empty, which cart_save() now treats as "nowhere to write". - gb_load_bootrom_mem() does the same for boot ROMs, so the power-on animation works wherever the image comes from. - gb_run_frame() was declared in gb.h but never existed; main.c had a private run_frame() instead. Implement it in gb.c and point main.c at it, so every host advances the machine on one identical boundary. The link-socket layer (--serial-sock) is the only genuinely unportable part of the core, so under __wasm__ it becomes no-op stubs: the link is simply always down and the call sites stay untouched. The file-backed loaders compile out there too, which keeps the libc surface tiny. Verified: blargg cpu_instrs (11/11), instr_timing, mem_timing, mem_timing-2 all still Passed; --shot output unchanged.
-rw-r--r--src/cart.c79
-rw-r--r--src/cart.h6
-rw-r--r--src/gb.c37
-rw-r--r--src/gb.h3
-rw-r--r--src/main.c10
5 files changed, 110 insertions, 25 deletions
diff --git a/src/cart.c b/src/cart.c
index 95cd279..6bab6a8 100644
--- a/src/cart.c
+++ b/src/cart.c
@@ -59,18 +59,12 @@ static size_t ram_size_bytes(u8 code) {
}
}
-int cart_load(Cart *c, const char *path) {
+// Set up a cart around a ROM image already in memory, TAKING OWNERSHIP of
+// `rom` (freed by cart_free). Everything past the file read is identical for
+// a path load and a browser upload, so both entry points land here.
+static int cart_init_owned(Cart *c, u8 *rom, size_t sz) {
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 = rom;
c->rom_size = sz;
memcpy(c->title, c->rom + 0x134, 16);
@@ -102,6 +96,57 @@ int cart_load(Cart *c, const char *path) {
c->rom_bank = 1;
c->rom_bank_hi = 0;
c->ram_bank = 0;
+ c->save_path[0] = 0; // no backing file unless a path load sets one
+
+ 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;
+}
+
+// Load from a ROM image in memory (browser upload, test fixture). Copies it,
+// so the caller keeps ownership of `data`. No battery file is attached -
+// persist cart_ram_ptr()/cart_ram_size() yourself.
+int cart_load_mem(Cart *c, const u8 *data, size_t sz) {
+ if (!data || sz < 0x150) {
+ fprintf(stderr, "rom too small\n");
+ return -1;
+ }
+ u8 *rom = malloc(sz);
+ if (!rom) return -1;
+ memcpy(rom, data, sz);
+ return cart_init_owned(c, rom, sz);
+}
+
+#ifndef __wasm__
+int cart_load(Cart *c, const char *path) {
+ 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;
+ }
+
+ u8 *rom = malloc(sz);
+ if (!rom) {
+ fclose(f);
+ return -1;
+ }
+ if (fread(rom, 1, sz, f) != (size_t)sz) {
+ free(rom);
+ fclose(f);
+ return -1;
+ }
+ fclose(f);
+
+ if (cart_init_owned(c, rom, sz) != 0) return -1;
// battery save path: rom path with .sav
snprintf(c->save_path, sizeof(c->save_path), "%s.sav", path);
@@ -112,22 +157,22 @@ int cart_load(Cart *c, const char *path) {
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;
+ if (!c->has_battery || !c->ram || !c->save_path[0]) return;
FILE *sf = fopen(c->save_path, "wb");
if (sf) {
fwrite(c->ram, 1, c->ram_size, sf);
fclose(sf);
}
}
+#else
+// No filesystem in the browser: battery RAM is handed to the host instead
+// (see cart_ram_ptr/cart_ram_size in the wasm API).
+void cart_save(Cart *c) { (void)c; }
+#endif // !__wasm__
void cart_free(Cart *c) {
cart_save(c);
diff --git a/src/cart.h b/src/cart.h
index ec864bb..8fcf1b5 100644
--- a/src/cart.h
+++ b/src/cart.h
@@ -42,8 +42,12 @@ typedef struct {
char save_path[512];
} Cart;
-// Load a ROM file into cart. Returns 0 on success.
+// Load a ROM file into cart. Returns 0 on success. (Not in the wasm build -
+// there is no filesystem to load from.)
int cart_load(Cart *c, const char *path);
+// Load from a ROM image already in memory; copies it, so the caller keeps
+// ownership of `data`. No battery file is attached. Returns 0 on success.
+int cart_load_mem(Cart *c, const u8 *data, size_t sz);
void cart_free(Cart *c);
void cart_save(Cart *c);
diff --git a/src/gb.c b/src/gb.c
index aed51b7..89a792b 100644
--- a/src/gb.c
+++ b/src/gb.c
@@ -9,8 +9,10 @@
#include <errno.h>
#include <fcntl.h>
#include <time.h>
+#ifndef __wasm__
#include <sys/socket.h>
#include <sys/un.h>
+#endif
// ---------- link socket resilience (--serial-sock) ----------
// The link/network bridge socket can die under us (hub restarted, peer
@@ -18,6 +20,17 @@
// a background reconnect at most every 500ms from the serial poll path. RX
// stays "pending" while down (serial_no_eof), so the guest just sees a quiet
// link that comes back.
+//
+// There are no unix sockets in a browser, so the wasm build gets no-op stubs
+// instead: the link is simply always down (serial_*_fd stay -1, so the serial
+// paths below never touch a descriptor) and the call sites stay identical.
+#ifdef __wasm__
+static bool serial_check(GB *gb) {
+ (void)gb;
+ return false;
+}
+static void serial_mark_dead(GB *gb) { (void)gb; }
+#else
static double mono_sec(void) {
struct timespec ts;
@@ -59,6 +72,7 @@ static bool serial_check(GB *gb) {
gb->serial_sock_path);
return true;
}
+#endif // !__wasm__
void gb_request_interrupt(GB *gb, u8 flag) {
gb->iff |= flag;
@@ -287,6 +301,15 @@ void gb_hdma_hblank(GB *gb) {
}
}
+// Run until the PPU finishes the next frame. The cycle budget is the escape
+// hatch for a disabled LCD, which never raises frame_ready: two frames' worth
+// of cycles and we return regardless, so the caller can't wedge.
+void gb_run_frame(GB *gb) {
+ gb->ppu.frame_ready = false;
+ u64 budget = gb->cycles + 70224 * 2;
+ while (!gb->ppu.frame_ready && gb->cycles < budget && !gb->poweroff) cpu_step(gb);
+}
+
void gb_init(GB *gb) {
u8 *saved_boot = gb->boot_rom; // preserve a boot ROM loaded pre-init
int saved_len = gb->boot_rom_len;
@@ -296,6 +319,19 @@ void gb_init(GB *gb) {
gb_reset(gb);
}
+// Install a boot ROM already sitting in memory (the browser has no files to
+// open, and it keeps the size check in one place). Takes a copy.
+int gb_load_bootrom_mem(GB *gb, const u8 *data, int n) {
+ if (!data || (n != 0x100 && n != 0x800 && n != 0x900)) return -1;
+ free(gb->boot_rom);
+ gb->boot_rom = malloc(n);
+ if (!gb->boot_rom) return -1;
+ memcpy(gb->boot_rom, data, n);
+ gb->boot_rom_len = n;
+ return 0;
+}
+
+#ifndef __wasm__
int gb_load_bootrom(GB *gb, const char *path) {
FILE *f = fopen(path, "rb");
if (!f) { perror("open boot rom"); return -1; }
@@ -317,6 +353,7 @@ int gb_load_bootrom(GB *gb, const char *path) {
fclose(f);
return 0;
}
+#endif // !__wasm__
void gb_reset(GB *gb) {
CPU *c = &gb->cpu;
diff --git a/src/gb.h b/src/gb.h
index 33746b4..a03204f 100644
--- a/src/gb.h
+++ b/src/gb.h
@@ -137,7 +137,10 @@ void gb_init(GB *gb);
void gb_reset(GB *gb);
// Load a boot ROM (BIOS) image so the next gb_reset boots through it. Returns
// 0 on success. 256-byte images boot as DMG; 2304-byte images as CGB.
+// (The path version is absent from the wasm build - no filesystem.)
int gb_load_bootrom(GB *gb, const char *path);
+// Same, from an image already in memory; takes a copy.
+int gb_load_bootrom_mem(GB *gb, const u8 *data, int n);
// Run one full CPU instruction (advancing all subsystems). Returns T-cycles.
int gb_step(GB *gb);
void gb_run_frame(GB *gb);
diff --git a/src/main.c b/src/main.c
index a810e99..1b6a7b5 100644
--- a/src/main.c
+++ b/src/main.c
@@ -57,13 +57,9 @@ static double now_sec(void) {
return ts.tv_sec + ts.tv_nsec / 1e9;
}
-static void run_frame(GB *gb) {
- gb->ppu.frame_ready = false;
- // safety bound in case LCD is off (no frame_ready)
- u64 budget = gb->cycles + 70224 * 2;
- while (!gb->ppu.frame_ready && gb->cycles < budget && !gb->poweroff)
- cpu_step(gb);
-}
+// (the frame loop itself lives in gb.c as gb_run_frame, so the native binary
+// and the wasm build advance on exactly the same boundary)
+#define run_frame gb_run_frame
// Scripted button taps for testing: each char in `keys` = one token occupying
// 20 frames (pressed for the first 10, released for the next 10, so the guest