SearchA-ZI › Infinite Mac

Infinite Mac

2022 Open source · Apache-2.0 Online

Infinite Mac bundles emulators (Mini vMac, Basilisk II, SheepShaver and Previous) compiled to WebAssembly with a curated library of system versions, so any release from System 1 to Mac OS 9, and NeXTSTEP, boots in a browser tab, with drag-and-drop file transfer.

Visit the official site ↗

Runs on: Web browser

Infinite Mac Online Emulator

Play Infinite Mac using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Macintosh Plus (System 6)Infinite MacMacintosh PlusApple Classic Mac OSgreyOpen ⛶

Machines emulated

Operating systems

Chips

Notes

Embedding

infinite-mac cross-compiles Mini vMac (and Basilisk II, DingusPPC, …) to WebAssembly. For a 68k Macintosh the smallest, most self-contained core is Mini vMac; here it is built as a Macintosh Plus (Motorola 68000, 512×342 1-bit screen). We self-host everything - the rebuilt minivmac-Plus.js/.wasm, a grey Apple Mac Plus ROM and a System 6 boot disk - and drive the module ourselves from a <canvas>:

// the Emscripten module is a global factory (MODULARIZE, no ES6 export)
var Module = await emulator({ locateFile: p => SRC + p });   // finds minivmac-Plus.wasm
Module.FS.writeFile("/rom", macPlusRomBytes);                 // 128 KiB Mac Plus ROM
Module.FS.writeFile("/prefs", "rom /rom\ndisk /disk1\nscreen win/512/342\n");
Module._emudbg_boot();                                    // InitOSGLU + InitEmulation, no blocking loop
requestAnimationFrame(function loop(){ Module._emudbg_run_frame(); requestAnimationFrame(loop); });

A plain-JS workerApi, no worker, no SharedArrayBuffer. The Emscripten glue talks to its host through a global workerApi (screen blit, input, disk I/O). Infinite-mac normally provides that from a Web Worker over shared memory; we provide an ordinary main-thread object instead:

