SearchA-ZW › WebMSX

WebMSX

2016 Freeware · no clear licence Online

WebMSX is an MSX emulator written in JavaScript by Paulo Peccin that runs in any modern web browser and can be installed as an offline web app on desktop and mobile. It emulates MSX, MSX2 and MSX2+ systems with sound and video extensions.

Visit the official site ↗

Runs on: Web browser

WebMSX Online Emulator

Play WebMSX using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
MSX BASIC (MSX1)WebMSXPhilips VG-8020greyOpen ⛶
MSX BASIC (MSX2)WebMSXPhilips NMS 8245greyOpen ⛶

Machines emulated

Chips

Notes

Embedding

WebMSX ships as a single bundled script (wmsx.js, the "embedded" build) that carries the free MSX system ROMs — MSX BIOS and MSX BASIC — inside it, so it self-hosts completely and boots offline. We drop a target <div id="wmsx-screen">, load wmsx.js, and set the machine BEFORE it auto-starts:

// runs synchronously right after wmsx.js defines the global WMSX,
// and before DOMContentLoaded triggers WebMSX's own boot
WMSX.MACHINE               = 'MSX1';      // boot an MSX1 to MSX BASIC
WMSX.SCREEN_ELEMENT_ID     = 'wmsx-screen';
WMSX.AUTO_POWER_ON_DELAY   = 0;           // power on immediately
WMSX.FAST_BOOT             = 1;           // 10x speed through the boot
WMSX.SCREEN_RESIZE_DISABLED = true;

We drive the loop. WebMSX runs itself from an internal clock (room.mainVideoClock) that cannot be paused or stepped from outside — which the debugger needs. So once the machine has powered on we pause() that clock and pump frames from a loop WE own, calling WebMSX's own per-frame entry point:

var room = WMSX.room, machine = room.machine;
room.mainVideoClock.pause();                    // stop WebMSX's own loop
(function frame(){
  room.mainVideoClockPulse();               // one video frame: video + Z80 + INT + audio
  if (bps.size && bps.has(pc())) running = false;   // frame-granular breakpoint
  if (running) requestAnimationFrame(frame);
})();

Reaching into the running machine. WebMSX exposes the live objects the debugger needs off WMSX.room.machine:

MemberKindWhat it does
machine.cpu.busClockPulses(n)methodAdvance the Z80 by n bus clocks. Looping it until PC moves is our single-instruction step.
machine.cpu.saveState()methodSnapshot every Z80 register into a plain object whose keys (PC, SP, A, F, B, C, DE, HL, IX, IY, AF2 …) survive minification — how the debugger reads the registers.
machine.cpu.loadState(s)methodWrite registers back. Editing one field of a saveState() snapshot and re-loading pokes a single register without disturbing the rest.
machine.bus.read(a) / write(a,v)methodRead/write the slot-paged 64K CPU address space. MSX I/O is port-mapped, so a memory read is side-effect-free — safe for an auto-polling hex view.
machine.bus.setWriteMonitor(fn)methodWebMSX's own memory-write hook; we ride it for native write watchpoints.
room.controllersHub.keyDown/keyUp(e)methodWebMSX's char-accurate key mapper. We feed it real events (physical keyboard) and synthetic ones (the on-screen keyboard).
room.mainVideoClock.pause()/go()methodStop / start WebMSX's own clock. We stop it and drive frames ourselves.

Debugger integration

Plugging WebMSX into the shared debugger meant wrapping a self-driving emulator without forking it.

1 · A driven loop, because WebMSX owns its clock. WebMSX's Clock runs the machine from an internal requestAnimationFrame that cannot be paused or stepped from outside. After the machine powers on we pause() that clock and pump room.mainVideoClockPulse() from our own loop, so pause, resume and frame-step are ours to control.

2 · Registers through save-state. The Z80 registers are closure-private locals, but cpu.saveState() returns them in an object whose KEYS are fixed strings — so they read correctly even from the minified build. We read via saveState() and write by editing one field of a snapshot and calling loadState(), which never corrupts the rest of the CPU state.

