SearchA-ZA › apple2js

apple2js

2013 Open source · MIT Online

apple2js is an Apple II emulator written originally in JavaScript and now in TypeScript by Will Scullin. It emulates the Apple ][, ][+ and //e in a web browser with color display, sound and disk image support.

Visit the official site ↗

Runs on: Web browser

apple2js Online Emulator

Play apple2js using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Apple II · Applesoft BASICapple2jsApple IIgreyOpen ⛶
Apple II · Integer BASICapple2jsApple IIgreyOpen ⛶

Machines emulated

Chips

Notes

Embedding

apple2js is a TypeScript project, not a single script. We bundle a small boot entry with webpack into apple2js-boot.js, load it as one plain <script>, and drive the machine ourselves rather than using apple2js's own React UI — so the loop can be paused and single-stepped, which the debugger needs.

Boot. We construct the core Apple2 class (not its UI), point it at a <canvas>, reset the CPU, then run a loop on cpu.stepCycles():

import { Apple2 } from "./apple2";
const apple2 = new Apple2({
  canvas: document.querySelector("#screen"),
  rom: "fpbasic", characterRom: "apple2_char",   // Applesoft + character ROM
  e: false, enhanced: false, gl: false, tick: ()=>{}
});
await apple2.ready;
const cpu = apple2.getCPU(), io = apple2.getIO(), vm = apple2.getVideoModes();
cpu.reset();                                // reset vector → Autostart ROM → BASIC
(function frame(){
  cpu.stepCycles(io.getKHz() * 16);        // ~one video frame of 6502 time
  io.tick(); vm.blit();
  requestAnimationFrame(frame);
})();

The whole machine is plain objects. apple2js exposes everything the debugger needs as ordinary methods — no wasm heap to reach into:

MemberKindWhat it does
cpu.step()methodExecute exactly one 6502 instruction. The single-step primitive.
cpu.stepCycles(n)methodRun for n clocks — the fast run-at-speed path.
cpu.getState() / setState(s)methodsRead / write all registers at once: {a,x,y,s,pc,sp} (s = the status byte).
cpu.getPC()methodThe live program counter — what the breakpoint check and follow-PC read.
cpu.read(addr) / write(addr,v)methodsBanked CPU-bus read / write; write pokes memory live.
cpu.reset()methodReset the 6502 (reloads PC from the $FFFC vector).
io.keyDown(ascii) / io.keyUp()methodsFeed the Apple II keyboard latch a 7-bit ASCII code.
io.blit() / vm.blit()methodsRender the current video frame into the canvas.

Because the CPU, bus and I/O are ordinary JavaScript objects, the debugger single-steps with cpu.step(), reads and writes registers straight through getState/setState, disassembles from cpu.read, and implements breakpoints and watchpoints as host-side checks around those calls — with no changes to the emulator core.

Debugger integration

Wiring apple2js into the shared in-browser debugger needed two pieces of custom work.

1 · A boot shim, because apple2js has no drop-in build. Its normal entry builds a full React/DOM UI and owns an internal requestAnimationFrame loop that cannot be paused or single-stepped from outside — which the debugger needs. So we wrote a small boot entry that constructs just the core Apple2 machine and drives it from a loop we control. When no breakpoints are armed it runs a whole frame fast with cpu.stepCycles(); when a breakpoint or watchpoint is set it steps one instruction at a time:

function runCycles(target){
  if (!bps.size && !wps.size){ cpu.stepCycles(target); return false; }
  for (let done=0; done<target; ){
    if (bps.has(cpu.getPC())) return true;   // execution breakpoint
    const pre = watchSnapshot(), c0 = cpu.getCycles();
    cpu.step(); done += cpu.getCycles() - c0;
    if (watchChanged(pre)) return true;     // value-change watchpoint
  }
}

2 · Bundled to a single self-contained script. apple2js loads its ROMs with dynamic import(); we build with webpack's dynamicImportMode: 'eager' so the Applesoft, Integer BASIC and character ROMs are inlined into one apple2js-boot.js — no async chunks and no runtime fetches to self-host. The boot publishes window.EMU_BOOT when it is ready, so the debugger plug-in polls for it.

Techniques for deeper access. The plug-in reaches into the running machine through apple2js's own surfaces — no fork of the emulator core:

  • Side-effect-free reads. The hex and disassembly views read the bus with a wrapper that skips the $C000–$C0FF soft-switch window, so auto-polling the view can never flip a display page or clear the keyboard strobe.
  • Direct state. Registers are read and written through cpu.getState() / setState(); the status flags are the bits of state.s.
  • Single instruction step. cpu.step() runs exactly one instruction — the primitive under Step and under the breakpoint loop.
  • Breakpoints & watchpoints. Execution breakpoints compare cpu.getPC() before each instruction; memory watchpoints compare the watched addresses' values after each instruction — both host-side, so the emulator core is untouched.

Everything the debugger shows — registers read/write, memory hex/disassembly, follow-PC, single-step, breakpoints and watchpoints — is built from these.

Architecture

apple2js is a readable, object-oriented Apple II. Each block of the machine is its own class hanging off the core Apple2:

  • CPU6502 (from @whscullin/cpu6502) — the 6502 interpreter; it can also emulate a 65C02 for the //e. Memory is reached through page handlers registered with addPageHandler.
  • Apple2IO — the $C0xx I/O space: keyboard latch, speaker, annunciators, game paddles and the peripheral-card slots.
  • VideoModes (2D-canvas or WebGL) with LoresPage / HiresPage — the text, lo-res and hi-res display, rastered to a 560×384 canvas.
  • RAM and ROM page handlers — 48K of main RAM plus the system ROM; a LanguageCard adds the upper 16K.
  • Peripheral cards — Disk II, SmartPort, parallel printer, Videoterm 80-column, clock — each a slot handler.
  • ROMs — Applesoft (fpbasic) or Integer BASIC (intbasic) plus a character ROM, compiled in as byte arrays.

Each frame steps the CPU for a slice of 6502 time and blits the video. Because every component is an ordinary object with getState/setState, the whole machine is inspectable — which is what the debugger reads each refresh.

Sound

Pattern: re-enable the emulated sound chip (V-stub). apple2js already emulates the Apple II's sound hardware — there is no separate chip, just the machine's famous 1-bit speaker. Every CPU access to the $C030 soft switch flips the speaker cone; a program makes tones by toggling $C030 at the right rate (BASIC's bell, PEEK(-16336) loops, game sound routines). Apple2IO carries a complete sampler for this: a phase that inverts on each $C030 access, walked into output samples inside its _tick() and delivered through addSampleListener(). The problem was only that our headless boot never turned it on — it never called io.sampleRate() or io.addSampleListener() — so the speaker ran disabled and the machine was silent. We re-enable it at runtime and route its samples to the shared sink, without rebuilding the webpack bundle.

Pitch. Apple2IO derives cycles_per_sample = 1000 · kHz / rate, so the output rate must be the sink's rate or every tone comes out at the wrong pitch. We set it to the AudioContext rate up front:

const rate = EmuAudio.sampleRate;              // e.g. 44100
io.sampleRate(rate, Math.round(rate / 60));    // re-enable the speaker sampler at the sink rate
io.addSampleListener(buf => { /* queue the core's mono float samples */ });

Per-frame push. The boot owns the run-loop and calls io.tick() once per rendered frame (and once per single-step). We wrap io.tick() — no extra requestAnimationFrame, so audio stays locked to the video, to pause and to Step — and on each call drain exactly Math.round(EmuAudio.sampleRate / 60) samples from the queue, run them through a one-pole DC blocker (the speaker sample is a level, so a raw stream carries a resting DC step and a 60Hz frame tail; centring it makes a real wave rather than a lone click), scale to Int16, duplicate the mono speaker to left and right, and EmuAudio.push() that interleaved-stereo frame. On a brief underrun we hold the last level and let the DC blocker decay it out.

Mute contract. window.EMU_BOOT.transport exposes isMuted() / setMute(m), delegated straight to EmuAudio. It starts muted — browsers block audio before a user gesture — and the shell's Sound button unmutes on a real click, which resumes the sink's AudioContext.

Caveat. Because the sound is literally the 1-bit speaker, it is faithful but coarse by nature — clicks and buzzy square-ish tones, exactly as real Apple II software drove it; there is no PCM or music chip in the base machine (a Mockingboard AY card is not wired in this build). Silence produces no samples, so a booted-but-idle BASIC prompt is correctly quiet until something touches $C030.