gameboyjs
gameboyjs is a pure-JavaScript Nintendo Game Boy (DMG) emulator by Rob Louie. It interprets the Sharp SM83 CPU and renders the picture processor scanline by scanline to a canvas, so it runs a Game Boy directly in the browser with no install. Here it is wired into the in-page debugger, where you can single-step the SM83, set breakpoints and watchpoints, and inspect the whole memory map.
Runs on: Web browser
gameboyjs Online Emulator
Play gameboyjs using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Libbet and the Magic Flute | gameboyjs | Game Boy | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
gameboy-emulator (roblouie) is a pure-JavaScript Game Boy emulator shipped as a UMD bundle that exposes a single global, gameboy.Gameboy. Vendor gameboy.js and drive the machine yourself instead of calling gameboy.run() - its internal requestAnimationFrame loop cannot be paused or single-stepped, which the debugger needs.
Boot and load a ROM. Construct the machine, hand it the cartridge as an ArrayBuffer, replicate run()'s init, then drive your own loop on cpu.tick() (one instruction, returns its cycle count) with gpu.tick() and apu.tick() clocked in lockstep:
const gb = new gameboy.Gameboy();
const buf = await (await fetch(romUrl)).arrayBuffer();
gb.loadGame(buf);
gb.cpu.initialize(); // PC=00, post-boot register values
gb.memory.reset();
gb.memory.writeByte(0xFF40, 0x83); // turn the LCD on (LCDC)
const ctx = canvas.getContext('2d');
(function frame(){
let cycles = 0;
while (cycles <= 70224) { // one video frame of dot clocks
const c = gb.cpu.tick(); // exactly one SM83 instruction
gb.gpu.tick(c); gb.apu.tick(c); cycles += c;
}
ctx.putImageData(gb.gpu.screen, 0, 0); // gpu.screen is a 160x144 ImageData
requestAnimationFrame(frame);
})();
The machine is plain objects. Everything the debugger needs is a live property, with no wasm heap to reach into:
| Member | Kind | What it does |
|---|---|---|
cpu.tick() | method | Execute exactly one SM83 instruction; returns its cycle count. The single-step primitive. |
cpu.initialize() | method | Set PC to 00 and the registers to their documented post-boot values. |
cpu.registers | field | Live registers: .A .F .B .C .D .E .H .L, the pairs .AF .BC .DE .HL, and .programCounter / .stackPointer. Each has a .value get/set. |
cpu.registers.F | field | The flag register with .Z .N .H .CY bit accessors (bits 7/6/5/4). |
gpu.tick(cycles) | method | Advance the PPU; when a scanline completes it rasters pixels into gpu.screen. |
gpu.screen | field | A 160×144 ImageData (subclass) painted straight to a 2D canvas with putImageData. |
memory.readByte(a) / writeByte(a, v) | method | The CPU bus: routes cartridge, I/O and RAM. writeByte pokes memory live. |
memory.memoryBytes | field | The raw 64 KB internal store (VRAM, WRAM, OAM, I/O, HRAM) as a Uint8Array - side-effect-free reads. |
cartridge | field | The loaded cartridge: .readByte, .romSize, .title, MBC banking. |
input | field | Eight plain booleans - isPressingUp/Down/Left/Right/A/B/Start/Select - set by a pad or keyboard. |
Because the CPU, PPU and memory are ordinary JavaScript, the debugger single-steps with cpu.tick(), reads and writes registers straight off cpu.registers, and implements breakpoints and watchpoints as host-side checks around those calls, no changes to the emulator core.
Debugger integration
Wiring gameboy-emulator into the shared in-browser debugger needed one piece of custom work, a boot shim that owns the loop, plus a set of techniques for reaching into the live machine.
A boot shim, because run() owns an un-pausable loop. The library's Gameboy.run() drives itself with an internal requestAnimationFrame loop that cannot be paused or single-stepped from outside, which the debugger needs. So the boot replicates run()'s init (cpu.initialize(), memory.reset(), LCD on) and then drives the machine from a loop we control:
// our loop, not the library's, so pause / step / breakpoints work
function runFrame(){
let cyc = 0;
while (cyc <= 70224) {
if (bps.size && bps.has(cpu.registers.programCounter.value)) { running = false; return; }
const c = cpu.tick(); gpu.tick(c); apu.tick(c); cyc += c;
}
}
function stepInsn(){ const c = cpu.tick(); gpu.tick(c); apu.tick(c); }
Techniques for deeper access. The plug-in then reaches into the running machine through the library's own surfaces, no fork of the emulator core:
- Side-effect-free reads. The hex and disassembly views read VRAM, WRAM, OAM, I/O and HRAM straight off
memory.memoryBytes(the rawUint8Array), so auto-polling a view never routes through the joypad or DMA logic. The cartridge chip reads throughcartridge.readByte, which is a pure ROM lookup. - Direct state. Registers are plain objects with a
.valueget/set, read and written live; the flag register exposesZ/N/H/CYbits, surfaced as clickable flag chips. - Single instruction step.
cpu.tick()already executes exactly one instruction and returns its cycles, so one call is one step - withgpu.tick/apu.tickclocked by the same count to keep the machine coherent. - Execution breakpoints. Because we own the loop, a breakpoint is a host-side
Setof PC values checked before eachtick(); when the loop is stepping instruction-by-instruction it halts the moment PC matches. - Write watchpoints. The boot wraps
memory.writeByte; when a write hits a watched address it stops the loop, so a watchpoint pauses on the store.
Everything the debugger shows, registers read/write, memory hex/disassembly, follow-PC, single-step, breakpoints and watchpoints, is built from these, with no changes to the emulator itself.
Architecture
gameboy-emulator is a readable, interpreted Game Boy (DMG) by Rob Louie. Each hardware block is its own object hanging off the top-level Gameboy:
CPU- an interpreter of the Sharp SM83 (LR35902) core;tick()fetches, decodes and executes one instruction from a map of operation objects and returns its cycle count. It boots at00with the documented post-boot register state (the internal boot ROM / logo scroll is skipped).GPU- the pixel-processing unit: background, window and sprites rendered scanline-by-scanline into a 160×144ImageDatausing the four-shade DMG palette.APU- the four sound channels (two pulse, wave, noise), fed through a ring buffer to Web Audio when enabled.Memory- the 64 KB bus: cartridge (via the MBC), VRAM, WRAM, OAM, I/O registers and HRAM, plus DMA and the joypad register.Cartridge- parses the header and implements the mapper (ROM-only, MBC1, MBC3), including battery-backed save RAM.Input- eight booleans polled through the joypad register at$FF00.
Each frame runs the CPU for one video frame's worth of dot clocks while stepping the GPU and APU in lockstep, then paints the finished framebuffer. Because every component is an ordinary object, the whole machine state is inspectable, which is what the debugger reads each refresh.