SearchA-ZC › c64js

c64js

2016 Open source · MIT Online

c64js is a Commodore 64 emulator written entirely in JavaScript by Mikael Borgbrant. It runs in-browser, rendering the VIC-II picture to an HTML5 canvas, and bundles the KERNAL, BASIC and character ROMs so it boots straight to the C64 BASIC prompt with no downloads. Its 6510 CPU, memory banking and RAM are plain JavaScript objects, so the whole machine can be stepped and inspected live.

Visit the project on GitHub ↗

Visit the official site ↗

Runs on: Web browser

c64js Online Emulator

Play c64js using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
C64 BASICc64jsCommodore 64 (breadbin)openOpen ⛶
Hello World democ64jsCommodore 64 (breadbin)openOpen ⛶

Machines emulated

Chips

Notes

Embedding

c64js is a set of plain-global JavaScript modules (no bundler). Vendor src/js/** and load them in dependency order, then drive the machine yourself instead of calling main.start() — its internal requestAnimationFrame loop cannot be paused or stepped from outside, which the debugger needs.

Boot. Install the three bundled ROMs, initialise the bus, CPU and VIC-II, point the VIC-II at a <canvas>, then run your own loop built on mos6510.process() (one 6510 instruction, returns its cycle count):

memoryManager.kernel    = new Rom(0xe000, romDump.kernel);
memoryManager.basic     = new Rom(0xa000, romDump.basic);
memoryManager.character = new Rom(0x0000, romDump.character);
cpuMemoryManager.init();
mos6510.init(cpuMemoryManager);
vic2.setScreenCanvas(canvas);
vic2.init(vicMemoryManager);
(function frame(){                          // ~19656 VIC sub-cycles per frame
  for (var i = 0; i < 19656; i++) {
    if (cpuCycles === 0) cpuCycles = mos6510.process();  // one instruction
    cpuCycles--; vic2.process(i, 0);
  }
  requestAnimationFrame(frame);
})();

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

MemberKindWhat it does
mos6510.process()methodExecute exactly one 6510 instruction; returns its cycle count. The single-step primitive.
mos6510.reset()methodReset the CPU (reloads PC from the $FFFC vector).
mos6510.registerfieldLive registers: .a .x .y .pc .sp and .status (a flags object with .getStatusByte() / .setStatusFlags(b)).
cpuMemoryManager.readByte(a)methodRead the banked CPU bus (sees KERNAL / BASIC / CHARGEN / I/O per memoryMode).
cpuMemoryManager.writeByte(a, v)methodWrite the CPU bus — poke memory live for cheats or a debugger.
memoryManager.ram.memoryfieldThe raw 64 KB RAM array, under the ROM/I/O banks.
vic2fieldThe VIC-II; setScreenCanvas + process(cycle, toggle) raster the picture into the canvas.
fileLoad.writeAutoRun()methodPrime the keyboard buffer with Shift+RUN/STOP; a KERNAL LOAD trap then injects a queued .prg and runs it.

Because the CPU, bus and RAM are ordinary JavaScript, the debugger single-steps with mos6510.process(), reads and writes registers straight off mos6510.register, and implements breakpoints and watchpoints as host-side checks around those calls — no changes to the emulator core.

Architecture

c64js is a readable, interpreted C64. Each chip is its own global object; the CPU steps one instruction at a time and the VIC-II is clocked in lockstep at sub-cycle granularity:

  • mos6510 — the 6510 CPU interpreter (a 6502 core plus the two on-chip I/O port lines that bank the memory map). process() runs one instruction.
  • cpuMemoryManager — the PLA banking logic: which of RAM, BASIC, KERNAL, CHARGEN or I/O answers each address, driven by memoryMode.
  • vic-ii-6569 — the VIC-II video chip, rastered into the canvas (text and standard modes; sprites and some bitmap modes are incomplete).
  • cia-6526 ×2, keyboard — timers, the keyboard matrix and joystick ports.
  • sid (jsSID) — the 6581 sound chip; optional, and silent without a user gesture.
  • romDump.basic / .kernel / .character — the three system ROMs, embedded as byte arrays, installed as Rom banks.

The emulator is a proof-of-concept graphically, but its CPU and memory are faithful and, crucially, fully exposed — which is exactly what makes it a good debugging target.

Sound

Sound is a Pattern V-stub integration: the SID chip is already emulated (jsSID), so no new chip was written — only its output was rewired to the shared sink. The running 6510 already pokes the SID: a CPU write to $D400–$D7FF flows through cpuMemoryManager.writeByte to io.js, which calls sidPlayer.synth.poke(reg, val). That write path is untouched, so the debugger's side-effect-free bus read — which deliberately skips the $D000–$DFFF I/O window — is unaffected; the audio path samples the synth object directly, never through a bus read.

The one change is the output side. The core's own jsSID/pico WebAudio node is not started (two consumers pulling from one synth would corrupt its oscillator phase). Instead the synth is created at EmuAudio.sampleRate so it plays at the correct pitch with no resampling, and each video frame the boot loop pulls exactly Math.round(EmuAudio.sampleRate/60) mono samples, clamps and scales them to Int16, duplicates each to L and R, and pushes one interleaved-stereo frame to the shared EmuAudio sink:

// sid-mos6581.js — pull one video frame of audio from the live SID
sid.generateFrame = function (count) {
  var out = new Int16Array(count * 2);
  sidPlayer.synth.generateIntoBuffer(count, mono, 0);  // mono floats ~[-1,1]
  for (var i = 0; i < count; i++) {
    var s = (clamp(mono[i]) * 32767) | 0;
    out[i * 2] = s; out[i * 2 + 1] = s;               // duplicate mono to stereo
  }
  return out;
};
// embed.js boot loop, once per video frame:
EmuAudio.push(sid.generateFrame(Math.round(EmuAudio.sampleRate / 60)));

The Sound button works through transport.setMute / transport.isMuted, which delegate to EmuAudio. Audio starts muted (browsers block sound before a user gesture) and unmutes on a real click.

The machine boots to the READY prompt and stays there. Sound plays whenever the software drives the chip: POKE the SID registers, or run any program that does, and it comes straight out of the shared sink. For example, this sets voice 1 to full volume and holds a ~257 Hz triangle tone:

POKE54296,15:POKE54277,0:POKE54278,240
POKE54273,17:POKE54272,37:POKE54276,17