apple2js
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.
Runs on: Web browser
apple2js Online Emulator
Play apple2js using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Apple II · Applesoft BASIC | apple2js | Apple II | grey | Open ⛶ | |
| Apple II · Integer BASIC | apple2js | Apple II | grey | Open ⛶ |
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:
| Member | Kind | What it does |
|---|---|---|
cpu.step() | method | Execute exactly one 6502 instruction. The single-step primitive. |
cpu.stepCycles(n) | method | Run for n clocks - the fast run-at-speed path. |
cpu.getState() / setState(s) | methods | Read / write all registers at once: {a,x,y,s,pc,sp} (s = the status byte). |
cpu.getPC() | method | The live program counter, what the breakpoint check and follow-PC read. |
cpu.read(addr) / write(addr,v) | methods | Banked CPU-bus read / write; write pokes memory live. |
cpu.reset() | method | Reset the 6502 (reloads PC from the $FFFC vector). |
io.keyDown(ascii) / io.keyUp() | methods | Feed the Apple II keyboard latch a 7-bit ASCII code. |
io.blit() / vm.blit() | methods | Render 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–$C0FFsoft-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 ofstate.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 withaddPageHandler.Apple2IO- the$C0xxI/O space: keyboard latch, speaker, annunciators, game paddles and the peripheral-card slots.VideoModes(2D-canvas or WebGL) withLoresPage/HiresPage- the text, lo-res and hi-res display, rastered to a 560×384 canvas.RAMandROMpage handlers - 48K of main RAM plus the system ROM; aLanguageCardadds 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.