SearchA-ZS › SNES (LakeSnes)

SNES (LakeSnes)

2021 Open source · MIT Online

LakeSnes is a compact, readable C emulator of the whole Super Nintendo, the WDC 65C816 main CPU, the S-PPU video chips, the SPC700 and S-DSP audio, and the DMA controllers. For emulators.org its SDL-free core was rebuilt to WebAssembly with a small export file that samples the 65C816 register file and the SNES memories on demand, giving it the same in-browser debugger as the pure-JavaScript machines: live registers, side-effect-free memory, real single-instruction step, and real breakpoints and watchpoints. It boots the open homebrew game Space Rescue Squad by default.

Visit the official site ↗

Runs on: Web browser

SNES (LakeSnes) Online Emulator

Play SNES (LakeSnes) using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Space Rescue SquadSNES (LakeSnes)Super NESopenOpen ⛶

Machines emulated

Chips

Notes

Embedding

LakeSnes emulates a whole Super Nintendo - the WDC 65C816 main CPU, the S-PPU video chips, the SPC700 + S-DSP audio, and the DMA/HDMA controllers, as one WebAssembly module built by Emscripten. Its core (snes/*.c) is pure C with no SDL dependency; only the stock desktop main.c uses SDL. We therefore compiled just the core plus a small Emscripten glue file (emudbg.c) that replaces main.c, and we self-host everything (no CDN): the rebuilt snes.js + snes.wasm and a freely released homebrew ROM.

The build is a plain (non-modularised) Emscripten program, so you configure it by defining the global Module before the script loads, then drive it from JavaScript once the runtime is ready:

var Module = {
  locateFile: function(p){ return SRC + p; },        // find snes.wasm
  onRuntimeInitialized: function(){ boot(); }         // exports are ready
};
// boot(): init the machine, copy the ROM into the heap, load it (auto-resets).
Module._emu_init();
var p = Module._malloc(rom.length); Module.HEAPU8.set(rom, p);
Module._emu_load_rom(p, rom.length); Module._free(p);

Rendering is manual. The core has no SDL video, so each animation frame JavaScript asks the glue to repack the PPU framebuffer into RGBA and blits it straight from the wasm heap to a 2D canvas:

var ptr = Module._emu_render();                              // BGRX -> RGBA, opaque
var buf = new Uint8Array(Module.HEAPU8.buffer, ptr, 512*480*4);
imageData.data.set(buf); ctx.putImageData(imageData, 0, 0);
HandleKindWhat it does
Module._emu_run_frame()glueRun exactly one SNES frame (snes_runFrame) - the fast path, no debug checks. The run loop's advance and our frame-step.
Module._emu_render()glueCopy the PPU framebuffer out and repack it to RGBA; returns the heap pointer to blit.
Module._emu_set_button(pl,btn,down)glueSet / clear one controller button, drives the on-screen gamepad and the physical keyboard.
Module._emudbg_reg(i) / _emudbg_read(a)hookSample one 65C816 register / one CPU-bus byte on demand.
Module._emudbg_step()hookcpu_runOpcode - one 65C816 instruction (our Step i).
Module._emu_reset(hard)gluesnes_reset - reboots the machine.

Debugger integration

How a WebAssembly core with PRIVATE state still gets the full debugger. LakeSnes keeps the whole 65C816 architectural state in a C Cpu struct inside the wasm sandbox, with no JS view onto it. The Tier-4 rule is exactly this case: a core that hides state needs a patched build that copies its state out. So we rebuilt LakeSnes with one new file, emudbg.c, holding a handful of EMSCRIPTEN_KEEPALIVE functions that SAMPLE the state when the debugger asks (~10x/s + once per step). There is no per-instruction or per-cycle hook on the fast path - snes_runFrame is untouched, so a closed debugger costs nothing.

1 · Exactly what was changed. LakeSnes's snes/cpu.h exposes the register file directly, and snes_runCpuCycle shows the core already has a one-instruction primitive (cpu_runOpcode). emudbg.c reads those and replaces the SDL main.c:

// NEW FILE: emudbg.c - sampling only; reads the LakeSnes Cpu struct + SNES RAM
uint32_t emudbg_reg(int i)   // 0..7 = A X Y SP D(dp) DBR(db) PBR(k) PC
void     emudbg_set_reg(i,v)  // write-back into cpu->a / x / y / sp / dp / db / k / pc
uint32_t emudbg_pc()      // (cpu->k << 16) | cpu->pc, 24-bit linear PC for disasm
uint32_t emudbg_p() / emudbg_e()  // status byte N V M X D I Z C, and hidden E flag
uint32_t emudbg_read(a)    // side-effect-free 24-bit CPU-bus read (skips every I/O window)
uint8_t* emudbg_wram/vram/cgram/oam/aram()  // stable heap pointers to the raw memories
void     emudbg_step()    // cpu_runOpcode(snes->cpu) - ONE real 65C816 instruction
int      emudbg_run_frame_dbg()  // one frame, stop early on a breakpoint / watchpoint
What the debugger needsWhere it lives in LakeSnes
A X Y SP, D (direct page), DBR, PBR, PCCpu.a/x/y/sp/dp/db/k/pc (snes/cpu.h) - sampled by emudbg_reg; all writable via emudbg_set_reg.
P status + E (65C816 M/X/E width flags)The boolean fields Cpu.n/v/mf/xf/d/i/z/c packed into a byte by emudbg_p (writable), plus the emulation flag Cpu.e.
CPU bus (for disasm)emudbg_read mirrors snes_rread but returns 0 for the PPU/APU/DMA/controller I/O windows, so auto-polling never trips a latch or clears the NMI flag.
WRAM / VRAM / CGRAM / OAM / APU RAMsnes->ram, ppu->vram, ppu->cgram, ppu->oam, apu->ram - exposed as stable heap pointers and read straight from Module.HEAPU8 (no per-byte call).

2 · What is REAL here. Because LakeSnes has a true instruction primitive, this build has genuine debug facilities, not approximations:

  • Real registers - the full 65C816 file, read and written live off the Cpu struct.
  • Real single-instruction step - Step i calls cpu_runOpcode, which advances the whole machine (its CPU read/write handlers drive the PPU/DMA/APU in lock-step) by exactly one instruction. The PC and registers change by one instruction per click.
  • Side-effect-free memory - the CPU-bus read skips I/O, so auto-polling the hex/disasm view never changes machine state.
  • Real breakpoints + write watchpoints - armed guards are enforced by emudbg_run_frame_dbg, a SEPARATE run loop that steps opcode-by-opcode and stops when the 24-bit PC hits a breakpoint or a watched byte changes. It is entered only while a guard is armed; the normal snes_runFrame path has no checks, so a closed debugger pays nothing (the golden performance rule).

3 · The M/X width caveat. The shared w65816 disassembler is stateless and assumes native 16-bit mode, so an immediate to the accumulator/index groups is shown 2 bytes wide even when the live M/X flag makes it 8-bit. The registers panel shows the true M/X/E bits, so you can read the disasm accordingly. Every other opcode length is deterministic.

How to rebuild this emulator (reproducible from scratch):

# toolchain: Homebrew emscripten 6.0.3 (emcc on PATH)
git clone https://github.com/elzo-d/LakeSnes        # MIT
# drop emudbg.c (above) into the repo root; it replaces the SDL main.c.
cd LakeSnes
emcc -O2 -I ./snes \
  snes/spc.c snes/dsp.c snes/apu.c snes/cpu.c snes/dma.c snes/ppu.c \
  snes/cart.c snes/input.c snes/statehandler.c snes/snes.c snes/snes_other.c \
  emudbg.c -o snes.js \
  -sEXPORTED_FUNCTIONS=_emu_init,_emu_load_rom,_emu_run_frame,_emu_render,_emu_set_button,_emu_reset,_emudbg_reg,_emudbg_set_reg,_emudbg_pc,_emudbg_p,_emudbg_set_p,_emudbg_e,_emudbg_set_e,_emudbg_read,_emudbg_read_block,_emudbg_write,_emudbg_wram,_emudbg_vram,_emudbg_cgram,_emudbg_oam,_emudbg_aram,_emudbg_step,_emudbg_bp_clear,_emudbg_bp_add,_emudbg_wp_clear,_emudbg_wp_add,_emudbg_run_frame_dbg,_malloc,_free \
  -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,getValue,setValue,HEAPU8,HEAPU16,HEAPU32 \
  -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=33554432 -sENVIRONMENT=web -sMODULARIZE=0 -sEXPORT_NAME=Module
# out: snes.js (~13K) + snes.wasm (~87K), vendored here as-is. No SDL, no audio.

Architecture

LakeSnes is a compact, readable C emulator of the whole Super Nintendo; this is its Emscripten/WebAssembly port. The entire machine lives in one wasm module:

  • WDC 65C816 - the 16-bit main CPU (snes/cpu.c), with the register file (snes/cpu.h) and the one-instruction cpu_runOpcode this integration relies on. The 65C816 is the same CPU as the Apple IIgs, in native 16-bit mode here.
  • S-PPU - the two picture processors (snes/ppu.c): backgrounds, sprites/OAM, VRAM and the CGRAM palette, producing the 512x480 framebuffer we blit.
  • SPC700 + S-DSP - the audio subsystem (snes/spc.c, snes/dsp.c, snes/apu.c) with its own 64 KB RAM; this web build compiles it but leaves audio output unwired.
  • DMA / HDMA + cart mapper - snes/dma.c and snes/cart.c (LoROM/HiROM/ExHiROM detection in snes/snes_other.c).
  • ROM - Space Rescue Squad, a 256 KB LoROM homebrew platformer released for the 2025 SNESDEV game jam (game code zlib-licensed), booted straight to its title/gameplay screen.

The default machine runs the 65C816 in native 16-bit mode, which the shared w65816 disassembler models directly.