SearchA-ZP › PCjs

PCjs

2012 Open source · MIT Online

PCjs is a JavaScript emulation project that runs vintage computers in the browser, including the IBM PC 5150, PC/XT 5160, PC/AT 5170 and PCjr. It boots period software such as PC DOS, early Microsoft Windows and OS/2, and is known for its accurate, well-documented machine configurations.

Visit the official site ↗

Runs on: Web browser

PCjs Online Emulator

Play PCjs using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
IBM PC 5150 — Cassette BASICPCjsIBM PC 5150greyOpen ⛶
IBM PC XT — Windows 1.01PCjsIBM PC/XTWindows 1.xgreyOpen ⛶
IBM PC AT — Windows 3.0PCjsIBM PC/ATWindows 3.xgreyOpen ⛶
IBM PC AT — Windows 3.1PCjsIBM PC/ATWindows 3.xgreyOpen ⛶

Machines emulated

Operating systems

Chips

Notes

Embedding

PCjs is a full IBM PC framework, not a drop-in emulator object. We vendor the uncompiled pcx86 machine bundle (the compiled build renames every internal method, so the debugger could not reach the CPU) and a single self-written machine.xml describing one Model 5150: an 8088, 64 KB RAM, a CGA card, the IBM PC BIOS at 0xFE000 and IBM Cassette BASIC at 0xF6000. The DIP switches say "No IPL floppy", so after POST the BIOS calls INT 18h and drops into ROM BASIC.

Boot. We let PCjs build and run the machine with its own embed call, then reach into the live component registry it publishes on window.PCjs.components:

embedPCx86("ibm5150", "machine.xml", "components.xsl");   // PCjs builds + auto-runs the machine
var C = window.PCjs.components;
var cpu = C.find(c => c.stepCPU && c.getIP);        // the CPUx86 instance
var bus = C.find(c => c.type === "Busx86");         // the 1 MB physical bus
var vid = C.find(c => c.type === "Video");           // vid.canvasScreen is the <canvas>

Everything the debugger needs is a live property on those objects — PCjs is JavaScript all the way down:

MemberKindWhat it does
cpu.stepCPU(0)methodExecute exactly one instruction (a 0 cycle-minimum means single-step; breakpoints and interrupts are suppressed). The single-step primitive.
cpu.startCPU() / cpu.stopCPU()methodsStart / stop PCjs's own run loop — used for full-speed run and to pause.
cpu.regEAX … cpu.regEDIfieldsThe general registers (low word = AX…DI); read and written live.
cpu.getCS/DS/SS/ES(), cpu.getIP(), cpu.getSP(), cpu.getPS()methodsSegment registers, IP, SP and the FLAGS word (with matching setters).
cpu.regLIPfieldThe linear instruction pointer = (CS<<4)+IP in real mode — the value used for the program counter and execution breakpoints.
bus.getByteDirect(a)methodRead a physical byte with no side effects — what the hex and disassembly views use.
bus.setByte(a, v)methodWrite a physical byte — poke memory live.
kbd.injectKeys(s)methodFeed characters to the emulated keyboard; drives the on-screen keys.

Because the CPU, bus and video are ordinary objects, the debugger single-steps with stepCPU(0), reads and writes the registers directly, disassembles from getByteDirect, and implements breakpoints as host-side PC checks around those calls — with no changes to PCjs itself.

One shim, several machines. The same boot shim builds whichever IBM PC a config selects — it just passes that config's machineId + machineXml to embedPCx86. The default is the licence-clean 5150 Cassette BASIC; the Windows configs point at a self-contained 5160 (XT) or 5170 (AT) XML whose <hdc> carries a pre-installed PC DOS + Windows hard-disk image, and set an autorun string that is typed once DOS reaches the prompt:

embedPCx86(cfg.machineId, BASE + cfg.machineXml, BASE + "...components.xsl");
// after the machine builds + PC DOS boots off C:, type the autorun line to start Windows
if (cfg.autorun) setTimeout(() => kbd.injectKeys(cfg.autorun), cfg.autorunMs);   // e.g. "win" + Enter

Debugger integration

Wiring PCjs into the shared debugger needed two pieces of custom work, because PCjs is a self-contained framework that runs its own show.