3 · Memory, the Z80 and keyboard. The hex/disassembly views read bus.read() (side-effect-free for MSX memory); the shared z80 disassembler decodes it. Single-step loops cpu.busClockPulses(1) until PC advances. Watchpoints ride WebMSX's own bus.setWriteMonitor(). Keys go through WebMSX's own char-accurate mapper (controllersHub.keyDown/keyUp), so the physical keyboard and the on-screen MSX keyboard both produce the right symbols.

Limitation — breakpoint granularity. On MSX the Z80 is clocked from inside the VDP each frame, and the minified build caches that CPU-clock reference where it cannot be interposed. So there is no seam to check the PC at every instruction while running at speed. Execution breakpoints are therefore honoured at frame granularity during a run (the loop checks the PC once per frame), and exactly, instruction-by-instruction, while single-stepping. Pause, resume, frame-step, instruction-step, register read/write, memory read/write and watchpoints are all exact.

Architecture

WebMSX is a full MSX line emulator by Paulo Peccin (MSX1, MSX2, MSX2+, turbo R; NTSC or PAL). The whole machine hangs off WMSX.room.machine:

  • cpu — the Zilog Z80 interpreter (an R800 core is added for turbo R). busClockPulses() clocks it; saveState()/loadState() expose the registers.
  • bus — the slot/subslot memory map: BIOS, RAM, cartridges and mappers paged into the 64K address space, plus the port-mapped I/O devices.
  • vdp — the video chip (V9918 on MSX1; V9938/V9958 on MSX2+), which also clocks the CPU and raises the 60/50Hz interrupt each frame; it rasters into a canvas.
  • PSG (AY-3-8910) and the optional SCC, MSX-MUSIC (OPLL), PCM and OPL4 sound devices.
  • system ROMs — the MSX BIOS and MSX BASIC, embedded in the build as base64 and paged into slot 0.

Because the CPU, bus and registers are reachable as ordinary objects, the debugger reads and drives the live machine each refresh without any change to the emulator core.

Sound

WebMSX already emulates and mixes its own sound — the PSG (AY-3-8910), the optional SCC and MSX-MUSIC (OPLL) all feed a wmsx.WebAudioSpeaker that owns a ScriptProcessor node wired to its own AudioContext. So this is a "native-audio" integration: we do NOT route samples through the shared EmuAudio sink (that would double or phase-corrupt the sound). We simply start it silent and give the Sound button control of WebMSX's own pipeline.

The catch is that the AudioContext and its ScriptProcessor are closure-private inside the speaker. We capture them by briefly wrapping the global AudioContext constructor during boot — WebMSX builds its context with new window.AudioContext when it powers on, which is after our synchronous shim runs — and by wrapping the returned context's createScriptProcessor to grab the output node:

var AC = window.AudioContext || window.webkitAudioContext;
window.AudioContext = function Wrapped(){
  var ctx = new AC();             // WebMSX's own context
  _audioCtx = ctx;                    // keep the handle
  var csp = ctx.createScriptProcessor;
  ctx.createScriptProcessor = function(){ _spNode = csp.apply(ctx, arguments); return _spNode; };
  return ctx;
};

Once the machine is up we grab room.speaker (the WebAudioSpeaker) and wire the standard transport contract to drive WebMSX's OWN path. Muting sets the speaker's mute flag (it then mixes silence) and suspends the context; unmuting resumes the context and clears the flag:

var speaker = room.speaker;
function setMute(m){
  if (m) { speaker.mute();   _audioCtx.suspend(); }
  else  { _audioCtx.resume(); speaker.unMute(); }
}

It starts muted (setMute(true) at boot, and browsers keep the context suspended until a gesture anyway), so nothing plays until the Sound button's real click resumes the context. transport.isMuted / transport.setMute are what the shell's Sound button toggles.