SearchA-ZA › Apple IIgs (KEGS)

Apple IIgs (KEGS)

1996 Open source · GPL-3.0 Online

This is Kent Dickey's KEGS - a full-featured, cycle-accurate Apple IIgs emulator - rebuilt from its C source to WebAssembly so it runs entirely in the browser. It emulates the WDC 65C816 CPU, the Mega II and the Ensoniq sound chip, the Super Hi-Res display, and the 3.5"/5.25"/SmartPort drives. The build boots the Apple IIgs on its real ROM and a system disk.

Unlike a stock WebAssembly port, this build was rebuilt with a small sampling API over KEGS's own debugger, so the shared in-browser debugger gets genuine live 65C816 registers, side-effect-free memory, real single-instruction step, and real breakpoints and watchpoints - enforced by the emulator core, not faked.

Visit the official KEGS site ↗

Visit the official site ↗

Runs on: Web browser

Apple IIgs (KEGS) Online Emulator

Play Apple IIgs (KEGS) using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Apple IIgs · System Disk (ProDOS)Apple IIgs (KEGS)Apple IIGSgreyOpen ⛶
Apple IIgs · X-MAS Demo (Super Hi-Res)Apple IIgs (KEGS)Apple IIGSopenOpen ⛶
Apple IIgs · ROM 01 self-test (no disk)Apple IIgs (KEGS)Apple IIGSgreyOpen ⛶

Machines emulated

Chips

Notes

Embedding

KEGS is a portable-C Apple IIgs emulator. Its core (the 65C816 interpreter engine_c.c, the Mega II, video and disk) is platform-independent; only a thin driver layer (X11 / Windows / Mac) touches the host. We compile the core with Emscripten and replace that driver layer with a small emsdriver.c - no SDL. KEGS renders each frame into a 32-bit pixel buffer (g_mainwin_kimage.wptr), which JavaScript reads straight out of the WASM heap and blits to a 2D canvas, exactly like the vAmiga port.

var Module = await createKegs({ locateFile: f => SRC + f });   // loads kegs.wasm
Module.FS.writeFile('ROM', rom01Bytes);         // the IIgs ROM, as the file KEGS opens
Module.FS.writeFile('config.kegs', 's5d1 = sysdisk.2mg\n');  // slot 5 = 3.5" disk
Module.ccall('wasm_boot', 'number', [], []);   // parse_argv + kegs_init: builds the machine

Rendering is manual. Each animation frame we advance the machine by one video field and copy its pixel buffer (KEGS writes 0x00RRGGBB; we add the alpha byte) into the canvas:

var src = new Uint32Array(Module.HEAPU8.buffer, Module._wasm_pixel_buffer(), w*h);
for (var i=0; i<w*h; i++){ var v=src[i]; dst[i] = 0xff000000 | ((v&0xff)<<16) | (v&0xff00) | ((v>>16)&0xff); }
ExportWhat it does
wasm_boot()Build the machine (after the ROM + config + disk are in the Emscripten FS).
wasm_dbg_run_frame()Run exactly one video field of 65C816 time; returns 1 if a breakpoint paused the core.
wasm_dbg_step()Run exactly ONE 65C816 instruction (via KEGS's g_stepping).
wasm_pixel_buffer()Pointer to the 32-bit ARGB-ish frame buffer in the WASM heap.
wasm_key(a2code, up)Inject an Apple ADB key code; drives the physical + on-screen keyboard.
wasm_dbg_a / _x / _y / _pc / _psr …Sample one 65C816 register; matching wasm_dbg_set_* write it back.
wasm_dbg_read / _write(addr)Side-effect-free read / write of the 24-bit 65C816 bus.
wasm_dbg_bp_set / _wp_set(addr)Install a real execution breakpoint / write watchpoint (KEGS guard).

Debugger integration

This is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines. KEGS already ships an interactive debugger (debugger.c) with a real single-step and a real breakpoint facility; we simply expose that machinery to JavaScript as a handful of sampling functions. They are called ~10×/second while the debugger window is open, and once per Step, never on the emulator's hot path (the project's golden performance rule). The stock KEGS run loop is byte-for-byte unchanged.

1 · Exactly what was changed. KEGS keeps the 65C816 register file in one global, Engine_reg engine (fields acc, xreg, yreg, stack, direct, dbank, psr and kpc, which packs the program-bank register with the 16-bit PC). We added wasmdbg.c, ~40 EMSCRIPTEN_KEEPALIVE functions that read/write that struct and drive KEGS's own control paths:

