// ---- wasm entry point: the browser's "main" ------------------------------- // // The native build has render.c / kitty.c / control.c / main.c wrapped around // the core; a browser has none of that. This file is the whole host layer for // the wasm target: no terminal, no sockets, no files, no frame pacing (the // page's requestAnimationFrame owns that). It just exposes the core as a // handful of C functions the page can call, plus two buffers it can read. // // The contract, in the order a page uses it: // _initialize() once, libc + ctors (wasi reactor model) // p = gbw_alloc(len) get a staging buffer for the ROM bytes // gbw_load(p, len) parse cart + reset (0 = ok) // gbw_free(p) staging buffer is no longer needed // gbw_set_buttons(mask) BTN_* bits, 1 = pressed // gbw_run_frame() advance exactly one frame // gbw_framebuffer_rgba() 160*144*4 bytes, ready for putImageData // // Everything hands back a pointer into the wasm linear memory; the page reads // it through a typed array view. Nothing here allocates per frame. #include "gb.h" #include "cart.h" #include #include #define EXPORT(name) __attribute__((export_name(#name))) name // One machine, statically allocated: a GB is ~120KB of arrays, and keeping it // out of the heap means the ROM upload is the only thing that ever mallocs. static GB gb; static bool loaded = false; // The PPU draws RGB888, but canvas ImageData wants RGBA8888. Converting here // costs one pass over 23k pixels in wasm instead of a per-pixel loop in JS. static u8 fb_rgba[SCREEN_H * SCREEN_W * 4]; // ---- memory handoff ------------------------------------------------------- // The page can't write into wasm memory without somewhere to put things. void *EXPORT(gbw_alloc)(int n) { return malloc((size_t)n); } void EXPORT(gbw_free)(void *p) { free(p); } // ---- lifecycle ------------------------------------------------------------ // Parse a ROM image and reset the machine. Returns 0 on success. // Safe to call again to swap games; the old cart is released first. int EXPORT(gbw_load)(const u8 *rom, int len) { if (loaded) { cart_free(&gb.cart); // no-op battery save in this build loaded = false; } Cart c; if (cart_load_mem(&c, rom, (size_t)len) != 0) return -1; gb_init(&gb); // zeroes the machine, keeps any boot ROM gb.cart = c; // install the cart, then boot into it gb_reset(&gb); loaded = true; return 0; } // Install a boot ROM (256B DMG / 2304B CGB) so the next load boots through the // power-on animation. Must be called before gbw_load. Returns 0 on success. int EXPORT(gbw_load_bootrom)(const u8 *data, int len) { return gb_load_bootrom_mem(&gb, data, len); } void EXPORT(gbw_reset)(void) { if (loaded) gb_reset(&gb); } int EXPORT(gbw_is_loaded)(void) { return loaded ? 1 : 0; } // ---- running -------------------------------------------------------------- // Advance exactly one frame (to the next VBlank). The page calls this from // requestAnimationFrame; the emulator itself does no timing. void EXPORT(gbw_run_frame)(void) { if (loaded) gb_run_frame(&gb); } // Pressed-button bitmask (BTN_A | BTN_LEFT | ...), 1 = pressed. Unlike the // terminal, a browser reports key-release, so this is just the live state. void EXPORT(gbw_set_buttons)(int mask) { gb.buttons = (u8)mask; } // ---- reading the screen --------------------------------------------------- // Convert the PPU framebuffer to RGBA and return it. 160*144*4 bytes. const u8 *EXPORT(gbw_framebuffer_rgba)(void) { const u8 *src = &gb.ppu.fb[0][0][0]; u8 *dst = fb_rgba; for (int i = 0; i < SCREEN_W * SCREEN_H; i++) { *dst++ = *src++; *dst++ = *src++; *dst++ = *src++; *dst++ = 0xFF; } return fb_rgba; } // Raw RGB888 straight out of the PPU (160*144*3) - what --shot writes, so the // golden-frame test can compare it byte for byte against a native PPM. const u8 *EXPORT(gbw_framebuffer_rgb)(void) { return &gb.ppu.fb[0][0][0]; } int EXPORT(gbw_screen_w)(void) { return SCREEN_W; } int EXPORT(gbw_screen_h)(void) { return SCREEN_H; } // ---- cart info & battery saves -------------------------------------------- const char *EXPORT(gbw_title)(void) { return gb.cart.title; } int EXPORT(gbw_is_cgb)(void) { return gb.cart.cgb ? 1 : 0; } int EXPORT(gbw_has_battery)(void) { return gb.cart.has_battery ? 1 : 0; } // Cartridge RAM, for the page to persist (IndexedDB/localStorage) and restore. // There is no .sav file in a browser, so this *is* the save. u8 *EXPORT(gbw_sram_ptr)(void) { return gb.cart.ram; } int EXPORT(gbw_sram_size)(void) { return (int)gb.cart.ram_size; } // Overwrite cartridge RAM from a previously saved blob. Returns bytes copied; // a size mismatch copies nothing (wrong game, or a different MBC layout). int EXPORT(gbw_sram_load)(const u8 *data, int len) { if (!gb.cart.ram || (size_t)len != gb.cart.ram_size) return 0; memcpy(gb.cart.ram, data, (size_t)len); return len; } // Total T-cycles since power-on; handy for pacing checks and debugging. double EXPORT(gbw_cycles)(void) { return (double)gb.cycles; } // ---- link cable ----------------------------------------------------------- // The page plays the part tools/gbhub plays natively: drain what this Game Boy // transmitted, decide where it goes, push bytes back in. SLIP-framed bytes are // IP packets; unframed bytes are console I/O (which is why typing into the // link port lands on the shell's stdin). // // A fixed staging buffer avoids allocating per frame; the page reads it // through a typed array right after the call. static u8 serial_buf[2048]; // Bytes this Game Boy put on the wire since the last drain. Returns the count; // the data sits at gbw_serial_buf(). int EXPORT(gbw_serial_drain)(void) { return gb_serial_tx_drain(serial_buf, (int)sizeof serial_buf); } u8 *EXPORT(gbw_serial_buf)(void) { return serial_buf; } // Hand bytes to this Game Boy's link port (write them into gbw_serial_buf() // first, then call this with the length). void EXPORT(gbw_serial_push)(int n) { if (n > 0) gb_serial_rx_push(serial_buf, n > (int)sizeof serial_buf ? (int)sizeof serial_buf : n); } // RX bytes dropped because the guest wasn't consuming - a wedged link shows up // here as a climbing number instead of silently losing packets. int EXPORT(gbw_serial_dropped)(void) { return gb_serial_rx_dropped(); }