jsSMS
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.
Runs on: Web browser
jsSMS Online Emulator
Play jsSMS using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Rainbow demo | jsSMS | Sega Master System | open | Open ⛶ | |
| Rainbow demo | jsSMS | Master System II | open | Open ⛶ |
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:
| Member | Kind | What it does |
|---|---|---|
sms.cpu.frame() | method | Interpret one whole video frame (a scanline at a time, driving the VDP and interrupts). The fast-run primitive. |
sms.cpu.interpret() | method | Fetch and execute exactly one Z80 instruction at pc. The single-step primitive. |
sms.cpu.eol() | method | End-of-scanline: render the line, service VDP interrupts; returns true at end of frame. |
sms.cpu.reset() | method | Reset the Z80 (PC→0, SP→0xDFF0, like the SMS BIOS leaves it). |
sms.cpu.pc / .sp / .a / .f / .b .c .d .e .h .l | field | Live Z80 registers (8-bit halves, plus the shadow set a2 b2 … and ixL ixH iyL iyH i r). |
sms.cpu.getUint8(a) | method | Read 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) | method | Write the bus, poke RAM or hit a mapper register live. |
sms.cpu.frameReg | field | The four Sega-mapper paging registers that select which 16 KB ROM bank answers each slot. |
sms.vdp | field | The 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.controller1 | field | Player-1 pad latch (active-low: clear a bit to press). What an on-screen gamepad drives. |
sms.pause_button | field | Set 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 toIN/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 incpu.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.