github bfirsh/jsnes v2.0.0
2.0.0

7 hours ago

Highlights

JSNES 2.0 is a major release focused on hardware accuracy, ease of use, and performance. The emulator now passes significantly more tests from the AccuracyCoin and nestest test suites, supports more games through six new mappers, adds Game Genie cheat code support, and ships a batteries-included Browser class for embedding in web pages with a single function call.

New: jsnes.Browser class

A new high-level Browser class makes it easy to embed JSNES in a web page without wiring up canvas rendering, audio, or input handling yourself:

<div id="nes" style="width: 512px; height: 480px"></div>
<script src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
<script>
  var browser = new jsnes.Browser({
    container: document.getElementById("nes"),
  });
  jsnes.Browser.loadROMFromURL("my-rom.nes", function (err, data) {
    browser.loadROM(data);
  });
</script>

The Browser class handles:

  • Canvas rendering and scaling to fit its container
  • Audio output via AudioWorklet (see below)
  • Keyboard input with configurable key bindings (persisted to localStorage)
  • Gamepad input with configurable button mapping
  • Zapper (light gun) support via mouse
  • Frame timing at 60 fps
  • Error handling via onError callback
  • Screenshots via browser.screenshot()
  • Full cleanup via browser.destroy()

It also works well with React and other frameworks — see the README for examples.

Game Genie support

New nes.addGameGenieCode(code) and nes.removeGameGenieCode(code) methods let users apply Game Genie cheat codes at runtime. Standard 6-letter (compare-free) and 8-letter (compare) codes are both supported.

New mappers

Six new cartridge mappers expand game compatibility:

  • Mapper 9 (MMC2) — Mike Tyson's Punch-Out!!, Punch-Out!! (PAL). Tile-triggered CHR bank switching via latches.
  • Mapper 71 (Camerica/Codemasters) — Fire Hawk, Micro Machines, Bee 52, MiG 29. UxROM variant with optional single-screen mirroring.
  • Mapper 79 (NINA-03/NINA-06) — Tiles of Fate, Krazy Kreatures, Impossible Mission II. GxROM-like with register in expansion area.
  • Mapper 118 (TxSROM) — Armadillo, Pro Sport Hockey, Goal! Two. MMC3 variant with CHR-controlled nametable mirroring.
  • Mapper 119 (TQROM) — Pin-Bot, High Speed. MMC3 variant supporting simultaneous CHR ROM and CHR RAM.
  • Mapper 240 — Sheng Huo Lie Zhuan, Jing Ke Xin Zhuan. Simple PRG/CHR banking via writes to $4020-$40FF.

Turbo buttons

New turbo button support simulates the auto-fire feature of the NES Advantage and similar controllers. Controller.BUTTON_TURBO_A and Controller.BUTTON_TURBO_B auto-fire at ~30 Hz while held. The Browser class maps these to the S and A keys by default.

Hardware accuracy improvements

Numerous fixes to match real NES hardware behavior, verified by the AccuracyCoin and nestest test suites:

NMI timing

  • Bus-cycle-aware NMI edge detection matching real 6502 behavior.
  • NMI promotion timing at VBlank clear for φ2 accuracy — the PPU can suppress or promote NMI depending on the exact dot within an instruction.
  • Proper handling of $2000 writes enabling NMI while VBlank is already active.

PPU catch-up

  • The PPU is now advanced inline after every CPU bus operation (load, write, push, pull), so mid-instruction PPU register reads see cycle-accurate state. Previously, the PPU only advanced between instructions.

Dummy reads and writes

  • Branch instructions: Taken branches now perform a dummy read from the old PC location, and page-crossing branches perform an additional dummy read from the partially-computed address.
  • Implied/accumulator modes: Second cycle reads from PC, matching real 6502 behavior.
  • Absolute indexed: Dummy read from address with uncorrected high byte on page crossing.
  • Zero page indexed: Dummy read from unindexed zero-page address while adding X/Y.
  • Pre-indexed indirect: Dummy read from pointer address before adding X.
  • Store and RMW instructions: Always perform the indexed dummy read, even without page crossing.
  • RMW dummy writes: ASL, LSR, ROL, ROR, INC, DEC (and unofficial equivalents) write the original value back to the address before writing the modified value.

These matter because dummy reads/writes are real bus operations — they update the data bus value and trigger I/O side effects (e.g., reading $4015 clears interrupt flags).

