# n64js runs a Nintendo 64 emulator in the browser, and its HLE audio microcodes are the hard part

> hulkholden/n64js is a JavaScript N64 emulator that runs in a web page, with a JIT recompiler for the CPU, high level emulation for graphics and audio, and an experimental low level RDP path. Cycle timing is still an approximation, saving depends on very recent browser APIs, and the project is explicit that the author built it as a personal challenge.

**hulkholden/n64js** — An n64 emulator in JavaScript

- Repository: https://github.com/hulkholden/n64js
- Stars: 656 · Forks: 86
- Language: JavaScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/hulkholden-n64js

## Twenty five years of emulator work, ported to JavaScript

n64js is a Nintendo 64 emulator written in modern JavaScript that runs directly in a browser page. The project description says it runs many ROMs at full framerate.

The motivation is unusual and worth reporting accurately, because it tells you what kind of artefact to expect. The author describes the project as mostly for the challenge, having spent around 25 years on and off working on N64 emulators, and writing one in JavaScript as a way to stretch into an unfamiliar language. It is presented as an example of what modern browsers can do.

The lineage explains the engineering style. n64js is derived from Daedalus, an emulator the author started around 1999 and returned to periodically for many years. A bet with another developer around 2012 about writing a JavaScript port produced the current project.

The audience is therefore two groups at once: people who want a playable browser emulator, and people who want to read an emulator written in a language that makes the usual low level tricks look unfamiliar. The repository layout fits the second group, with a src/ tree, a test/ directory, tools/ and headless scripts for running the emulator without a UI.

The licence is MIT, and the repository carries DEVELOPMENT.md for building and testing, BENCHMARKS.md for profiling, and a package.json pinning Bun 1.3.14 as the package manager.

## High level emulation first, low level emulation as the fallback

The single most important architectural decision in n64js is that it tries to avoid emulating the RSP the hard way whenever it can.

Graphics are primarily rendered through WebGL high level emulation, with an experimental low level RDP path available. The checklist in the implementation status section lists the HLE display lists that are covered, naming GBI0, GBI1, GBI2, S2DEX, L3DEX, Turbo3D, T3DUX and ZSortp, while the LLE renderer is still marked experimental.

Audio works the same way. High level audio emulation is used by default for supported microcodes, derived from captured RSP disassembly and libultra headers. The supported families are named: ABI1 variants for games including Super Mario 64, Tetrisphere, GoldenEye 007 and Blast Corps, NAUDIO variants for Army Men: Air Combat and Banjo-Kazooie, and NEAD variants for Mario Kart 64, Star Fox 64, Ocarina of Time and Majora's Mask.

The fallback is automatic. Microcode identities and command shapes that are not reviewed fall back to low level RSP emulation, and the options panel has Audio, Emulation Mode, LLE to force every audio task onto the RSP. Two titles are singled out: Indiana Jones and Battle for Naboo always use low level RSP execution for their custom graphics microcodes, even when HLE graphics is selected, and their output uses the experimental RDP renderer.

This design is what makes browser performance possible, and it is also the source of the compatibility gaps. A game with a custom microcode gets slower output and possibly a glitchy frame, because the fast path does not exist for it.

## Internal render scale and canvas scale are two different knobs

The graphics options panel exposes three settings that are easy to confuse, and separating them is the fastest way to understand how n64js presents a frame.

Internal Render Scale defaults to 1x and uses the VI source resolution, often 320x240. Raising it, up to 4x, gives higher resolution 3D geometry because the geometry is computed at that resolution rather than scaled up afterwards.

Canvas Scale controls the output display resolution independently of the internal rendering. The final presentation pass upscales the frame and applies the CRT effects at the canvas resolution. The documentation is specific about the combination: 1x internal rendering gives a native pixel CRT look, while 2x gives 640x480 rendering for a 320x240 game.

There is a detail buried in that paragraph that tells you the emulator is emulating video modes properly. Progressive CPU framebuffers upload native pixels, while interlaced CPU video retains field weaving. That is the behaviour of a real video interface, not a framebuffer approximation.

