Introducing SEN

August 4, 20269 min read
On This PageIntroducing SEN6 sections

I’ve been working on writing a NES emulator for some time. Over recent years I made a couple attempts but failed because I was following tutorials that take an overly simplified approach. Finally I decided to try again with a more comprehensive approach.

The Goals

From the start I wanted to have an NES emulator core can be used easily in other projects. My requirements were:

  • The core can be used as a library
  • WASM bindings
  • Saving and loading of battery backed RAM
  • Save states
  • A libretro core so it can be used in RetroArch
  • Run-ahead and net-play support in RetroArch

The New Approach

Some of the goals listed above require a certain level of correctness and timing accuracy. Not all emulators are equally correct, it’s possible to have a somewhat inaccurate emulator that is able to run a good amount of games, but will fail on some.

The simplistic tutorials I followed previously, usually emulate a system that reads a byte at the program counter address, decodes the byte into an instruction, executes the instruction and then returns the amount of cycles the instruction took to execute. Then for every CPU cycle it took to execute an instruction, the system would run three PPU (Picture Processing Unit) cycles. This is a simplified approach that doesn’t take into account the actual hardware of the NES because observeable bus reads and writes happen at certain cycles and not at the end of an instruction and CPU and PPU execution should be interleaved, i.e. run one CPU cycle and then three PPU cycles.

My new approach should allows the system to run cycle by cycle. For this I chose to model instructions as microcode sequences where each microcode operation takes one cycle to execute. The execution state of the CPU is modeled as a state machine that can be in one of two states: Fetch or Exec. In the Exec state the CPU executes the next microcode operation in the current microcode sequence.

      +----------------------+
      |     CpuState::Fetch  |
      +----------------------+
                 |
                 | Read opcode from PC
                 | Increment PC
                 v
      +----------------------+
      | Decode opcode        |
      |                      |
      | Select its sequence  |
      | of micro-operations  |
      +----------------------+
                 |
                 v
+-----------------------------------+
| CpuState::Exec(cursor)            |
|                                   |
| cursor remembers:                 |
|                                   |
|   1. which sequence is running    |
|      Opcode, NMI, or IRQ          |
|   2. current position in sequence |
|   2. which micro-op comes next    |
+-----------------------------------+
                 |
                 | Execute one micro-op
                 v
      +----------------------+
      | Instruction done?    |
      +----------------------+
           |             |
          no            yes
           |             |
           |             v
           |      Return to Fetch
           v
  Advance cursor to the
  next micro-operation

For an LDA absolute instruction the cpu effectively executes the following sequence of microcode operations and opcode fetches:

Fetch opcode
    |
    v
Read address low byte
    |
    v
Read address high byte
    |
    v
Read value from address into A

Settling on this approach was the most important decision as it lays the groundwork necessary to implement the CPU and peripherals in a cycle accurate manner. However this work is not done yet. My emulator still fails the overwhelming tests from the AccuryCoin test suite (most fail because I haven’t implemented any unofficial instructions yet).

I won’t go into the all the details of the rest of the implementation here, because frankly it’s a lot of code and I don’t want to bore you. Instead here is a high level overview of the ownership and components of the project.

desktop                         libretro                        wasm
winit + pixels + CPAL     RetroArch/libretro API         JS/WASM API
|                                 |                                |
| input                           | input                    input |
+---------------------------------+--------------------------------+
                                  |
                                  | Nes::run_frame(InputFrame)
                                  v
