SearchA-ZV › vAmigaWeb

vAmigaWeb

2021 Open source · GPL-3.0 Online

vAmigaWeb is a WebAssembly port of the vAmiga emulator that brings cycle-exact Amiga emulation to the web browser. It focuses on Amiga gaming and provides an embeddable player component so ADF disk images can be run directly on a web page.

Visit the official site ↗

Runs on: Web browser

vAmigaWeb Online Emulator

Play vAmigaWeb using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Amiga 500 (AROS Kickstart)vAmigaWebAmiga 500openOpen ⛶

Machines emulated

Operating systems

Chips

Notes

Embedding

vAmigaWeb wraps the cycle-exact vAmiga emulator, whose CPU is the Moira 68000 core, compiled to WebAssembly. The build is one Emscripten module - vAmiga.js loads vAmiga.wasm, runs its main() (which constructs the machine), and then JavaScript loads a Kickstart ROM, powers on, and drives the frame loop. We self-host everything (no CDN): the rebuilt vAmiga.js / vAmiga.wasm and the two open AROS ROMs.

var Module = {
  locateFile: function(f){ return SRC + f; },     // find vAmiga.wasm
  onRuntimeInitialized: function(){ setTimeout(boot, 0); } // main() has run -> wrapper exists
};
// boot(): load the AROS ROM + ext ROM by filename suffix, then power on.
Module.ccall('wasm_loadFile', 'string', ['string','number','number','number'], ['aros.rom_file', ptr, len, 0]);
Module.ccall('wasm_power_on', 'string', ['number'], [1]);      // powerOn() + run()

Rendering is manual. The core does not use SDL for video; each frame JavaScript reads the emulator's pixel buffer straight out of the WASM heap and blits it to a 2D canvas:

var ptr = Module._wasm_pixel_buffer() + yOff*(HPIXELS<<2);
var buf = new Uint8Array(Module.HEAPU8.buffer, ptr, HPIXELS*clipped_height<<2);
imageData.data.set(buf);
ctx.putImageData(imageData, -xOff, 0, xOff, 0, clipped_width, clipped_height);
MemberKindWhat it does
Module._wasm_run() / _wasm_halt()exportResume / pause the machine. Our transport's resume / pause.
Module._wasm_execute()exportCompute exactly one video frame. Our frame-step, and the run loop's advance.
Module._wasm_draw_one_frame(now)exportMessage pump + returns how many frames behind wall-clock we are (drives catch-up).
Module._wasm_peek(a) / _wasm_poke(a,v)exportSide-effect-free byte read / write of the 24-bit 68000 bus (spypeek8 / poke8).
Module._wasm_key(code, pressed)exportInject an Amiga raw key code; drives the physical + on-screen keyboard.
Module._wasm_dbg_*exportThe debug sampling API added in the rebuild (see below): registers, step, breakpoints.

Debugger integration

This emulator is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines - registers, memory, real single-step, and real breakpoints. The stock vAmigaWeb build keeps the entire CPU inside the WASM sandbox and exports no register getters, so we rebuilt it from source with a tiny sampling API over the Moira core's public debug interface. This is the whole point of the Amiga's Moira core: it has an excellent C++ debug API, so the Amiga can be a genuinely full 68000 debug target.

