SearchA-ZO › O2EM

O2EM

1996 Open source · Artistic-2.0 Online

O2EM is the long-running Magnavox Odyssey2 / Philips Videopac emulator started by Daniel Boris in 1996 and since maintained as a clean, portable C core (the libretro fork). It emulates the console's Intel 8048 microcontroller and the Intel 8244 video-and-sound chip. Here the core is compiled to WebAssembly and driven by a small custom frontend, giving in-browser play plus a live 8048 debugger, registers, memory, disassembly, single-step and breakpoints.

Visit the official site ↗

Runs on: Web browser

O2EM Online Emulator

Play O2EM using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Odyssey2 (SELECT GAME + Hello demo)O2EMMagnavox Odyssey²openOpen ⛶
Odyssey2 (Joystick demo)O2EMMagnavox Odyssey²openOpen ⛶

Machines emulated

Chips

Notes

Embedding

O2EM is a C emulator (originally Daniel Boris, 1996; the modern libretro fork). We compile its core to WebAssembly with Emscripten and drive it from a ~180-line frontend, wasmfront.c, that replaces a RetroArch host with the smallest thing that runs the core in a browser and hands its framebuffer, audio and input to the page.

Build (reproducible). In a scratch clone of github.com/libretro/libretro-o2em:

// Homebrew emscripten on PATH (/opt/homebrew/bin), no emsdk needed
emcc -O2 -I. -Isrc -Iallegrowrapper -Ilibretro-common/include \
  src/*.c libretro.c allegrowrapper/wrapalleg.c src/vkeyb/*.c \
  libretro-common/{compat,encodings,file,streams,time,vfs}/*.c \
  wasmfront.c -o o2em.js \
  -sMODULARIZE=1 -sEXPORT_NAME=O2EM -sENVIRONMENT=web \
  -sALLOW_MEMORY_GROWTH=1 -sINVOKE_RUN=0 -sEXPORTED_FUNCTIONS=_malloc,_free \
  -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,FS,HEAPU8

Boot. The module is a factory; the BIOS is written into the in-memory FS (the core reads it by filename), then the cart is handed to retro_load_game through the frontend's o2_load:

const Module = await O2EM({ locateFile: f => base + f });
Module.FS.writeFile('/o2rom.bin', biosBytes);   // system BIOS (grey)
Module.ccall('o2_init');
const p = Module._malloc(cart.length); Module.HEAPU8.set(cart, p);
Module.ccall('o2_load', 'number', ['number','number'], [p, cart.length]);
(function loop(){ Module._o2_run(); blit(); requestAnimationFrame(loop); })();

Each o2_run() runs exactly one video frame; the frontend converts the core's RGB565 buffer to RGBA for a <canvas>. Input is fed with o2_set_joypad(port, mask) (Odyssey2 joysticks) and o2_set_key(retroKey, down) (the console keypad).

Debugger integration

The interesting part is giving a compiled wasm core the SAME live debugger the JavaScript emulators have, without touching the hot path. The 8048's registers and RAM live in the core's own C globals (acc, pc, psw, reg_pnt, intRAM, …). wasmfront.c adds a handful of EMSCRIPTEN_KEEPALIVE functions that, when called, read those globals; nothing is polled per cycle:

EMSCRIPTEN_KEEPALIVE int  o2_pc(void)      { return pc; }
EMSCRIPTEN_KEEPALIVE int  o2_acc(void)     { return acc; }
EMSCRIPTEN_KEEPALIVE int  o2_reg(int i)    { return intRAM[(reg_pnt + (i & 7)) & 0x3f]; }  // R0-R7
EMSCRIPTEN_KEEPALIVE int  o2_iram(int a)   { return intRAM[a & 0x3f]; }             // side-effect free
EMSCRIPTEN_KEEPALIVE int  o2_step(void)    { dbg_single = 1; RLOOP = 1; cpu_exec(); dbg_single = 0; return pc; }

The only change inside the emulation loop is a single flag test at the tail of cpu.c's per-instruction while loop, the same class as the breakpoint check already there, and it never calls back into JS:

if (pc == app_data.breakpoint) break;
if (dbg_single) break;   // set only while the debugger issues a Step

With that, o2_step() advances exactly one 8048 instruction. The machine plug-in (o2em-debug.js) reads the registers through these hooks each refresh, disassembles with the new i8048 decoder, and the boot loop implements breakpoints (step instruction-by-instruction, halt when PC matches the set) and watchpoints (snapshot a watched internal-RAM byte each step, halt on change) host-side. All reads are on-demand, so the emulator runs full speed with the debugger closed.

Architecture

The Odyssey2 is an Intel 8048 (MCS-48) microcontroller paired with the Intel 8244 (i8244 / "VDC") video-and-sound chip. The 8048 has its own 1 KB mask ROM (the system BIOS), 64 bytes of on-chip RAM (where R0-R7 and the stack live), and reaches the game cartridge and the VDC over its external bus. O2EM models each piece:

  • cpu.c - the 8048 interpreter: acc, pc, psw, the register banks in intRAM, the timer/counter and interrupts. One cpu_exec() runs a whole video frame.
  • vdc.c - the i8244: the grid, the four sprites, the character generator (the multicolour font on the "SELECT GAME" screen) and the sound, rasterised into an RGB565 buffer.
  • vmachine.c - the bus: BIOS + cartridge ROM banking, the 256-byte external RAM, and the VDC/keyboard I/O windows.
  • keyboard.c - the console's 48-key membrane keyboard and the two joysticks.

The debugger's memory views expose the program ROM (current 4 KB bank, disassembled as 8048 code), the 64-byte internal RAM (with R0-R7) and the 256-byte external RAM.