diff options
| author | gbc dev <gbc@localhost> | 2026-09-22 15:56:52 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-09-22 15:56:52 +0200 |
| commit | c4848bb0a114a95aaeb282ac105087f2913eb012 (patch) | |
| tree | f2598990d638ca8cedd5779e1250cb4a1a95d3b1 | |
| parent | kitty: truecolor graphics output with a layered HUD (diff) | |
| download | sl0pboy-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.c | 79 | ||||
| -rw-r--r-- | src/cart.h | 6 | ||||
| -rw-r--r-- | src/gb.c | 37 | ||||
| -rw-r--r-- | src/gb.h | 3 | ||||
| -rw-r--r-- | src/main.c | 10 |
5 files changed, 110 insertions, 25 deletions
@@ -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); @@ -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); @@ -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; @@ -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); @@ -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 |