The CRT effect itself has three modes: Off, which is the default, Simple, and Mattias. The Mattias style adds colour separation, spatial ghosting, adjustable curvature and animated scanlines that pause with emulation. It follows the MattiasCRT shader by Mattias Gustavsson and its implementation is adapted from his MIT licensed crtemu_pc.h, with the attribution and the full MIT notice retained in src/hle/graphics/shaders/crt_mattias.glsl. The adaptation uses the current frame for ghosting and drops the original library's temporal blur buffers and frame overlay.

Controls are keyboard or gamepad through the browser Gamepad API, with a documented mapping table from N64 inputs to keys and buttons.

## Cycle accuracy is still a checkbox left open

The implementation status list is honest in a way that many emulator projects are not, and it is worth reading line by line.

The CPU section shows an interpreter and a dynamic recompiler marked done, COP0 instructions, COP1 floating point instructions and the TLB also done. Cycle accuracy is the one open box. The documentation states the consequence directly: instruction timing is still an approximation rather than cycle accurate, which can affect timing sensitive games.

The graphics section is marked incomplete for the same reason, with HLE listed as done and LLE marked experimental for RDP support. The Controller section has static key bindings and Gamepad API support done, with configurable bindings still open. Save is marked incomplete too: persistence through localStorage is done, along with MemPack, EEPROM 4K and 16K, SRAM and FlashRAM, but import and export are not implemented.

Compatibility is measured rather than asserted. The documentation says over 95 percent of the n64-systemtest tests pass. The remaining areas are graphics, where some microcodes such as Factor 5's custom ones and hardware specific framebuffer effects still have unimplemented features or visual glitches, cycle timing, and edge case CPU and FPU behaviour, where some 64-bit addressing edge cases and floating point rounding near numerical limits are not fully emulated.

On performance, the claim is scoped: with dynamic CPU recompilation and HLE graphics and audio, most games run at full framerate on modern desktop hardware, tested primarily on Apple Silicon Macs. Games falling back to LLE audio or the experimental LLE graphics path need significantly more CPU and GPU work.

## Save games need browser APIs that arrived very recently

There is a hard platform requirement here that is easy to miss and impossible to work around with a polyfill.

Saving and loading require native support for Uint8Array.prototype.toBase64() and Uint8Array.fromBase64(). The documentation names the versions that support them: Chrome and Edge 140 or newer, Firefox 133 or newer, or Safari 18.2 and newer including iOS. WebGL is also required as well.

That is a narrow window. The base64 conversions on typed arrays are recent additions to the platform, and the project chose to use them rather than write its own encoder. The result is that the emulator does not run on older browsers, and a Safari user on an older release sees no working save.

The save subsystem itself is more complete than the import and export gap suggests. Persistence goes through localStorage, and the memory card formats are covered: MemPack for the Controller Pak, EEPROM in both 4K and 16K variants, SRAM, and FlashRAM. What is missing is moving save data between machines, since import and export are listed as open work alongside savestates.

Chrome is named as the preferred browser, with the note that it typically offers the best performance.

For a hosted instance, that last point and the base64 requirement together mean the browser is part of your support matrix, not just the user's choice.

## Running it in a page, or building it with Bun

The quickest way to use the emulator needs no build at all. A hosted version is published on GitHub Pages, and the instruction is to click Load in the toolbar and choose a local N64 ROM file in .z64, .v64 or .n64 format. ROMs and save data stay locally in the browser, so nothing is uploaded.

For development, the build pipeline is driven by Bun, with packageManager pinned to bun@1.3.14 and the source living in src/ with a js/ directory, index.html and the stylesheets at the root. The npm scripts are the entry points:

```json
  "build": "bun run tools/build.js",
  "build-debug": "bun run tools/build.js --debug",
  "benchmark": "bun run ./src/headless/benchmark.js",
  "headless": "bun run ./src/headless/headless.js",
  "inventory": "bun run ./src/inventory/inventory.js",
```