Open bus

  • CPU data bus is updated on every load, write, push, pull, opcode fetch, and interrupt vector fetch.
  • PPU I/O latch correctly returned for write-only PPU registers.
  • Controller ports ($4016/$4017) only drive bits 0–4; bits 5–7 come from the CPU data bus.
  • $4015 bit 5 comes from the CPU data bus (not driven by APU).

APU frame counter

  • Rewritten with cycle-accurate step timing, matching the real APU's 4-step and 5-step sequences.
  • APU state is now caught up before $4015 reads, so status reads mid-instruction return accurate values.
  • APU frame counter clocks even when emulateSound is disabled, since it drives IRQs that affect CPU behavior.

Other accuracy fixes

  • Sprite 0 hit: Now requires an actual opaque background tile pixel, not just a palette RAM access.
  • PPU open bus decay: The PPU I/O latch now decays to zero after ~600ms (36 frames) of inactivity, matching real hardware capacitance.
  • Palette reads: Fixed palette read behavior to return palette data in bits 0–5 with open bus in bits 6–7. Palette mirroring ($3F10/$3F14/$3F18/$3F1C mirror $3F00/$3F04/$3F08/$3F0C) is now correct.
  • Monochrome mode: Fixed swapped color values in the PPU's monochrome/greyscale mode.
  • OAM DMA: Fixed to copy all 256 bytes with proper address wrapping.
  • Controller strobe: Fixed strobe and post-8th-read behavior to match hardware (returns 1 after all 8 buttons are read).
  • DMC registers: Fixed $4013 length handling and DMC DMA bus hijacking interaction with unofficial SHx opcodes.
  • APU frame IRQ: Writing $4017 with bit 6 set prevents the frame interrupt flag from being set entirely, not just suppresses the IRQ.
  • BMI: Fixed page-crossing cycle penalty for the BMI instruction.
  • CPU flags: Fixed RTI and PLP to ignore bits 4 and 5 from the stack. Fixed B flag being incorrectly set during IRQ and NMI.
  • Zapper: Fixed $4017 zapper bits being incorrectly set when no zapper is connected.
  • 16-bit reads: Fixed load16bit boundary check when reading across RAM/mapper boundary.
  • JSR cycle order: Target high byte is now read after pushing the return address, matching real 6502 behavior.
  • CHR ROM protection: Writes to CHR ROM address space are now correctly ignored on mappers with ROM (not RAM) in that region.
  • Trainer ROMs: Fixed trainer data offset not being applied when loading ROMs with a 512-byte trainer.

AudioWorklet

The Browser class uses the modern AudioWorklet API instead of the deprecated ScriptProcessorNode. This provides better audio timing, lower latency, and forward compatibility with browsers that are removing ScriptProcessorNode. Audio is automatically resumed on first user interaction to comply with browser autoplay policies.

Performance

  • Typed arrays (~13% speedup): Internal memory buffers (VRAM, pattern tables, sprite RAM, etc.) converted from plain JavaScript arrays to Uint8Array/Int32Array, improving memory access performance.
  • Fast/slow memory access split (~11% speedup): Common WRAM and mapper reads take a fast path that skips PPU/APU register side-effect handling. The slow path with full side effects is only used for I/O regions.
  • Game Genie function pointers: ROM reads use a swappable function pointer instead of branching on every read to check for Game Genie patches.

API changes

New

  • jsnes.Browser class for easy web embedding (see above).
  • loadROM() accepts Uint8Array and ArrayBuffer in addition to binary strings. This makes it easy to load ROMs from fetch() responses or file input elements.
  • Game Genie: nes.addGameGenieCode(code) and nes.removeGameGenieCode(code) (see above).
  • Turbo buttons: Controller.BUTTON_TURBO_A and Controller.BUTTON_TURBO_B (see above).
  • TypeScript type definitions ship with the package (nes.d.ts, controller.d.ts, browser.d.ts).

Changed

  • preferredFrameRate removed from the NES constructor options. Use nes.setFramerate(rate) at runtime instead.

Bug fixes

  • Fixed infinite loop when the CPU encountered an invalid/unknown opcode. The emulator now halts with a crash status instead of hanging.
  • Fixed mapper not being reset after construction in loadROM(), which could cause state leaks between ROMs.

Don't miss a new jsnes release

NewReleases is sending notifications on new releases.