1 · Vendor the uncompiled bundle. The compiled pcx86.js is run through Closure Compiler, which renames stepCPU, getIP, regEAX and friends to one-letter names — so the debugger cannot reach them. We ship pcx86-uncompiled.js instead, which keeps every original name.

2 · A boot shim that owns the run policy. PCjs auto-runs the CPU through its own requestAnimationFrame loop, which cannot be stopped per-instruction — which the debugger needs for breakpoints. So the transport switches strategy based on state:

function resume(){
  if (bps.size || wps.size) steppedLoop();   // our loop: stepCPU(0) + check PC each instruction
  else cpu.startCPU();                     // no breakpoints: PCjs's own loop, full speed
}
function stepInsn(n){ cpu.stopCPU(); while (n-->0) cpu.stepCPU(0); cpu.updateCPU(true); }

Techniques for deeper access. The plug-in reaches the running machine through PCjs's own surfaces, no fork:

  • Side-effect-free reads. The hex and disassembly views scrub memory with bus.getByteDirect(addr) (not getByte), so auto-polling the view never trips a memory-mapped I/O side effect.
  • Direct state. Registers are read and written straight off the CPUx86 object; cpu.regLIP is the linear PC used for follow-PC and breakpoints.
  • Execution breakpoints. With any breakpoint set, the transport runs its own loop of stepCPU(0) and halts when cpu.regLIP matches — while an empty set runs PCjs at full native speed.
  • Write watchpoints. Best-effort: in stepped mode the loop snapshots each watched byte and pauses when one changes. (PCjs writes memory through per-block handlers rather than the bus, so watchpoints are only active while a breakpoint or watchpoint is set and the stepped loop is running.)
  • Single instruction step. cpu.stepCPU(0) runs exactly one instruction; a positive cycle count runs a timed burst (used for frame-step).

Registers, memory hex/disassembly, follow-PC, single-step and breakpoints are all built from these. The one concession to PCjs owning its loop is that watchpoints are stepped-mode only.

Architecture

PCjs is a faithful, component-based IBM PC. Each chip is a JavaScript class registered in a global component list; a machine is assembled from an XML description transformed to component <div>s by an XSLT stylesheet, then powered up together:

  • CPUx86 — the 8088 core. stepCPU() runs one instruction or a burst; the full register file (regEAX…regEDI, the segment objects, regLIP, regPS) is plain fields and accessor methods.
  • Busx86 — the 20-bit (1 MB) physical address space, split into memory blocks; getByteDirect/setByte read and write it.
  • ROMx86 ×2 — the IBM PC BIOS (0xFE000) and IBM Cassette BASIC (0xF6000), loaded from JSON byte arrays.
  • Videox86 — the CGA card, rastered into a <canvas> using the CGA font ROM.
  • ChipSet — the 8259 PIC, 8253 PIT, 8255 PPI (the DIP switches) and speaker; Keyboard maps the browser keyboard to the 83-key matrix.

With no diskette and the switches set to "No IPL", the BIOS boot routine falls through to INT 18h, which enters the built-in ROM BASIC — the classic cassette-BASIC screen, and a clean licence-safe default that needs no PC-DOS.

The Windows machines. The extra configs reuse the very same component classes, wired up by a per-config machine XML into a bigger PC that boots a real operating system from a hard disk:

  • Windows 1.01 — an IBM PC XT (5160): the 8088 with 640 KB, an IBM EGA card (Videox86 in model="ega", its own video BIOS ROM at 0xC0000), the Xebec fixed-disk ROM, and a 10 MB hard disk (HDC type XT) holding PC DOS 2.00 + Windows 1.01.
  • Windows 3.0 / 3.1 — an IBM PC AT (5170): an 80286 with 1–2 MB (the Busx86 now 24-bit), EGA or VGA video, and a 20 MB hard disk (HDC type AT, whose controller is in the AT ROM BIOS) holding PC DOS + Windows 3.x. The 286 runs Windows in protected mode.

Every machine XML is self-contained — it inlines the video adapter and its ROM and the hard-disk controller, with no ref= includes and no floppy library — so the full dependency tree (BIOS, video ROM, disk image) is vendored on disk. The debugger attaches to all of them identically, because they are the same CPUx86 / Busx86 / Video classes.