aboutsummaryrefslogtreecommitdiffstats
path: root/web/gbcore.js
blob: 8cecb820fc2b8b60dfc3e15438d16f5ee4e003cc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// 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,
};