+------------------------------------------------------------------+
|                              sen-core                            |
|                                                                  |
|  +------------------------------------------------------------+  |
|  | Nes                                                        |  |
|  |                                                            |  |
|  |  - Cpu                                                     |  |
|  |  - SchedulerPhase                                          |  |
|  |  - Frame: 256 x 240 RGB                                    |  |
|  |  - audio_samples: VecDeque<f32>                            |  |
|  |  - NesCpuBus                                               |  |
|  |                                                            |  |
|  |       +-------------------+    Bus::read / Bus::write      |  |
|  |       |       Cpu         | <--------------------------+   |  |  
|  |       |                   |                            |   |  |
|  |       | Fetch             |                            v   |  |
|  |       | Exec(Microcode)   |             +----------------+ |  |
|  |       +-------------------+             | NesCpuBus      | |  |
|  |                                         |                | |  |
|  |                                         | - system RAM   | |  |
|  |                                         | - PPU          | |  |
|  |                                         | - APU          | |  |
|  |                                         | - Cartridge    | |  |
|  |                                         | - controllers  | |  |
|  |                                         | - OAM DMA      | |  |
|  |                                         | - DMC DMA      | |  |
|  |                                         | - cycle count  | |  |
|  |                                         +---+---+----+---+ |  |
|  +---------------------------------------------|---|----|-----+  |
|            +-----------------------------------+   |    |        |
|            |                        +--------------+    |        |
|            |                        |                   |        |
|            v                        v                   v        |
|       +-----------+             +--------+          +---------+  |
|       |    PPU    |             |  APU   |          |Cartridge|  |
|       |           |             |        |          |         |  |
|       | pixels    |             | mixing |          | Board   |  |
|       | NMI       |             |  IRQ   |          | Mapper  |  |
|       | frame end |             |  DMC   |          | IRQ     |  |
|       +-----+-----+             +--------+          +----+----+  |
|             |                                            |       |
|             +--------------- PPU bus --------------------+       |
+------------------------------------------------------------------+
                                |
                   +------------+-------------+
                   |                          |
                   v                          v
            completed RGB frame         generated audio samples

WASM bindings

This turned out to be surprisingly easy. The sen-wasm crate is a thin wrapper around sen-core’s NES struct as well as a frame and audio buffer, and controller input. The implementation mostly delegates to the NES struct implementation and converts error types to JS values. See the full wrapper here.

wasm/src/lib.rs
#[wasm_bindgen]
pub struct Emulator {
    nes: Nes,
    rgba_frame: Vec<u8>,
    audio: Vec<f32>,
    controller1: ControllerButtons,
    controller2: ControllerButtons,
}
 
#[wasm_bindgen]
impl Emulator {
    #[wasm_bindgen(constructor)]
    pub fn new(rom: &[u8], sample_rate: f64) -> Result<Emulator, JsValue>;
 
    #[wasm_bindgen(js_name = runFrame)]
    pub fn run_frame(&mut self);
 
    #[wasm_bindgen(js_name = frameWidth)]
    pub fn frame_width(&self) -> usize;
 
    #[wasm_bindgen(js_name = frameHeight)]
    pub fn frame_height(&self) -> usize;
 
    #[wasm_bindgen(js_name = frameBuffer)]
    pub fn frame_buffer(&self) -> Uint8Array;
 
    #[wasm_bindgen(js_name = takeAudio)]
    pub fn take_audio(&mut self) -> Float32Array;
 
    pub fn set_controller1(&mut self, mask: u8);
 
    #[wasm_bindgen(js_name = setController2)]
    pub fn set_controller2(&mut self, mask: u8);
 
    pub fn reset(&mut self, rom: &[u8], sample_rate: f64) -> Result<(), JsValue>;
 
    #[wasm_bindgen(js_name = saveRam)]
    pub fn save_ram(&self) -> Result<Uint8Array, JsValue>;
 
    #[wasm_bindgen(js_name = loadSaveRam)]
    pub fn load_save_ram(&mut self, ram: &[u8]) -> Result<(), JsValue>;
 
    #[wasm_bindgen(js_name = saveState)]
    pub fn save_state(&self) -> Result<Uint8Array, JsValue>;
 
    #[wasm_bindgen(js_name = loadState)]
    pub fn load_state(&mut self, image: &[u8]) -> Result<(), JsValue>;
 
    #[wasm_bindgen(js_name = setGameGenieCodes)]
    pub fn set_game_genie_codes(&mut self, codes: Vec<String>) -> Result<(), JsValue>;
}

