SearchA-ZG › gameboyjs

gameboyjs

2021 Open source · MIT Online

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.

Visit the official site ↗

Runs on: Web browser

gameboyjs Online Emulator

Play gameboyjs using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Libbet and the Magic FlutegameboyjsGame BoyopenOpen ⛶

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:

MemberKindWhat it does
cpu.tick()methodExecute exactly one SM83 instruction; returns its cycle count. The single-step primitive.
cpu.initialize()methodSet PC to 00 and the registers to their documented post-boot values.
cpu.registersfieldLive 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.FfieldThe flag register with .Z .N .H .CY bit accessors (bits 7/6/5/4).
gpu.tick(cycles)methodAdvance the PPU; when a scanline completes it rasters pixels into gpu.screen.
gpu.screenfieldA 160×144 ImageData (subclass) painted straight to a 2D canvas with putImageData.
memory.readByte(a) / writeByte(a, v)methodThe CPU bus: routes cartridge, I/O and RAM. writeByte pokes memory live.
memory.memoryBytesfieldThe raw 64 KB internal store (VRAM, WRAM, OAM, I/O, HRAM) as a Uint8Array - side-effect-free reads.
cartridgefieldThe loaded cartridge: .readByte, .romSize, .title, MBC banking.
inputfieldEight 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 raw Uint8Array), so auto-polling a view never routes through the joypad or DMA logic. The cartridge chip reads through cartridge.readByte, which is a pure ROM lookup.
  • Direct state. Registers are plain objects with a .value get/set, read and written live; the flag register exposes Z/N/H/CY bits, 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 - with gpu.tick/apu.tick clocked by the same count to keep the machine coherent.
  • Execution breakpoints. Because we own the loop, a breakpoint is a host-side Set of PC values checked before each tick(); 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 at 00 with 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×144 ImageData using 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.