MemberKindWhat it does
workerApi.blit(ptr,len)callbackThe core converts the 1-bit Mac framebuffer to RGBA and calls this; we copy those bytes from Module.HEAPU8 into an ImageData and putImageData onto the canvas.
workerApi.getInputValue(addr)callbackPolled once per tick for mouse position/button and keyboard events; we return them from a small queue fed by DOM pointer/key handlers (Mac key codes via infinite-mac's own JS→ADB table).
workerApi.disks.read(id,ptr,s,n)callbackThe ROM's disk driver reads the boot image through here; we HEAPU8.set the requested slice of the in-memory .dsk. No Emscripten FS for disks.
Module._emudbg_run_frame()exportRuns one 60 Hz tick (input poll + CPU + devices), paced by requestAnimationFrame - the emulator's own MainEventLoop refactored so it never blocks.

Because the whole machine runs on the main thread, the debugger reads the 68000 state synchronously through the hooks below, the same immediacy the pure-JavaScript cores give, from a WebAssembly core.

Debugger integration

This is a Tier-4 integration: the CPU state lives inside a compiled wasm module, so we rebuilt Mini vMac from source with a handful of state-export functions and reused the shared 68000 decoder. Two problems had to be solved: reaching the register file, and running the core somewhere the debugger can read it synchronously.

1 · On-demand state-export hooks (the only source change). Mini vMac keeps the 68000 in one file-local struct regs in MINEM68K.c (regs.regs[16] = D0-D7 then A0-A7, regs.pc, and the flag fields). We appended functions that read/advance it only WHEN CALLED, never inside the instruction loop, honouring the golden rule:

// MINEM68K.c, reads the live 68000 register file on demand
EMSCRIPTEN_KEEPALIVE unsigned int emudbg_reg(int i){        // 0-7 D, 8-15 A, 16 PC, 17 SR
    if (i < 16) return V_regs.regs[i];
    if (i == 16) return m68k_getpc();
    if (i == 17) return m68k_getSR();       // resolves the lazy condition flags
    return 0;
}
EMSCRIPTEN_KEEPALIVE void emudbg_set_reg(int i, unsigned int v);  // poke a register
EMSCRIPTEN_KEEPALIVE unsigned int emudbg_read(unsigned int a);     // one bus byte, via get_vm_byte
EMSCRIPTEN_KEEPALIVE void emudbg_read_block(unsigned int a, unsigned char* out, int len);
EMSCRIPTEN_KEEPALIVE void emudbg_step_insn(void){ V_regs.ResidualCycles = 0; m68k_go_nCycles(1); } // ~1 instruction

2 · Run it on the main thread, one tick at a time. Infinite-mac's MainEventLoop() is a blocking for(;;) that only works inside a Web Worker (it sleeps/blocks on SharedArrayBuffer between ticks). A blocked worker cannot answer the debugger's synchronous register reads, and this site is not cross-origin isolated, so we refactored the loop into two callable exports that run on the page's own thread:

// OSGLUESC.c / PROGMAIN.c - the non-blocking loop, split for JS to drive
EMSCRIPTEN_KEEPALIVE int  emudbg_boot(void);       // ZapOSGLUVars + InitOSGLU + InitEmulation
EMSCRIPTEN_KEEPALIVE void emudbg_run_frame(void);  // one MainEventLoop iteration, no sleep
EMSCRIPTEN_KEEPALIVE void emudbg_step_tick(void);  // DoEmulateOneTick, unconditional (frame-step)

The boot shim publishes these on window.EMU_BOOT.transport; <slug>-debug.js then calls EmuKit.defineMachine with the 68000 register set (read live via emudbg_reg, written via emudbg_set_reg), memory chips (the 24-bit bus, RAM at $000000, ROM at $400000) read side-effect-free with device windows skipped, and the shared m68000 decoder for disassembly.

Play / pause / step, and the granularity. Pause simply stops calling emudbg_run_frame; resume restarts the rAF loop. Two step primitives, both bypassing the real-time pacing so they advance while paused: single-instruction step (emudbg_step_insnm68k_go_nCycles(1) after zeroing the leftover cycle debt, so exactly one 68000 instruction retires (a true instruction step, verified by the PC advancing one instruction at a time) and frame step (emudbg_step_tickDoEmulateOneTick, one 60 Hz tick of CPU + devices, useful for stepping past a STOP that waits on the vertical-blank interrupt). Breakpoints / watchpoints are surfaced in the UI but not enforced: Mini vMac has no cheap native breakpoint facility, and adding a per-instruction PC check to the dispatch loop would violate the golden performance rule, so they are kept as plain sets rather than pretended exact.

What is real vs. approximate. Registers (D0-D7, A0-A7, PC, SR with X N Z V C S), memory read/write across the whole 24-bit bus, disassembly, pause/resume, single-instruction step and frame step are all live and accurate against the real 68000 state. The condition flags come from m68k_getSR, which resolves Mini vMac's lazy-flag state, so they are exact when sampled. Breakpoints/watchpoints are the only non-enforced surface.

Architecture

Mini vMac is a compact, faithful Macintosh emulator; infinite-mac cross-compiles it to WebAssembly with a custom "esc" OS-glue that renders to a canvas and streams disks from JavaScript. The build here is the Macintosh Plus variant.

  • MINEM68K.c - the Motorola 68000 interpreter and its register file regs (the debug hooks read D0-D7/A0-A7/PC and resolve the status register here).
  • GLOBGLUE.c - the 24-bit address bus: RAM at $000000, the 128 KiB ROM at $400000, and the VIA / SCC / IWM device windows (which the debugger's reads skip so they never latch).
  • VIAEMDEV.c, IWMEMDEV.c, SCCEMDEV.c, SCRNEMDV.c, SONYEMDV.c - the 6522 VIA, the IWM floppy controller, the SCC serial, the 512×342 video, and the Sony disk driver that reads the boot image through workerApi.disks.
  • OSGLUESC.c / PROGMAIN.c - the Emscripten OS glue and the main loop; refactored here into emudbg_boot + emudbg_run_frame so the machine runs on the browser's main thread under our own requestAnimationFrame instead of a blocking worker loop.

Boot media is a grey Apple Mac Plus ROM (128 KiB, checksum unverified by Mini vMac) and a System 6 disk image; both are Apple copyright, self-hosted here and removed on request.