SearchA-ZJ › jsSMS

jsSMS

2013 Open source · GPL-3.0 Online

jsSMS is a Sega Master System and Game Gear emulator written entirely in JavaScript by Guillaume Marty. It ships both a dynamic recompiler (which JIT-compiles Z80 code to JavaScript) and a plain interpreter; the embed here runs the interpreter so the whole Z80 can be single-stepped in the debugger. Rendering is to an HTML5 canvas.

Visit the project on GitHub ↗

Visit the official site ↗

Runs on: Web browser

jsSMS Online Emulator

Play jsSMS using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Rainbow demojsSMSSega Master SystemopenOpen ⛶
Rainbow demojsSMSMaster System IIopenOpen ⛶

Machines emulated

Chips

Notes

Embedding

jsSMS is a set of plain-global JavaScript modules (no bundler). Vendor the core files and load them in dependency order. It ships two CPU back-ends: a dynamic recompiler (which JIT-compiles Z80 blocks to JavaScript via esprima/escodegen) and a plain interpreter. We construct it with ENABLE_COMPILER:false so it runs the interpreter. The recompiler cannot be paused mid-block or stepped one instruction at a time, which the debugger needs.

Boot. Give JSSMS your own UI object (jsSMS calls ui.writeFrame() once per frame and the VDP draws straight into ui.canvasImageData.data), hand it a ROM as a binary string, reset, then drive it from your own loop built on cpu.frame():

var sms = new JSSMS({ ui: EmuUI, ENABLE_COMPILER: false });
var buf = await (await fetch(romUrl)).arrayBuffer();
var bin = ''; new Uint8Array(buf).forEach(b => bin += String.fromCharCode(b));
sms.readRomDirectly(bin, 'game.sms');   // parse header, page the cartridge
sms.reset();
(function loop(){ sms.cpu.frame(); requestAnimationFrame(loop); })();

The machine is plain objects. With the interpreter the whole Z80 state is live JavaScript. Nothing hidden inside compiled blocks:

MemberKindWhat it does
sms.cpu.frame()methodInterpret one whole video frame (a scanline at a time, driving the VDP and interrupts). The fast-run primitive.
sms.cpu.interpret()methodFetch and execute exactly one Z80 instruction at pc. The single-step primitive.
sms.cpu.eol()methodEnd-of-scanline: render the line, service VDP interrupts; returns true at end of frame.
sms.cpu.reset()methodReset the Z80 (PC→0, SP→0xDFF0, like the SMS BIOS leaves it).
sms.cpu.pc / .sp / .a / .f / .b .c .d .e .h .lfieldLive Z80 registers (8-bit halves, plus the shadow set a2 b2 … and ixL ixH iyL iyH i r).
sms.cpu.getUint8(a)methodRead the banked Z80 address space (ROM pages via the 0xFFFC–0xFFFF mapper registers, 8 KB work RAM at 0xC000, on-cart SRAM).
sms.cpu.setUint8(a, v)methodWrite the bus, poke RAM or hit a mapper register live.
sms.cpu.frameRegfieldThe four Sega-mapper paging registers that select which 16 KB ROM bank answers each slot.
sms.vdpfieldThe VDP video chip; vdp.VRAM is the 16 KB video RAM, vdp.CRAM the colour RAM, and it rasters into your canvas image.
sms.keyboard.controller1fieldPlayer-1 pad latch (active-low: clear a bit to press). What an on-screen gamepad drives.
sms.pause_buttonfieldSet true to trigger the console Pause (a Z80 NMI), checked once per frame.

Because the interpreter keeps every register and the whole bus as ordinary JavaScript, the debugger single-steps with interpret(), reads and writes registers straight off sms.cpu, and implements execution breakpoints and write watchpoints as host-side checks around those calls, no changes to the emulator core.

Debugger integration

The debugger plug-in (jssms-debug.js) reads the live machine from window.EMU_BOOT and calls EmuKit.defineMachine. The Z80 is disassembled by the shared z80 decoder (/debugger/src/cpus/z80.js) - jsSMS is the first Z80 machine on the site to use it - so this file only has to expose a program counter, the register set and a byte reader.

Owning the loop. We do not call sms.start() (its internal requestAnimationFrame loop can't be paused or stepped). Instead we re-implement cpu.frame() ourselves so we can check the program counter against a breakpoint set before every instruction: when no breakpoints are set we run the native cpu.frame() at full speed; when any are set we loop interpret() a scanline at a time, halting the moment pc hits a breakpoint. Write watchpoints wrap cpu.setUint8/setUint16 and pause when a watched address is written.

Registers. Z80 registers live as 8-bit halves on sms.cpu; the plug-in presents the 16-bit pairs AF BC DE HL IX IY SP PC and decodes the flag byte (S Z H P/V N C) into individual flag chips, each writable.

Architecture

jsSMS is an interpreted (and, unused here, dynamically recompiling) Master System / Game Gear. Each chip is its own object hanging off the top-level JSSMS:

  • Z80 - the Zilog Z80 CPU. interpret() is a giant opcode switch (base page plus the CB, ED, DD/FD and DDCB/FDCB prefixes); frame() drives it a scanline at a time and hands off to the VDP at end of line.
  • Vdp - the Sega VDP (a superset of the TMS9918): tile/sprite rendering in Mode 4 into a 256×192 32-bit framebuffer, with its own 16 KB VRAM and colour RAM.
  • SN76489 - the PSG sound chip (three square waves and a noise channel); silent here, we disable audio for headless use.
  • Ports - the Z80 I/O port map wiring the VDP, PSG and controllers to IN/OUT.
  • Keyboard - the two controller latches and the Pause / Start lines.
  • Sega mapper - the cartridge is paged in 16 KB banks through registers at 0xFFFC–0xFFFF, held in cpu.frameReg.

Each cpu.frame() interprets a frame's worth of Z80 cycles, rendering each scanline as it goes, then repaints the canvas. Because the interpreter keeps the machine as ordinary objects, the entire state is inspectable and pokeable at runtime, which is what makes it a good debugging target.