// wasmdbg.c, sampling only, no hot-path hook
extern Engine_reg engine;               // KEGS's live 65C816 register file
u32  wasm_dbg_a(){   return engine.acc  & 0xffff; }
u32  wasm_dbg_pc(){  return engine.kpc  & 0xffff; }        // PC; pbr = (kpc>>16)
u32  wasm_dbg_read(u32 a){ return get_memory_c(a) & 0xff; } // side-effect free
void wasm_dbg_step(){ g_stepping = 1; run_a2_one_vbl(); g_stepping = 0; } // ONE instruction
void wasm_dbg_bp_set(u32 a){ set_bp(a, a, 4); }        // KEGS native execute guard
void wasm_dbg_wp_set(u32 a){ set_bp(a, a, 3); }        // read|write watchpoint

Single-step is real: KEGS sets the engine's cycle budget to exactly one instruction when g_stepping is set, so run_a2_one_vbl() returns after one opcode. Breakpoints are real: set_bp arms a per-page guard that the engine checks only for pages that carry one (via fixup_brks), not a per-instruction callback, and a hit calls KEGS's halt2_printf, which sets g_halt_sim; the run loop notices and stops.

2 · The Emscripten driver. KEGS's X11 driver (xdriver.c) owns main() and the frame loop and, crucially, sets the display's RGB masks and builds the colour palette. We replaced it with emsdriver.c: a no-op window layer that lets JavaScript own the loop, forwards keys through KEGS's own adb_key_event, and, the one non-obvious step, sets the RGB masks to 0x00RRGGBB and calls video_set_palette() at boot (without it every pixel draws black). Sound / joystick / serial host layers are stubbed (headless in the browser today).

3 · What is REAL here. Live registers A, X, Y, SP, D, DBR, PBR, PC and the P status byte (with the emulation flag E and the native-mode width flags M and X), disassembled with the shared w65816 decoder; the full 24-bit bus read side-effect-free through get_memory_c; single-instruction step; and execution breakpoints + write watchpoints enforced inside the KEGS core. Nothing is faked host-side.

Architecture

KEGS (Kent's Emulated GS) is a full, cycle-accurate Apple IIgs. The whole machine is portable C compiled to one WASM module:

  • 65C816 - the 16-bit CPU, interpreted in engine_c.c; its registers live in the global Engine_reg engine. This is what the debugger samples.
  • Mega II + FPI/CYA - the custom glue that makes it a IIgs: bank shadowing, the slow-RAM ($E0/$E1) I/O and video space, and speed control.
  • Video - text, lo-res, hi-res, double-hi-res and the 320/640 Super Hi-Res modes, rendered per scanline into the pixel buffer JavaScript blits.
  • Ensoniq DOC - the 32-oscillator wavetable sound chip (emulated in-core; audio output is not wired up in this browser build).
  • IWM + SmartPort - 3.5" (slot 5) and 5.25" (slot 6) drives and the SmartPort hard-disk interface, reading .2mg / .po / .dsk / .woz images.
  • ROM - the Apple IIgs ROM 01 (Apple ©), mapped to banks $FE–$FF; with a system disk it boots ProDOS or GS/OS.

How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc on PATH).

git clone https://github.com/a2kegs/kegs.git   # KEGS, GPL-3.0
# add two files to src/: emsdriver.c (Emscripten host driver + main/wasm_boot,
#   sets RGB masks + video_set_palette, stubs sound/joystick/serial) and
#   wasmdbg.c (the wasm_dbg_* sampling API over Engine_reg + set_bp/g_stepping).
# in debugger.c: reduce PC_LOG_LEN from 2M to 8K (trims ~112MB of static trace
#   buffers the web debugger never uses, so the wasm needs little memory).
emcc -O2 src/*.c   # (core objects only; NOT xdriver/scc*driver/*snd_driver/joystick_driver)
  -sMODULARIZE=1 -sEXPORT_NAME=createKegs -sEXIT_RUNTIME=0 -sALLOW_MEMORY_GROWTH=1
  -sFORCE_FILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=ccall,FS,HEAPU8
  -sEXPORTED_FUNCTIONS=_wasm_boot,_wasm_dbg_*,_wasm_key,_wasm_pixel_buffer,...
  -o kegs.js         # -> kegs.js + kegs.wasm (vendored here)

The only source edits are the two additive files, the PC_LOG_LEN trim, and dropping the platform driver objects - the emulation hot path is untouched. The IIgs ROM 01 dump we ship has its two 64K halves stored high-bank-first; KEGS wants bank $FE then $FF, so the halves are swapped in the vendored apple2gs-rom01.bin.