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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
// ---- 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 <stdlib.h>
#include <string.h>
#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(); }
|