SearchA-ZM › MartyPC

MartyPC

2023 Open source · MIT Online & downloadable

MartyPC by dbalsom (GloriousCow) is a cross-platform emulator focused on cycle-exact emulation of early 8088-based systems including the IBM PC 5150, PC/XT, PCjr and Tandy 1000. It faithfully runs demanding demos and games, emulates the NEC V20 and multiple video cards, and offers a web build alongside native binaries.

Visit the official site ↗

Runs on: Windows, macOS, Linux, Web browser

MartyPC Online Emulator

Play MartyPC using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
DOSMartyPCIBM PC 5150DOS (MS-DOS / DR-DOS)greyOpen ⛶

Machines emulated

Operating systems

Chips

Notes

Embedding

MartyPC is a cycle-accurate IBM PC/XT emulator written in Rust. Rather than embed its egui/wgpu web application (a self-contained GUI that draws its own menus and debugger windows into one canvas, and needs wasm threads + cross-origin isolation), we compiled just its core emulation library, marty_core, to WebAssembly behind a small hand-written #[wasm_bindgen] shim we call martypc_web. The shim constructs a Machine, runs it, and hands JavaScript the screen plus MartyPC's own debugger reads. We self-host everything (no CDN): martypc_web.js + martypc_web_bg.wasm, the freely-redistributable GLaBIOS 8 KB XT ROM, and MartyPC's own 360 KB DOS boot floppy.

// martypc_web is built with wasm-pack --target no-modules, so it defines a
// global wasm_bindgen(url) -> Promise<exports>. exports.memory is the linear
// memory (used to blit the framebuffer); wasm_bindgen.MartyPc is the class.
wasm_bindgen("/emulator/martypc/src/martypc_web_bg.wasm").then(async function (wasm) {
  var bios = new Uint8Array(await (await fetch(SRC + "glabios_xt.rom")).arrayBuffer());
  var emu = new wasm_bindgen.MartyPc(bios);          // builds the 5160 machine
  var fd = new Uint8Array(await (await fetch(SRC + "dosboot.img")).arrayBuffer());
  emu.insert_floppy(0, fd);                          // fluxfox parses the .img
  emu.resume();                                        // start running
});
Shim methodWhat it does
emu.run_frame(cycles)Run one frame of CPU cycles (~4.77 MHz / 60 fps) when running. Wraps Machine::run(cycles, Run).
emu.step_insn()Execute exactly ONE 8088 instruction. Wraps Machine::run(1, Step) - MartyPC's native single-instruction step.
emu.render() / emu.frame_ptr()Convert the CGA card's buffer to RGBA (via marty_videocard_renderer) and expose a pointer into wasm memory to blit onto the canvas.
emu.key_press(code, shift, ctrl, alt)Feed a JS KeyboardEvent.code string straight to Machine::key_press (MartyKey variants equal the web KeyCode names).

Debugger integration

This is a Tier-4 "WASM core, full debugger" integration. MartyPC keeps its architectural state private inside Rust structs, so, unlike v86, which hands JS typed-array views, we could not just read memory offsets. Instead we added a tiny export shim and rebuilt the core, turning MartyPC's OWN internal debugger API into JavaScript-callable getters. The golden rule holds: every getter only samples state when the debugger calls it (~10×/s and once per Step); nothing was added to the CPU's per-cycle or per-instruction hot path.

What the debugger needsHow the shim provides it (marty_core API)
AX BX CX DX SP BP SI DI, CS DS SS ES, IP, FLAGSemu.regs() reads the Cpu trait's get_register16() / get_flags(); writable via set_register16() / set_flags(). All 16-bit, the true 8088 file.
Linear program counter (disasm)emu.flat_ip() = Cpu::flat_ip() = (CS<<4)+IP.
Physical memory (1 MB)emu.peek(addr) = BusInterface::peek_u8() - a side-effect-free read that never touches an I/O device.
Single stepemu.step_insn() = Machine::run(1, ExecutionOperation::Step). This is MartyPC's native one-instruction step - a real per-instruction advance, not a time-slice burst.
Breakpoints / watchpointsemu.set_breakpoints(exec[], watch[]) installs BreakPointType::ExecuteFlat / MemAccessFlat - MartyPC's native breakpoints, checked inside its own run loop (no per-instruction JS hook). When one fires, Machine transitions to BreakpointHit and emu.state() reports it, so the loop stops.