1 · Exactly what was changed (the only source edit). We appended ~17 extern "C" functions to main.cpp. They read live state through the CPU object wrapper->emu->cpu.cpu, which is a vamiga::CPU that publicly derives moira::Moira, so Moira's getters and the breakpoint/watchpoint guard lists are directly reachable. They are sampling functions: the debugger calls them ~10×/second while its window is open, and once per Step. There is no per-instruction or per-cycle callback anywhere - the emulator's hot path is byte-for-byte unchanged (the project's golden performance rule).

// added to main.cpp - sampling only, no hot-path hook
extern "C" u32  wasm_dbg_reg(int i){        // 0-7 = D0-D7, 8-15 = A0-A7
  auto *cpu = wrapper->emu->cpu.cpu;             // moira::Moira
  return i<8 ? cpu->getD(i) : cpu->getA(i-8); }
extern "C" u32  wasm_dbg_pc(){ return wrapper->emu->cpu.cpu->getPC(); }
extern "C" u32  wasm_dbg_sr(){ return wrapper->emu->cpu.cpu->getSR(); }
extern "C" void wasm_dbg_step(){ wrapper->emu->stepInto(); }        // REAL 1-instruction step
extern "C" void wasm_dbg_bp_set(u32 a){ wrapper->emu->cpu.breakpoints.setAt(a); }
extern "C" void wasm_dbg_wp_set(u32 a){ wrapper->emu->cpu.watchpoints.setAt(a); }
// + set_reg / set_pc / set_sr / sp / usp / ssp / is_paused / bp_del|clear / wp_del|clear

The methods used are Moira's getD/setD, getA/setA, getPC/setPC, getSR/setSR, getSP/getUSP/getISP; vAmiga's own full-system single-step VAmiga::stepInto(); and the GuardsAPI lists cpu.breakpoints / cpu.watchpoints (setAt/removeAt/removeAll). Memory does not need a new hook; the stock build already exports wasm_peek (mem->spypeek8<Accessor::CPU>, side-effect free) and wasm_poke (poke8).

2 · What is REAL here (vs the pure-JS Amiga port). Because Moira exposes a true instruction primitive, this build has:

  • Real registers - D0-D7, A0-A7, PC, SR (with X N Z V C S flags), and USP/SSP, read and written live off the Moira register file.
  • Real single-instruction step - Step i calls VAmiga::stepInto(), which advances the whole machine by exactly one 68000 instruction (not a frame). The PC and registers change by one instruction per click.
  • Real execution breakpoints + write watchpoints - setAt(addr) installs a guard that Moira checks inside the core; when hit, the core pauses itself and the run loop notices (wasm_dbg_is_paused) and stops. These are enforced by the emulator, not faked host-side.
  • Side-effect-free memory - reads use spypeek8, so auto-polling the hex/disasm view never clears a latch or acknowledges an interrupt.

3 · The controllable loop. We own the frame loop so the transport can pause/step. Each animation frame, while running, we pump messages with wasm_draw_one_frame(now), blit the pixel buffer, and advance the emulation with wasm_execute() (one frame) up to the wall-clock deficit. Pause calls wasm_halt(); resume calls wasm_run(). The loop also checks wasm_dbg_is_paused() every frame, so a breakpoint that pauses the core from the inside is reflected in the UI. Single-instruction step uses wasm_dbg_step(); the coarse frame-step uses wasm_execute().

4 · The reusable recipe for a WASM core whose state is private. v86 could be debugged with no rebuild because it hands JS typed-array views onto its state. A C/C++ Emscripten core like vAmiga keeps its registers in WASM locals, invisible to JS, so the recipe is: (1) find the CPU object and its debug API in the source (here, Moira on emu->cpu.cpu); (2) add a handful of extern "C" sampling functions that copy that state out on demand - never a per-cycle hook; (3) add their names to EXPORTED_FUNCTIONS and rebuild; (4) map the debugger's registers/memory/step/breakpoints onto them. A core with a real instruction-step and a guard facility (like Moira) yields a fully real debugger; a core without one yields registers + memory + a coarser step.

Architecture

vAmiga is a cycle-exact Commodore Amiga emulator; vAmigaWeb is its Emscripten/WebAssembly port. The whole machine lives in one WASM module built from C++:

  • Moira - the Motorola 68000/010/020 core (Core/Components/CPU/Moira), with the public debug API this integration relies on: register getters/setters, execute(), a disassembler, and a Guards breakpoint facility. The Amiga wrapper vamiga::CPU derives from it.
  • Agnus / Denise / Paula - the custom chips: DMA + blitter + copper (Agnus), the bitplane/playfield video (Denise), and four-channel audio + disk (Paula), all cycle-driven by a central event scheduler.
  • Memory - chip RAM at $000000, the Kickstart ROM at $F80000, and the custom/CIA I/O windows; spypeek8 reads the bus without side effects.
  • Boot ROM - the AROS Kickstart replacement (AROS Public Licence, freely redistributable), shipped in the vAmigaWeb repo. With that ROM and no floppy, the Amiga reaches its animated insert-disk screen, rendered by the real Agnus/Denise/Copper chain.

How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc/em++/emcmake/emmake on PATH), cmake, make.

git clone --recurse-submodules https://github.com/vAmigaWeb/vAmigaWeb.git   # v4 / commit 9967181
cd vAmigaWeb
# 1. append the wasm_dbg_* sampling funcs to main.cpp (after wasm_get_cpu_cycles)
# 2. add their names to BOTH -sEXPORTED_FUNCTIONS lists in CMakeLists.txt
# 3. (optional, faster build) drop -flto and -O3 -> -O1 in CMakeLists.txt + Core/CMakeLists.txt
mkdir build && cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release
emmake make -j4                       # -> vAmiga.js (203K) + vAmiga.wasm (8.3M)

The default thread model is nonworker (single-thread; no SharedArrayBuffer, so it hosts anywhere). The only source change is the additive wasm_dbg_* block plus their export names; nothing on the emulation hot path is touched.