DEV Community

Cover image for How I Compiled 30-Year-Old Console Emulators to WebAssembly to Play 3,000+ Retro Games in the Browser
ludy.dev
ludy.dev

Posted on • Originally published at laowanke.com

How I Compiled 30-Year-Old Console Emulators to WebAssembly to Play 3,000+ Retro Games in the Browser

I’ve always had a massive soft spot for retro games. There’s something magical about the 8-bit and 16-bit eras of FC (NES), SFC (SNES), GBA, and classic arcade games. But modern emulation is surprisingly fragmented. You have to download native apps, hunt down sketchily hosted ROMs, configure controller mappings, and hope you don't download a Trojan in the process.

I wanted to change that. I thought: modern browsers are basically lightweight operating systems. Why not bring the entire console library to the web?

So, I built laowanke.com—a zero-download, instant-play nostalgic gaming platform running over 3,000+ retro games directly in the browser. Here is how I pulled off the engineering under the hood.

Featured Image

The Tech Stack: Porting C/C++ Cores to WebAssembly

The absolute backbone of this project is WebAssembly (Wasm). Instead of writing emulation engines from scratch in JavaScript (which would be painfully slow and inaccurate), I leveraged legendary open-source C/C++ emulation cores (like retroarch/libretro projects) and compiled them using Emscripten.

By compiling these mature codebases to Wasm, I achieved near-native execution speed. The browser runs the game loops, processes CPU/GPU instructions of these vintage consoles, and pipes the video/audio output directly to a <canvas> element and the Web Audio API.

Solving the Real-time Save State Challenge

One of the biggest hurdles was managing save states. Classic game cartridges used battery-backed RAM (SRAM) to save progress, while modern emulators use state snapshots (save states).

To make this feel like a modern cloud gaming experience, I had to map the Wasm virtual file system to the browser's persistent storage. I implemented a hybrid storage system:

  • IndexedDB handles local persistence for auto-saves and manual save slots. It operates asynchronously, so I had to write a JS wrapper that hooks into the emulator's save state cycle without blocking the main rendering thread.
  • If a player logs in, these IndexedDB blobs are synced to a lightweight backend database, letting them resume their Pokémon GBA run on their phone right where they left off on their desktop.

Taming Audio Latency and Input Lag

In retro action games, a 50ms delay in button inputs or audio sync makes the game completely unplayable.

To solve input lag, I bypassed the standard browser keyboard events wherever possible and used the Gamepad API with a high-frequency polling loop mapped directly to the Wasm input registers. For audio, standard HTML5 Audio wasn't fast enough, so I implemented a dynamic audio buffer using the Web Audio API AudioWorklet. The Worklet pulls raw PCM audio data directly from the Wasm memory space in real-time, completely eliminating crackling and latency.

What's Next?

Right now, the platform supports keyboard controls, gamepads, save/load states, and a smooth catalog search UI. Next, I'm working on stabilizing WebRTC-based netplay so two players can play cooperative arcade games across different networks.

I’d love to get your thoughts on the performance and latency. Give it a spin at laowanke.com and let me know how it handles on your machines!

Top comments (0)