Rust-Pinky: Pinky — An NES emulator written in Rust

Build Status


Pinky is an NES emulator written in Rust completely from scratch based only on publicly available documentation.

You can run it in your Web browser!


  • Accurate-ish (cycle accurate) 6502, PPU and APU emulation.
  • A testsuite based on test ROMs.
  • A PPU testsuite automatically generated from a transistor-level simulation of a real PPU.
  • Supports NROM (0), MMC1 (1), UxROM (2), AxROM (7) and UNROM 512 (30) mappers.
  • Can be compiled as a libretro core.
  • Can be compiled into WebAssembly.

There are still many things missing, including:

  • Most unofficial 6502 instructions.
  • Support for other mappers.
  • Accurate PPU sprite overflow.
  • Savestate support.
  • PAL support.

Currently this is not a production quality emulator, though whatever games it can play (due to limited mapper support) it can play quite well (e.g. such games as Super Mario Brothers, Donkey Kong or Tetris; you can check out nesmapper.txt which will tell you which game use which mapper).

Getting started

Internally this project is split into multiple crates.

The pinky-libretro contains the libretro core of this emulator, which is the intended way to run it. It should be compatible with any libretro frontend, but it was only tested with RetroArch.

To compile the libretro core go into the pinky-libretro directory and type:

cargo build

This should build a shared object in target/debug called (on non-Linux systems the extension might be different, e.g. on Windows it'll be a .dll) which then you can use with RetroArch like this:

retroarch -L your_rom.nes

You can also run cargo build --release to build a significantly better optimized version (the debug build should run full speed on modern systems though).

There's also a simple standalone SDL2-based frontend in the pinky-devui directory; running it is just a matter of passing it a path to your game ROM on the command line.

The nes-testsuite contains an emulator agnostic testsuite of NES roms, which could be easily hooked to any other emulator simply by implementing a single trait (see nes/src/

The rp2c02-testsuite contains a PPU testsuite which is autogenerated with the help of Visual2C02, which is a transistor-level simulator of an actual NES PPU.

The nes contains the emulator itself. mos6502 has the 6502 interpreter, which could be useful for emulating other 6502-based machines.

There are already hundreds of NES emulators out there; why another?

Because why not? Writing a game console emulator is one of the most fun and rewarding projects out there, and nothing can compare with the feeling of beating one of your favorite games on an emulator you've wrote yourself.

The choice of NES is also an obvious one - it's the least time consuming console to emulate simply due to the fact that it's extremely well documented.


  • Fix compiler warnings
    Fix compiler warnings

    Oct 3, 2019

  • Update to 2018 edition
    Update to 2018 edition

    Oct 5, 2019

    Would this be something you'd consider merging a PR for?

  • Add BNROM (#34)
    Add BNROM (#34)

    Jan 31, 2020

    "Unable to load ROM - unhandled mapper: 34"

    Lack of BNROM support affects Deadly Towers, Haunted: Halloween '85, Lizard, and 240p Test Suite (BNROM version) (pinobatch/240p-test-mini). I was lead developer on HH85 and 240p and can help explain the mapper if you need.

  • Audio is a pop-fest
    Audio is a pop-fest

    Jan 31, 2020

    Audio is a pop-fest, as if random values were written to $4011 several times per second, and games play too slow. This is in Firefox 72.0.2 (64-bit) on Xubuntu 18.04 on a Dell Inspiron 11-3168 laptop with a quad-core Pentium N3710 CPU, running on AC power.

  • Backward A and B Buttons
    Backward A and B Buttons

    Jan 31, 2020

    A on the left and B on the right is backward from how many NES games map their controls. Affects Lode Runner, Championship Lode Runner, and Thwaite. (I was lead programmer for Thwaite.)

  • Add Action 53
    Add Action 53

    Jan 31, 2020

    Perhaps the easiest way to pump up the number of games you can (legally) offer through the demo would be to add support for the Action 53 mapper (iNES mapper 28). This mapper is about as complex as MMC1 (iNES mapper 1) and makes the emulator compatible with Action 53 Volume 4, which contains over 50 homebrew games.

  • Unable to play NESMaker games
    Unable to play NESMaker games

    Aug 25, 2018

    I tried to play the demo NES game from NESMaker but it fails saying

    Unable to load ROM - unhandled mapper: 30
    Sorry about that! Maybe try another ROM?

    It would be amazing if this emulator could run NESMaker games!

  • pinky-web stuck at
    pinky-web stuck at "Loading..." when compiled locally

    Feb 3, 2018

    I wanted to poke at pinky-web on my own machine, so I cloned master and followed the native WebAssembly backend instructions in the Readme. The exact nightly I downloaded was rustc 1.25.0-nightly (616b66dca 2018-02-02).

    The local server started up just fine, but if I go to localhost:8000 in a browser that ran the version at without problems, then I see the description on the left as expected, but on the right I see just "Loading..." instead of the ROM selection menu.

    If I open the console on the local version, I see only the single line

    Finished loading Rust wasm module 'pinky_web'

    instead of the two lines

    WebGL support using context: webgl
    Finished loading Rust wasm module 'pinky_web'

    I see at .

    So I suppose something is going wrong with the wasm loading somehow?

  • Publish to npm?
    Publish to npm?

    Nov 28, 2018

    It would be great to have pinky's wasm bundle published at npm. Then web developers could import and use it directly

  • Fix silencing of noise channel when envelope decay is on
    Fix silencing of noise channel when envelope decay is on

    Jan 19, 2018

    The noise channel is silent any time the envelope decay bit (i.e. bit 4 of $400C) is set to 0.

    When bit 4 is 0, the envelope decay is disabled, so is_manually_controlled is set to false, and thus the volume of the noise channel gets set to self.generated_volume in VolumeGenerator's volume() function. The problem is, self.generated_volume is never updated, and so it stays at the default volume of 0, silencing the channel.

    As far as I can tell, the noise channel is supposed to work exactly the same as the square channels when it comes to the volume register (apart from duty bits), so my proposed fix is to duplicate the function used to update the square channels' volume generator, and update the noise channel along with the squares in clock_sequencer().

  • Fixed silencing of square 1 channel when sweep is disabled
    Fixed silencing of square 1 channel when sweep is disabled

    Jan 18, 2018

    When the sweep is disabled, but the negate flag is set (%00001000 written to $4001), the frequency_generator_output() function shouldn't be called. In fact, because the last 3 bits are set to 0, the adjust_period_based_on_frequency_generator() function doesn't end up calling it.

    But, it's being called to check if the square channel should be silenced: !self.enabled || self.period < 8 || self.length_counter == 0 || self.frequency_generator_output() >= 0x7FF

    When frequency_generator_output() is called here (with the sweep disabled), self.period is shifted by 0 bits, so period_delta = self.period. Thus self.period.wrapping_sub( period_delta ).wrapping_sub( 1 ) = (0-1), underflowing to 0xFFFF which is indeed greater than 0x7FF. So the channel is silenced.

    Added an additional check to see if the sweep is enabled before checking its state.

  • missing information on how to start
    missing information on how to start

    Oct 30, 2016

    I saw your project on the changelog weekly newsletter and wanted to try it out. But now I have no idea how to start. I downloaded retroarch but there's no cli coming with it. Can you flesh this out in your documentation?