// One Game Boy = one wasm instantiation. // // The core keeps its machine in a static, so multiple Game Boys are multiple // *instances* of the same compiled module rather than a handle-based API in C. // Each gets its own linear memory, which means total isolation for free - and // compiling once and instantiating N times is cheap. const EBADF = 8; const dec = new TextDecoder(); // Compile once; hand the Module to every GameBoy.create(). export async function loadCore(url = "sl0pboy.wasm") { const res = await fetch(url); if (!res.ok) throw new Error(`fetch ${url}: HTTP ${res.status}`); try { return await WebAssembly.compileStreaming(res.clone()); } catch { return await WebAssembly.compile(await res.arrayBuffer()); // bad MIME type } } export class GameBoy { #ex; // wasm exports #onLog; static async create(module, rom, { onLog = null } = {}) { const gb = new GameBoy(); gb.#onLog = onLog; // The whole host interface: four stdio fd_* calls, from the cart banner. const wasi = { fd_write: (fd, iovs, iovsLen, nwritten) => { const dv = new DataView(gb.#ex.memory.buffer); let total = 0; const parts = []; for (let i = 0; i < iovsLen; i++) { const p = dv.getUint32(iovs + i * 8, true); const l = dv.getUint32(iovs + i * 8 + 4, true); parts.push(new Uint8Array(gb.#ex.memory.buffer, p, l).slice()); total += l; } if (gb.#onLog) { let n = 0; for (const a of parts) n += a.length; const flat = new Uint8Array(n); let o = 0; for (const a of parts) { flat.set(a, o); o += a.length; } const s = dec.decode(flat).trimEnd(); if (s) gb.#onLog(s); } dv.setUint32(nwritten, total, true); return 0; }, fd_read: () => EBADF, fd_seek: () => EBADF, fd_close: () => EBADF, }; const inst = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasi }); gb.#ex = inst.exports; gb.#ex._initialize(); // reactor model: libc + ctors if (rom) gb.load(rom); return gb; } // Copy bytes into wasm memory. Views are built fresh every time: any // allocation can grow the memory and detach every existing view. #into(bytes) { const ptr = this.#ex.gbw_alloc(bytes.length); if (!ptr) throw new Error("gbw_alloc failed"); new Uint8Array(this.#ex.memory.buffer, ptr, bytes.length).set(bytes); return ptr; } #cstr(ptr) { const m = new Uint8Array(this.#ex.memory.buffer); let e = ptr; while (m[e]) e++; return dec.decode(m.subarray(ptr, e)); } load(rom) { const p = this.#into(rom); const rc = this.#ex.gbw_load(p, rom.length); this.#ex.gbw_free(p); if (rc !== 0) throw new Error("not a Game Boy ROM"); } reset() { this.#ex.gbw_reset(); } runFrame() { this.#ex.gbw_run_frame(); } setButtons(mask) { this.#ex.gbw_set_buttons(mask); } get title() { return this.#cstr(this.#ex.gbw_title()); } get isCGB() { return !!this.#ex.gbw_is_cgb(); } get hasBattery() { return !!this.#ex.gbw_has_battery(); } get cycles() { return this.#ex.gbw_cycles(); } // 160*144*4, valid until the next wasm call that allocates. framebufferRGBA() { return new Uint8Array(this.#ex.memory.buffer, this.#ex.gbw_framebuffer_rgba(), 160 * 144 * 4); } // ---- link cable ---- // Bytes this Game Boy transmitted since the last call (a copy, so it stays // valid across later wasm calls). drainSerial() { const n = this.#ex.gbw_serial_drain(); if (!n) return null; return new Uint8Array(this.#ex.memory.buffer, this.#ex.gbw_serial_buf(), n).slice(); } // Hand bytes to its link port. pushSerial(bytes) { if (!bytes || !bytes.length) return; const buf = this.#ex.gbw_serial_buf(); new Uint8Array(this.#ex.memory.buffer, buf, bytes.length).set(bytes); this.#ex.gbw_serial_push(bytes.length); } get serialDropped() { return this.#ex.gbw_serial_dropped(); } // This machine's share of wasm linear memory - what N Game Boys really cost. get memBytes() { return this.#ex.memory.buffer.byteLength; } // ---- battery RAM ---- sram() { const n = this.#ex.gbw_sram_size(); return n ? new Uint8Array(this.#ex.memory.buffer, this.#ex.gbw_sram_ptr(), n).slice() : null; } loadSram(bytes) { const p = this.#into(bytes); const n = this.#ex.gbw_sram_load(p, bytes.length); this.#ex.gbw_free(p); return n === bytes.length; } } // Joypad bits, straight from gb.h. export const BTN = { A: 0x01, B: 0x02, SELECT: 0x04, START: 0x08, RIGHT: 0x10, LEFT: 0x20, UP: 0x40, DOWN: 0x80, };