PCjs
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.
Runs on: Web browser
PCjs Online Emulator
Play PCjs using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| IBM PC 5150 — Cassette BASIC | PCjs | IBM PC 5150 | grey | Open ⛶ | |
| IBM PC XT — Windows 1.01 | PCjs | IBM PC/XT | Windows 1.x | grey | Open ⛶ |
| IBM PC AT — Windows 3.0 | PCjs | IBM PC/AT | Windows 3.x | grey | Open ⛶ |
| IBM PC AT — Windows 3.1 | PCjs | IBM PC/AT | Windows 3.x | grey | Open ⛶ |
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:
| Member | Kind | What it does |
|---|---|---|
cpu.stepCPU(0) | method | Execute exactly one instruction (a 0 cycle-minimum means single-step; breakpoints and interrupts are suppressed). The single-step primitive. |
cpu.startCPU() / cpu.stopCPU() | methods | Start / stop PCjs's own run loop — used for full-speed run and to pause. |
cpu.regEAX … cpu.regEDI | fields | The general registers (low word = AX…DI); read and written live. |
cpu.getCS/DS/SS/ES(), cpu.getIP(), cpu.getSP(), cpu.getPS() | methods | Segment registers, IP, SP and the FLAGS word (with matching setters). |
cpu.regLIP | field | The linear instruction pointer = (CS<<4)+IP in real mode — the value used for the program counter and execution breakpoints. |
bus.getByteDirect(a) | method | Read a physical byte with no side effects — what the hex and disassembly views use. |
bus.setByte(a, v) | method | Write a physical byte — poke memory live. |
kbd.injectKeys(s) | method | Feed 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)(notgetByte), 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.regLIPis 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 whencpu.regLIPmatches — 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/setByteread 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;Keyboardmaps 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 (
Videox86inmodel="ega", its own video BIOS ROM at0xC0000), the Xebec fixed-disk ROM, and a 10 MB hard disk (HDCtypeXT) 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
Busx86now 24-bit), EGA or VGA video, and a 20 MB hard disk (HDCtypeAT, 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.