The headless entry points are the interesting part for anyone who wants to automate rather than play. The headless script runs the emulator without the browser UI, the benchmark script exists for timing, and the inventory scripts query ROM inventories in batch, single and summary forms.

There is also a visual test path, with a texture sampler script under test/webgl, a build script that bundles a visual page and a test runner invoked with node, plus Playwright in the development dependencies and a lint script that runs eslint across src, js, test and tools with a zero warning budget.

DEVELOPMENT.md covers local builds, tests, headless scripts, ROM inventory queries, CI workflows and publishing, and BENCHMARKS.md covers benchmarking and profiling.

## Weekly releases, MIT terms, and an open to-do list

Maintenance is the healthiest part of this project. The repository is not archived, the last push was on 2026-10-19, and the release tags land about once a week: v1.0.6 on 2026-10-18, v1.0.5 on 2026-10-11 and v1.0.4 on 2026-10-02.

That cadence is worth noticing against the package metadata, where the version field still reads 1.0.0 while tags carry 1.0.4 through 1.0.6. If you pin a dependency, pin a tag rather than trusting the version field in package.json.

The licence is MIT, which covers the emulator code, and the third party situation is handled explicitly. The Mattias CRT shader is an adaptation of Mattias Gustavsson's MIT licensed crtemu_pc.h, with attribution and the full MIT notice retained in the source file. The runtime dependencies are small: lil-gui for the options panel, mitata for measurement and stats.js for statistics.

The open items are listed in the same document that reports the compatibility numbers: continue fixing graphics issues and expanding microcode coverage, configurable controller bindings, save game import and export, and savestates.

For an emulator that is still missing savestates and configurable bindings, weekly tags mean the gaps are moving targets. That cuts both ways: bugs get fixed quickly, and behaviour you depend on can change between two tags a week apart.

## Conclusion

Use n64js if you want to study emulator architecture in modern JavaScript, or if you want a browser based N64 you can run without installing anything, and you already own the ROM images you load. Do not adopt it as a cycle accurate reference or as a platform for timing sensitive games, since instruction timing is explicitly an approximation. Before relying on it, check that your browser supports Uint8Array.prototype.toBase64() and WebGL, then run the emulator against the n64-systemtest suite and compare the pass rate with the over 95 percent the documentation claims for your target games.

## FAQ

### What is n64js and how do I run it?

It is a Nintendo 64 emulator written in modern JavaScript that runs in the browser. A hosted build is published on GitHub Pages, where you click Load in the toolbar and choose a local ROM file in .z64, .v64 or .n64 format.

### Does n64js need a browser extension or a downloaded ROM set?

The documentation says to choose a local N64 ROM file yourself and that ROMs and save data stay locally in the browser. No plugin is required, only WebGL and recent browser support for the typed array base64 methods.

### How accurate is the emulation in n64js?

Over 95 percent of the n64-systemtest tests pass, according to the documentation. Instruction timing is still an approximation rather than cycle accurate, and some graphics microcodes and floating point edge cases remain unimplemented.

### Which browsers can run n64js?

Saving and loading need native Uint8Array.prototype.toBase64() and Uint8Array.fromBase64(), which means Chrome or Edge 140+, Firefox 133+, or Safari 18.2+ including iOS. WebGL is also required, and Chrome is described as the preferred browser.

### Can n64js save and load games?

Persistence through localStorage is implemented, along with MemPack, EEPROM 4K and 16K, SRAM and FlashRAM. Import and export of save data and savestates are still open items in the implementation status list.

### How do I build n64js locally?

The project uses Bun, pinned to bun@1.3.14 in package.json. The build runs through bun run tools/build.js, with a debug variant, plus separate scripts for headless runs, benchmarks and ROM inventory queries.

## Sources

- [hulkholden/n64js on GitHub](https://github.com/hulkholden/n64js)
- [Issues](https://github.com/hulkholden/n64js/issues)
- [License: MIT](https://github.com/hulkholden/n64js/blob/main/LICENSE)
- [README](https://github.com/hulkholden/n64js/blob/main/README.md)
- [Releases](https://github.com/hulkholden/n64js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hulkholden-n64js