The WASM bindings are published as a NPM package sen-wasm and can be used in any JavaScript project. Docs for the API can be found here.

For building the NPM package I set up an empty NPM project with a package.json that invokes cargo build, wasm-bindgen and wasm-opt on build.

package.json
{
  "scripts": {
    "clean": "rimraf pkg",
    "build:rust": "cargo build -p sen-wasm --release --target wasm32-unknown-unknown",
    "build:bindgen": "wasm-bindgen --target web --out-dir pkg --out-name sen ../target/wasm32-unknown-unknown/release/sen_wasm.wasm",
    "build:opt": "wasm-opt -Os pkg/sen_bg.wasm -o pkg/sen_bg.wasm",
    "build": "run-s clean build:rust build:bindgen build:opt",
    "docs": "typedoc --options typedoc.json",
    "docs:build": "run-s clean build:rust build:bindgen docs",
    "prepack": "npm run build"
  }
}

Below you can try a web demo of the package. ROM files are loaded locally and never leave your browser.

Load a .nes ROM to start.

Arrows: D-pad · X: A · Z: B · Enter: Start · Shift: Select

The demo is a small Astro component built directly on the NPM package. Here is an excerpt of the component’s source file:

src/components/SenEmulator.astro
import initSen, { Emulator } from "@lukad/sen";
 
const FRAME_TIME = 1000 / 60.0988;
const CONTROLLER_KEYS = new Map([
  ["KeyX", 1 << 0],
  ["KeyZ", 1 << 1],
  ["ShiftLeft", 1 << 2],
  ["ShiftRight", 1 << 2],
  ["Enter", 1 << 3],
  ["ArrowUp", 1 << 4],
  ["ArrowDown", 1 << 5],
  ["ArrowLeft", 1 << 6],
  ["ArrowRight", 1 << 7],
]);
 
  private loadRom = async () => {
    const file = this.input.files?.[0];
    this.input.value = "";
    if (!file) return;
 
    this.status.textContent = "Loading...";
 
    try {
      this.audio ??= new AudioContext();
      await this.audio.resume();
      senReady ??= initSen();
      const [, buffer] = await Promise.all([senReady, file.arrayBuffer()]);
      const rom = new Uint8Array(buffer);
 
      this.clearAudio();
      if (this.emulator) this.emulator.reset(rom, this.audio.sampleRate);
      else this.emulator = new Emulator(rom, this.audio.sampleRate);
 
      this.rom = rom;
      this.image = this.context.createImageData(this.emulator.frameWidth(), this.emulator.frameHeight());
      this.pauseButton.disabled = false;
      this.resetButton.disabled = false;
      this.setPaused(false);
      this.draw();
      this.status.textContent = file.name;
      this.focus();
    } catch (error) {
      this.status.textContent = error instanceof Error ? error.message : String(error);
    }
  };
 
  private keyDown = (event: KeyboardEvent) => {
    const button = CONTROLLER_KEYS.get(event.code);
    if (button === undefined || !this.emulator || this.paused) return;
 
    event.preventDefault();
    this.controller |= button;
    this.emulator.setController1(this.controller);
  };
 
  private tick = (time: number) => {
    this.animationFrame = requestAnimationFrame(this.tick);
 
    if (!this.emulator || this.paused) {
      this.previousTime = time;
      return;
    }
 
    if (!this.previousTime) this.previousTime = time;
    this.elapsed += Math.min(time - this.previousTime, 100);
    this.previousTime = time;
 
    let frameReady = false;
    while (this.elapsed >= FRAME_TIME) {
      this.emulator.runFrame();
      this.playAudio(this.emulator.takeAudio());
      this.elapsed -= FRAME_TIME;
      frameReady = true;
    }
 
    if (frameReady) this.draw();
  };
 
  private draw() {
    if (!this.emulator || !this.image) return;
    this.image.data.set(this.emulator.frameBuffer());
    this.context.putImageData(this.image, 0, 0);
  }
 
  private playAudio(samples: Float32Array) {
    if (!this.audio || samples.length === 0) return;
 
    const buffer = this.audio.createBuffer(1, samples.length, this.audio.sampleRate);
    buffer.copyToChannel(Float32Array.from(samples), 0);
 
    const source = this.audio.createBufferSource();
    source.buffer = buffer;
    source.connect(this.audio.destination);
    source.addEventListener("ended", () => this.audioSources.delete(source), { once: true });
 
    this.audioTime = Math.max(this.audioTime, this.audio.currentTime + 0.02);
    source.start(this.audioTime);
    this.audioTime += buffer.duration;
    this.audioSources.add(source);
  }