What is real vs approximate. Registers, memory, disassembly, play/pause, single-step and breakpoints are all real and come from MartyPC's own debugger. This 8088 is genuinely, fully debuggable in the browser, including a true per-instruction step (something the v86 flagship could not offer). Watchpoints map onto native memory-access breakpoints. The one honest limitation is that write-back is offered for the general/segment registers and FLAGS but not for deep microarchitectural state (the prefetch queue, cycle state), which the core does not expose for external mutation.

Exactly what we changed in MartyPC's source. We added one new crate and changed no existing MartyPC file. The crate, martypc_web, is a cdylib that depends on marty_core, marty_common and marty_videocard_renderer by path and exposes a single #[wasm_bindgen] pub struct MartyPc. Its methods call only public marty_core API:

// MartyPc::new(bios)  - build the machine (no config files needed)
let machine = MachineBuilder::new()
    .with_core_config(Box::new(&WebCoreConfig as &dyn CoreConfig)) // tiny default-returning impl
    .with_machine_config(&ibm5160_config())  // hand-built: 640K, CGA, Model F kbd, NEC FDC
    .with_roms(manifest)                     // GlaBIOS mapped at 0xFE000
    .build()?;
// registers: Cpu trait (marty_core::cpu_common)
cpu.get_register16(Register16::AX) ... cpu.get_flags() ... cpu.flat_ip()
// memory (side-effect-free):  machine.bus().peek_u8(addr)
// one instruction:            machine.run(1, Step)     // vs run(N, Run) for a frame
// native breakpoints:          machine.set_breakpoints(vec![BreakPointType::ExecuteFlat(a)])
// video -> RGBA:                VideoRenderer::draw(card.buf(Front), &mut rgba, extents, None, palette)

Architecture

MartyPC is a full-system, cycle-accurate emulator of early 8088 PCs (IBM 5150 / 5160 / PCjr / Tandy 1000). The core we compiled, marty_core, models the 8088 CPU, the 8253 PIT, 8259 PIC, 8237 DMA, 8255 PPI, the NEC µPD765 floppy controller and a CGA card, everything needed to POST and boot DOS, with no frontend or GPU dependency.

  • martypc_web_bg.wasm - marty_core (+ marty_videocard_renderer) compiled to wasm32 with our shim; holds the entire machine state.
  • martypc_web.js - the wasm-bindgen glue (built --target no-modules): a global wasm_bindgen() loader and the MartyPc class.
  • glabios_xt.rom - GLaBIOS 0.2.6 (8XC), a freely-redistributable open-source IBM PC/XT BIOS, mapped at 0xFE000.
  • dosboot.img - MartyPC's own 360 KB DOS boot floppy (from martypc.net; MS-DOS-compatible, freely distributed) that boots to A:\> with a CGA welcome screen.

How to rebuild martypc_web to wasm from scratch (reproducible).

# toolchain: stable Rust + wasm-pack + wasm-bindgen already on PATH (~/.cargo/bin)
# 1. clone MartyPC (commit e15cb04, github.com/dbalsom/martypc)
git clone https://github.com/dbalsom/martypc.git
# 2. create a sibling cdylib crate 'martypc_web' next to the martypc checkout,
#    depending on marty_core (default-features=false, +ega +vga to match the
#    renderer), marty_common, marty_videocard_renderer, and display_backend_trait
#    with feature use_egui_backend (lightest; we never touch the backend).
# 3. feature-unification fixes for wasm32-unknown-unknown:
#      uuid = { version="1", features=["v4","js"] }        (transitive via marty_core)
#      getrandom  0.2 features=["js"]  AND  getrandom 0.3 features=["wasm_js"]
#      .cargo/config.toml: rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]
#    NOTE: build single-threaded, do NOT copy MartyPC's +atomics/build-std flags.
# 4. lib.rs = the #[wasm_bindgen] MartyPc struct above (regs/peek/step/bp/render).
# 5. build:
wasm-pack build --release --target no-modules --out-dir pkg
# artefacts: pkg/martypc_web.js + pkg/martypc_web_bg.wasm -> emulator/martypc/src/