O2EM
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.
Runs on: Web browser
O2EM Online Emulator
Play O2EM using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Odyssey2 (SELECT GAME + Hello demo) | O2EM | Magnavox Odyssey² | open | Open ⛶ | |
| Odyssey2 (Joystick demo) | O2EM | Magnavox Odyssey² | open | Open ⛶ |
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 inintRAM, the timer/counter and interrupts. Onecpu_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.