SearchA-ZV › VIC-20 Emulator (mwenge)

VIC-20 Emulator (mwenge)

2013 Unlicensed Online

A pure-JavaScript Commodore VIC-20 emulator (mwenge's docs/vic20 core, a Willem-Dawson-style 6502 machine). It emulates the 6502 CPU, the VIC-6560/6561 video-and-sound chip and the two 6522 VIAs, boots straight to Commodore BASIC 2.0, and takes keyboard input. On emulators.org it is wired into the shared in-browser debugger, so you can single-step the 6502, set breakpoints and inspect memory.

View the source on GitHub ↗

Visit the official site ↗

Runs on: Web browser

VIC-20 Emulator (mwenge) Online Emulator

Play VIC-20 Emulator (mwenge) using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
VIC-20 BASICVIC-20 Emulator (mwenge)Commodore VIC-20greyOpen ⛶

Machines emulated

Chips

Notes

Embedding

mwenge's VIC-20 is a set of plain-global JavaScript modules (no bundler). Vendor them and load them in dependency order, then drive the machine yourself instead of calling vic20.execute() — its internal setTimeout loop cannot be paused or stepped from outside, which the debugger needs.

Boot. Construct the machine (it installs the CHARGEN / BASIC / KERNAL ROMs into mem and points the VIC-6560 at the <canvas id="canvas">), then run your own loop built on vic20.cycle() (one machine clock: steps both VIAs, the CPU and the VIC):

window.vic20 = new Vic20();   // must be the global 'vic20' (chip callbacks read it)
vic20.isPal = false; kerneldata = kerneldataNtsc;
vic20.init();                    // builds CPU / VIAs / VIC, installs ROMs, resets
var CYCLES = 16965;             // ~1.02 MHz / 60 Hz
(function loop(){
  for (var i = 0; i < CYCLES; i++) vic20.cycle();
  requestAnimationFrame(loop);
})();

The machine is plain objects. Everything the debugger needs is a live global — no wasm heap to reach into:

MemberKindWhat it does
vic20.cycle()methodRun one machine clock (VIAs + one CPU sub-cycle + VIC). The stepping primitive.
vic20.cpu.isNextInst()methodTrue when the next cycle() will fetch a fresh opcode — the instruction boundary used for single-step and PC breakpoints.
vic20.cpu.getPc()methodLive program counter (checked against the breakpoint set at each boundary).
vic20.cpu.getRegisters()methodLive registers {pc,sp,a,x,y,p}.
vic20.cpu.setA/setX/setY(hex)methodWrite A/X/Y (upstream, hex-string arg). We added numeric setPc/setSp/setP so the debugger can write PC/SP/P too.
vic20.invisibleRead(a) / invisibleWrite(a,v)methodSide-effect-free bus access — reads VIC/VIA without clearing latches, for a live hex view.
memfieldThe raw 64 KB memory array, under the ROM/I/O routing.
keysdownfieldThe 8-byte VIC-20 keyboard-matrix state the VIA scans; press = set a row,col bit.

Because the CPU, bus and RAM are ordinary JavaScript, the debugger single-steps by running cycle() to the next instruction boundary, reads and writes registers straight through the CPU's get/set methods, and implements breakpoints and watchpoints as host-side checks around the loop — the only change to the core is a three-line numeric setPc/setSp/setP patch.

Debugger integration

The plug-in (mwenge-vic20-debug.js) hands the shared framework the machine surface and nothing else:

  • Registers come from vic20.cpu.getRegisters() each refresh and are written back with setA/setX/setY (hex) and the added numeric setPc/setSp/setP; the status flags toggle bits of P.
  • Memory is exposed as five chips: the side-effect-free CPU bus (invisibleRead/invisibleWrite), the raw 64 KB RAM, and the BASIC / KERNAL / Character ROM windows.
  • Single step runs cycle() until cpu.isNextInst(), i.e. exactly one instruction.
  • Breakpoints and watchpoints are host-side: the boot loop checks the PC against a set at each instruction boundary, and wraps vic20.write to catch a watched address.

Architecture

The VIC-20 (1981) was Commodore's first colour home computer: a 6502, the VIC-6560/6561 video-and-sound chip, two 6522 VIAs for I/O, and Commodore BASIC 2.0 in ROM. mwenge's emulation keeps each as its own object, clocked in lockstep one cycle at a time:

  • Cpu6502 — the 6502 core, driven a sub-cycle at a time via a micro-op table; cycle() advances it.
  • Vic6560 — the VIC video/sound chip, rastered into the canvas (text and the standard multicolour modes).
  • Via6522 ×2 — the timers, keyboard-matrix scan and joystick/tape lines.
  • mem — the flat 64 KB array; vic20.read/write route reads and writes to RAM, ROM or the VIC/VIA registers.
  • The CHARGEN, BASIC and KERNAL ROMs are embedded (packed) in carts.js and copied into mem on reset.

It boots straight to **** CBM BASIC V2 **** — a complete, inspectable machine with nothing to load.

Sound

The VIC-6560 already emulates sound: Vic6560.genAudio() mixes its three tone voices plus the noise channel every sixteenth machine cycle and resamples the result into a mono float ring buffer. The vendored core drained that buffer through its own AudioContext and a ScriptProcessorNode. We reroute it to the site's shared sink instead.

At construction, when window.EmuAudio is present the VIC sets its resample target to the sink's rate (sampleRate = EmuAudio.sampleRate, so the pitch is exact) and skips creating any AudioContext — no second audio graph runs. A new pumpAudio() is called once per video frame from the boot loop: it drains exactly Math.round(EmuAudio.sampleRate / 60) samples for that frame, runs them through a one-pole DC blocker (R = 0.995) to strip the VIC's positive-only bias, duplicates each mono sample to left and right as Int16, and pushes the interleaved frame to EmuAudio:

// once per video frame, from the run loop
var n = Math.round(EmuAudio.sampleRate / 60);
var out = new Int16Array(n * 2);
for (var i = 0; i < n; i++) {
  var y = sample - dcPrevIn + 0.995 * dcPrevOut;   // DC blocker
  dcPrevIn = sample; dcPrevOut = y;
  var s = clamp16(y * 32767);
  out[i * 2] = out[i * 2 + 1] = s;              // mono -> L, R
}
EmuAudio.push(out);

The transport's setMute / isMuted delegate straight to EmuAudio, so the shell's Sound button controls it. Audio starts muted, as browsers require, and unmutes from that first real click.