Search › A-Z › N › Nintendo 64
Nintendo 64
This is the Nintendo 64 running in your browser: N64Wasm (Neil Barkhina) — the RetroArch ParaLLEl / mupen64plus core — compiled from source to WebAssembly with Emscripten and self-hosted, no CDN. It boots public-domain N64 homebrew and plugs into the shared in-frame debugger for the NEC VR4300 (MIPS III) main CPU, with genuine live registers, a real single-instruction step, side-effect-free RDRAM, breakpoints and watchpoints.
Runs on: Web browser
Nintendo 64 Online Emulator
Play Nintendo 64 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Mandelbrot (FPU) | Nintendo 64 | Nintendo 64 | open | Open ⛶ | |
| Julia (FPU) | Nintendo 64 | Nintendo 64 | open | Open ⛶ | |
| Mandelbrot (interactive) | Nintendo 64 | Nintendo 64 | open | Open ⛶ | |
| CP0 Register Test | Nintendo 64 | Nintendo 64 | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The Nintendo 64 core is N64Wasm (Neil Barkhina, MIT) — the RetroArch ParaLLEl / mupen64plus emulator compiled to WebAssembly with Emscripten. N64Wasm's frontend (mymain.cpp) is an SDL2 + WebGL2 app that drives the libretro core; we rebuilt it from source and self-host the result (n64wasm.js / n64wasm.wasm, no CDN). The module is a classic (non-MODULARIZE) Emscripten build linked with -sINVOKE_RUN=0, so main() only runs when we call it and the frame loop is ours to drive.
Boot. We define window.Module first (canvas + locateFile), load the glue, then in onRuntimeInitialized stage the assets, a config.txt (key map + options) and the ROM into the Emscripten FS, call main(), and start our own requestAnimationFrame loop over _runMainLoop() (one video frame each call):
var Module = { canvas: cv, locateFile: f => SRC + f, noInitialRun: true,
onRuntimeInitialized: function(){
Module.FS.writeFile('assets.zip', assets);
Module.FS.writeFile('config.txt', config); // key map + renderer options
Module.FS.writeFile('game.z64', rom);
Module.callMain(['game.z64']); // load ROM, init RCP, return
(function loop(){ if(running) Module._runMainLoop(); requestAnimationFrame(loop); })();
} };
Rendering is native. The core owns a WebGL2 context on the <canvas id="canvas"> and rasters straight to it, so there is no manual blit — pausing is just declining to call _runMainLoop().
Input. The frontend reads the browser keyboard through SDL2 (SDL_GetKeyboardState) mapped by config.txt, so the physical keyboard works with no glue. The on-screen pad (in the shell fold-up and the debugger Controls window) synthesizes DOM keydown/keyup events carrying the same code values, which SDL2 picks up — one input path for both.
| Member | Kind | What it does |
|---|---|---|
callMain(['game.z64']) | runtime | Run main(): read config, load the ROM into the ParaLLEl core, init the RCP, and return. |
_runMainLoop() | export | Emulate exactly one video frame (retro_run: VR4300 + RSP + RDP + VI). Our run loop's advance and frame-step. |
_neil_dbg_pc() / _neil_dbg_set_pc(v) | export | Read / write the VR4300 program counter (r4300_pc / generic_jump_to). |
_neil_dbg_reg_lo(i) / _neil_dbg_reg_hi(i) | export | Low / high 32 bits of the 64-bit GPR i (r4300_regs()[i]). |
_neil_dbg_set_reg(i,lo,hi) | export | Write back a 64-bit GPR. |
_neil_dbg_read8(a) / _neil_dbg_write8(a,v) | export | Side-effect-free RDRAM byte access (the ^3 big-endian swap), for the hex / disasm views. |
_neil_dbg_step_one() | export | Execute exactly one VR4300 instruction (InterpretOpcode()), the debugger's Step. |
Debugger integration
This is a WebAssembly console, yet it gets the same live debugger as the pure-JavaScript machines — real 64-bit registers, real memory, a real single-instruction step, plus execution breakpoints and memory watchpoints. mupen64plus keeps the VR4300 register file and RDRAM in plain C, reachable through its own accessors, which we expose from the rebuilt frontend as a small sampling API.
1 · Exactly what was added. A block of EMSCRIPTEN_KEEPALIVE functions appended to mupen64plus-core/src/main/main.c (registers + RDRAM) and one to r4300/pure_interp.c (the step). 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 — retro_run / the interpreter hot path is byte-for-byte unchanged (the golden performance rule). We also flipped the core's default from cached to pure interpreter so the single-step is one exact MIPS instruction.
// main.c — sampling only, no hot-path hook
uint32_t neil_dbg_pc(void) { return *r4300_pc(); }
uint32_t neil_dbg_reg_lo(int i) { return (uint32_t)(r4300_regs()[i&31]); }
uint32_t neil_dbg_reg_hi(int i) { return (uint32_t)((uint64_t)r4300_regs()[i&31] >> 32); }
int neil_dbg_read8(uint32_t a){ // side-effect-free RDRAM byte
uint8_t *d = (uint8_t*)g_dev.ri.rdram.dram;
return d[(a & (g_dev.ri.rdram.dram_size-1)) ^ 3]; } // ^3 = N64 big-endian byte
// pure_interp.c
void neil_dbg_step_one(void){ InterpretOpcode(); } // one exact VR4300 instruction
2 · What is REAL here. Because mupen64plus keeps the VR4300 state in a plain C context:
- Real registers — all 32 GPRs (r0-r31), PC, and HI/LO, read and written live off
r4300_regs()/r4300_pc(). The GPRs are 64-bit; the register panel shows the low 32 bits (the shared hex formatter is 32-bit), and writes preserve the high word. - Real single-instruction step —
Step icallsneil_dbg_step_one()=InterpretOpcode(), which runs exactly one MIPS III instruction (and its delay slot); PC advances by one instruction per click. - Side-effect-free memory — reads resolve RDRAM directly with the N64 big-endian byte order, so auto-polling the hex / disasm view never disturbs the machine. The new
vr4300decoder disassembles it as MIPS III. - Execution breakpoints & memory watchpoints — implemented host-side in the run loop, so they cost nothing when the debugger is closed.
3 · Breakpoints and watchpoints without a hot-path hook. With nothing armed, the loop runs a whole frame at native speed with _runMainLoop(). As soon as a breakpoint or watchpoint is set, the loop switches to stepping the VR4300 one instruction at a time with neil_dbg_step_one(), comparing the live PC against the breakpoint set and each watched RDRAM byte against its last sampled value, and pausing on a hit. (While single-stepping the CPU the RSP/RDP/VI do not advance, so the picture holds on its last frame until you resume.)
4 · The controllable loop. We own the frame loop so the transport can pause/step. Pause stops calling _runMainLoop(); Resume restarts it; Step (frame) runs one _runMainLoop(); Step i (instruction) calls neil_dbg_step_one().
Architecture
The Nintendo 64 (1996) is a 64-bit cartridge console built around two big chips. Its main CPU is the NEC VR4300, a 64-bit MIPS III core (the debugger's vr4300 decoder covers the MIPS I/II/III integer set, the 64-bit D-ops, and the COP0/COP1 FPU). Alongside it the RCP (Reality Co-Processor) holds the RSP (a MIPS-based vector unit for audio/geometry microcode) and the RDP (the rasteriser). mupen64plus emulates the whole machine; N64Wasm's ParaLLEl build runs it in the browser.
- NEC VR4300 (MIPS III) — the main CPU at ~93.75 MHz. mupen64plus runs it here as a pure interpreter;
InterpretOpcode()is the run-one-instruction primitive this integration single-steps with. - RSP — the Reality Signal Processor, a MIPS core running microcode; ParaLLEl emulates it (with the HLE / cxd4 paths) for graphics and audio lists.
- RDP — the Reality Display Processor, which rasterises triangles and the framebuffer, presented to the WebGL2 canvas.
- RDRAM — 4 MB (8 MB with the Expansion Pak) of Rambus memory; the debugger reads it live and side-effect-free.
- Cartridges load as
.z64/.n64/.v64images; mupen64plus HLE-boots the PIF, so no separate boot ROM is needed. The bundled ROMs are public-domain homebrew (krom / PeterLemon).
How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc on PATH).
git clone https://github.com/nbarkhina/N64Wasm
# 1. append the neil_dbg_* sampling hooks to mupen64plus-core/src/main/main.c
# and neil_dbg_step_one to r4300/pure_interp.c; force pure interpreter
# 2. add the hooks to EXPORTED_FUNCTIONS and rebuild (modern clang needs
# -Wno-error=incompatible-pointer-types for the 2021-era C):
cd N64Wasm/code && make # -> dist/n64wasm.js + dist/n64wasm.wasm
The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. It needs a WebGL2 context for the RCP output.