Libretro core

The libretro core was substantially more work than the WASM bindings. With the WASM API, the host calls methods on an Emulator object. With libretro, RetroArch is the host: it loads the core as a dynamic library and calls it through a fixed interface.

I used the libretro-core crate, which exposes that interface as a Rust trait and generates the extern "C" functions expected by libretro. This avoided most of the FFI boilerplate, leaving the core itself as an adapter between RetroArch and SEN.

Before loading a game, the core describes its supported content, controllers, video geometry, frame rate and audio sample rate. RetroArch then calls Core::run once per frame. SEN reads both controller ports, emulates one frame, converts its RGB framebuffer to XRGB8888 and its mono floating-point audio to stereo 16-bit samples, then submits both to the frontend.

     RetroArch calls Core::run
                |
                v
     Poll both controller ports
                |
                v
     Nes::run_frame(InputFrame)
                |
        +-------+-------+
        |               |
        v               v
RGB -> XRGB8888    mono f32 -> stereo i16
        |               |
        +-------+-------+
                |
                v
Submit video and audio to RetroArch

Running a game is only the basic part of the integration. Features such as save states, rewinding, run-ahead and netplay require the core to serialize its complete state and restore it deterministically.

A save state therefore contains much more than CPU registers and RAM. It must preserve the current position within an instruction, scheduler phase, pending interrupts and DMA transfers, PPU and APU timing, and cartridge mapper state. Otherwise a restored machine may initially look correct but diverge a few cycles later.

The core also exposes battery-backed cartridge RAM for persistent saves, registers the NES memory map for features such as RetroAchievements, and provides options for overscan cropping, aspect ratio, controller behavior and audio gain. A small .info file accompanies the dynamic library and tells RetroArch how to identify and load it.

The payoff is that these features do not need their own SEN-specific frontend. RetroArch handles controller mapping, presentation, audio output, shaders, recording and save management, while SEN remains responsible for emulating the machine.

A Web frontend

The emulator embedded above is intentionally minimal, but I also wanted a frontend suitable for actually playing games. It should work without installation, support mobile devices and retain the conveniences expected from a native emulator.

I built sen.fnord.tech with TypeScript, SolidJS, Tailwind CSS and Vite. It uses the same public @lukad/sen package as the demo above. There are no private bindings or browser-specific changes to the emulator core.

The frontend supports remappable keyboard and gamepad controls, a virtual controller for mobile devices, an optional CRT shader, save-state management and Game Genie codes. Settings and states are stored locally in IndexedDB and can optionally be synchronized through the cloud via a small rust + sqlite backend.

Building it was also a useful test of the API. If a complete frontend can be implemented using only the published package, then the separation between sen-core, its WASM bindings and the surrounding application is working as intended.

Please try it out at sen.fnord.tech.

What’s next?

SEN has now reached the shape I wanted when I started. The emulator is a reusable Rust library with WASM bindings, a libretro core and deterministic save states. The same core can run inside RetroArch, a browser application or another frontend without taking ownership of input, display or storage.

It is far from finished though. Not all cartridge mappers are emulated yet, although the most used ones are. As mentioned above, unofficial instructions are not implemented yet and the emulator still fails most AccuracyCoin tests. But the architecture is sound and I think the missing pieces can be implemented without needing major rewrites. I’m happy with the current design and will continue to work on the emulator.