DEV Community

bokuweb
bokuweb

Posted on • Originally published at Medium on

13 1

Writing An NES Emulator with Rust and WebAssembly

I wrote the NES emulator with Rust and WebAssembly to learn Rust. It’s not perfect and have some audio bugs, but it’s good enough to play Super Mario bros.

TL;DR

Here is the source code. Also, you can play the game in the canvas below

rustynes

The Nintendo Entertainment System (NES)

The Nintendo Entertainment System (NES) was the world’s most widely used video games.

  • CPU 6502(RP2A03), 8bit 1.79MHz
  • PPU Picture Processing Unit RP2C02
  • ROM ProgramROM:32KiB + CharactorROM:8KiB
  • WRAM WorkingRAM 2KiB
  • VRAM VideoRAM 2KiB
  • Color 52color
  • Resolution 256x240pixles
  • Sound Square1/2, Triangle, Noise, DPCM
  • Controller Up, Down, Left, Right, A, B, Start, Select

I had to emulate the above specs with WebAssembly and browser features.

Emulator Structure

Building WebAssembly with Rust

I used wasm32-unknown-emscripten to convert Rust to WebAssembly. Because I did not have wasm32-unknown-unknown when I started this project, since there are now great libraries such as stdweb and wasm-bindgen with wasm32-unknown-unknown consider using them It might be good, too.

build:
cargo rustc — release \
— target=wasm32-unknown-emscripten — \
-C opt-level=3 \
-C link-args=”-O3 -s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS=[‘_run’]”
view raw makefile hosted with ❤ by GitHub

The most important of these are NO_EXIT_RUNTIME and EXPORTED_FUNCTIONS. NO_EXIT_RUNTIME is used to freeze the memory on the Rust side to use it from the JavaScript side. Without this setting, memory will be freed and unexpected behavior will occur.

EXPORTED_FUNCTIONS is used to specify the function to export to the Javascript side. Actually it is invoked from JavaScript side as follows.

bokuweb/rustynes

The Game loop

NES works at 60 FPS. It means that It is necessary to refresh the screen every 16 ms. So I used emscripten_set_main_loop for this. If 0 or negative value is used as the second argument, requestAnimationFrame will be used internally. (See. https://kripken.github.io/emscripten-site/docs/api_reference/emscripten.h.html#c.emscripten_set_main_loop)

I wanted to use closure so I struggled and finally wrote it as follows.

#[macro_use]
extern crate lazy_static;
extern crate libc;
mod nes;
mod externs;
use nes::Context;
fn main() {}
#[no_mangle]
pub fn run(len: usize, ptr: *mut u8) {
let buf: &mut [u8] = unsafe { std::slice::from_raw_parts_mut(ptr, len + 1) };
let mut ctx = Context::new(buf);
nes::reset(&mut ctx);
externs::cancel_main_loop();
let main_loop = || {
let key_state = buf[len - 1];
nes::run(&mut ctx, key_state);
};
externs::set_main_loop_callback(main_loop);
}
view raw run.rs hosted with ❤ by GitHub
use std::cell::RefCell;
use std::ptr::null_mut;
use std::os::raw::{c_int, c_void};
#[allow(non_camel_case_types)]
type em_callback_func = unsafe extern "C" fn();
thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(null_mut()));
extern "C" {
pub fn emscripten_cancel_main_loop();
pub fn emscripten_set_main_loop(func: em_callback_func,
fps: c_int,
simulate_infinite_loop: c_int);
}
pub fn cancel_main_loop() {
unsafe { emscripten_cancel_main_loop(); }
}
pub fn set_main_loop_callback<F>(callback: F)
where F: FnMut()
{
MAIN_LOOP_CALLBACK.with(|log| { *log.borrow_mut() = &callback as *const _ as *mut c_void; });
unsafe {
emscripten_set_main_loop(wrapper::<F>, 0, 1);
}
}
unsafe extern "C" fn wrapper<F>()
where F: FnMut()
{
MAIN_LOOP_CALLBACK.with(|z| {
let closure = *z.borrow_mut() as *mut F;
(*closure)();
});
}
view raw loop.rs hosted with ❤ by GitHub

The CPU

The NES used the MOS6502 (at 1.79 MHz) as its CPU. The 6502 is an 8bit microprocessor.The 6502 had relatively few registers (A, X & Y) and they were special-purpose registers.

Registers

The stack pointer needs to point to a 16bit address space, but the upper 8 bits are fixed to 0x01. 256 bytes are available for the stack( 0x0100 to 0x01FF) in WRAM is allocated. That is, if the stack pointer register is 0xA0, the stack pointer is 0x01A0.

Name Size Description
A 8bit Accumrator
X 8bit Index
Y 8bit Index
S 8bit Stack Pointer(SP)
P 8bit Status
PC 16bit Program Counter(PC)
view raw registeres.csv hosted with ❤ by GitHub

This is expressed as follows.

#[derive(Debug)]
struct Status {
negative: bool,
overflow: bool,
reserved: bool,
break_mode: bool,
decimal_mode: bool,
interrupt: bool,
zero: bool,
carry: bool,
}
#[allow(non_snake_case)]
#[derive(Debug)]
pub struct Registers {
A: u8,
X: u8,
Y: u8,
SP: u8,
PC: u16,
P: Status,
}
view raw register.rs hosted with ❤ by GitHub

Memory map

The Program ROM is 0x8000~, The WRAM is mapped from 0x0000~0x07FF, and the PPU register is mapped to 0x2000~.

Address Size Device
0x0000~0x07FF 0x0800 WRAM
0x0800~0x1FFF - WRAM(mirror)
0x2000~0x2007 0x0008 PPU Registers
0x2008~0x3FFF - PPU Registers(mirror)
0x4000~0x401F 0x0020 APU I/O、PAD
0x4020~0x5FFF 0x1FE0 exROM
0x6000~0x7FFF 0x2000 exRAM
0x8000~0xBFFF 0x4000 ProgramROM
0xC000~0xFFFF 0x4000 ProgramROM
view raw memorymap.csv hosted with ❤ by GitHub

How to emulate CPU

The 6502 does not have a pipeline structure like a recent CPU, and can be emulated simply by repeating fetching, decoding, and execution from Program ROM.

pub fn run<T: CpuRegisters + Debug, U: CpuBus>(registers: &mut T, bus: &mut U) -> Data {
let _code = fetch(registers, bus);
let ref map = opecode::MAP;
let code = &*map.get(&_code).unwrap();
let opeland = fetch_opeland(&code, registers, bus);
match code.name {
Instruction::LDA if code.mode == Addressing::Immediate => lda_imm(opeland, registers),
Instruction::LDA => lda(opeland, registers, bus),
Instruction::LDX if code.mode == Addressing::Immediate => ldx_imm(opeland, registers),
Instruction::LDX => ldx(opeland, registers, bus),
Instruction::LDY if code.mode == Addressing::Immediate => ldy_imm(opeland, registers),
// …
}
}
pub fn lda<T: CpuRegisters, U: CpuBus>(opeland: Word, registers: &mut T, bus: &mut U) {
let computed = bus.read(opeland);
registers
.set_A(computed)
.update_negative_by(computed)
.update_zero_by(computed);
}
// Other instructions…
view raw cpu.rs hosted with ❤ by GitHub

In addition, the opcode dictionary is created using lazy_static. That is a very good library.

lazy_static! {
pub static ref MAP: HashMap<u8, Opecode> = {
let mut m = HashMap::new();
m.insert(0xA9, Opecode { name: Instruction::LDA, mode: Addressing::Immediate, cycle: cycles[0xA9] });
m.insert(0xA5, Opecode { name: Instruction::LDA, mode: Addressing::ZeroPage, cycle: cycles[0xA5] });
m.insert(0xB5, Opecode { name: Instruction::LDA, mode: Addressing::ZeroPageX, cycle: cycles[0xB5] });
m.insert(0xAD, Opecode { name: Instruction::LDA, mode: Addressing::Absolute, cycle: cycles[0xAD] });
// ...
}
}
view raw map.rs hosted with ❤ by GitHub

The PPU (Picture Processing Unit)

The PPU reads the sprite information from the cartridge and constructs the screen. So the data bus of the PPU is directly connected to the cartridge.

Sprites are 8 x 8 or 8 x16 pixels as follows, PPU places sprites based on data set in VRAM. (Below is the output of Super Mario Bros. sprite data).

Please refer to the following article for details about the NES graphic.

NES Graphics - Part 1

After generating data for one screen from VRAM data and sprite information, I emulated game screen by drawing on Canvas.

mod color;
use super::{BackgroundField, BackgroundCtx};
use super::PaletteList;
use super::{Sprite, SpritesWithCtx, SpritePosition};
use self::color::COLORS;
extern "C" {
fn canvas_render(ptr: *const u8, len: usize);
}
#[derive(Debug)]
pub struct Renderer {
buf: Vec<u8>,
}
impl Renderer {
pub fn new() -> Self {
Renderer { buf: vec![0xFF; 256 * 224 * 4] }
}
pub fn render(&mut self, background: &BackgroundField, sprites: &SpritesWithCtx) {
self.render_background(background);
self.render_sprites(sprites, background);
unsafe {
canvas_render(self.buf.as_ptr(), self.buf.len());
}
}
fn should_pixel_hide(&self, x: usize, y: usize, background: &BackgroundField) -> bool {
let tile_x = x / 8;
let tile_y = y / 8;
let background_index = tile_y * 33 + tile_x;
let sprite = &background[background_index];
// NOTE: If background pixel is not transparent, we need to hide sprite.
(sprite.tile.sprite[y % 8][x % 8] % 4) != 0
}
fn render_background(&mut self, background: &BackgroundField) {
for (i, bg) in background.into_iter().enumerate() {
if bg.is_enabled {
let x = (i % 33) * 8;
let y = (i / 33) * 8;
self.render_tile(bg, x, y);
}
}
}
fn render_sprites(&mut self, sprites: &SpritesWithCtx, background: &BackgroundField) {
for sprite in sprites {
self.render_sprite(&sprite.sprite,
&sprite.position,
&sprite.palette,
sprite.attr,
&background);
}
}
fn render_sprite(&mut self,
sprite: &Sprite,
position: &SpritePosition,
palette: &PaletteList,
attr: u8,
background: &BackgroundField) {
let is_vertical_reverse = (attr & 0x80) == 0x80;
let is_horizontal_reverse = (attr & 0x40) == 0x40;
let is_low_priority = (attr & 0x20) == 0x20;
for i in 0..8 {
for j in 0..8 {
let x = position.0 as usize + if is_horizontal_reverse { 7 - j } else { j };
let y = position.1 as usize + if is_vertical_reverse { 7 - i } else { i };
if is_low_priority && self.should_pixel_hide(x, y, background) {
continue;
}
if sprite[i][j] != 0 {
let color_id = palette[sprite[i][j] as usize];
let color = COLORS[color_id as usize];
let index = (x + (y * 0x100)) * 4;
self.buf[index] = color.0;
self.buf[index + 1] = color.1;
self.buf[index + 2] = color.2;
if x < 8 {
self.buf[index + 3] = 0;
}
}
}
}
}
fn render_tile(&mut self, bg: &BackgroundCtx, x: usize, y: usize) {
let offset_x = (bg.scroll_x % 8) as i32;
let offset_y = (bg.scroll_y % 8) as i32;
for i in 0..8 {
for j in 0..8 {
let x = (x + j) as i32 - offset_x;
let y = (y + i) as i32 - offset_y;
if x >= 0 as i32 && 0xFF >= x && y >= 0 as i32 && y < 224 {
let color_id = bg.tile.palette[bg.tile.sprite[i][j] as usize];
let color = COLORS[color_id as usize];
let index = ((x + (y * 0x100)) * 4) as usize;
self.buf[index] = color.0;
self.buf[index + 1] = color.1;
self.buf[index + 2] = color.2;
if x < 8 {
self.buf[index + 3] = 0;
}
}
}
}
}
}
view raw renderer.rs hosted with ❤ by GitHub

canvas_render is Javascript side code. If you are using emscriptenyou will be able to call on the Rust side via mergeInto.

mergeInto(LibraryManager.library, {
canvas_render: function (ptr, len) {
Module.NES.buf = new Uint8Array(Module.HEAPU8.buffer, ptr, len);
Module.NES.image.data.set(Module.NES.buf);
Module.NES.ctx.putImageData(Module.NES.image, 0, 0);
}
}

The game Pad

The game pad emulated using keydownEvent. Specifically, the following handlers are registered at initialization, and specific bytes of ArrayBuffer are written at keyDown / keyUp. This is because, from the viewpoint of Browser, the memory on the Rust side can be handled as ArrayBuffer.

const convertKeyCode = (keyCode) => {
switch (keyCode) {
case 88: return 0x01; // X A
case 90: return 0x02; // Z B
case 65: return 0x04; // A SELECT
case 83: return 0x08; // S START
case 38: return 0x10; // ↑ ↑
case 40: return 0x20; // ↓ ↓
case 37: return 0x40; // ← ←
case 39: return 0x80; // → →
}
};
const onKeydown = (e) => {
buf[size - 1] |= convertKeyCode(e.keyCode);
}
const onKeyup = (e) => {
buf[size - 1] &= ~convertKeyCode(event.keyCode);
}
const setupKeyHandler = () => {
if (typeof window !== 'undefined') {
document.addEventListener('keydown', onKeydown);
document.addEventListener('keyup', onKeyup);
}
};
view raw key.js hosted with ❤ by GitHub

The Sound

Just like Canvas, we used mergeInto to invoke Javascript code using WebAudio API from Rust side.

mergeInto(LibraryManager.library, {
start_oscillator: function (index) {
Module.NES.oscs[index].start();
},
stop_oscillator: function (index) {
Module.NES.oscs[index].stop();
},
close_oscillator: function (index) {
Module.NES.oscs[index].close();
},
set_oscillator_frequency: function (index, freq) {
Module.NES.oscs[index].setFrequency(freq);
},
change_oscillator_frequency: function (index, freq) {
Module.NES.oscs[index].changeFrequency(freq);
},
set_oscillator_volume: function (index, volume) {
Module.NES.oscs[index].setVolume(volume);
},
set_oscillator_pulse_width: function (index, width) {
Module.NES.oscs[index].setPulseWidth(width);
},
set_noise_frequency: function (freq) {
Module.NES.noise.setFrequency(freq);
},
change_noise_frequency: function (freq) {
Module.NES.noise.changeFrequency(freq);
},
set_noise_volume: function (volume) {
Module.NES.noise.setVolume(volume);
},
close_noise: function () {
Module.NES.noise.close();
},
stop_noise: function () {
Module.NES.noise.stop();
},
start_noise: function () {
Module.NES.noise.start();
}
}
view raw merge.js hosted with ❤ by GitHub

As an example, the waveform is generated using the WebAudio API as follows

import pulse from './pulse.js';
export default class Oscillator {
constructor(type) {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext
this.context = new AudioContext();
} catch (e) {
throw new Error('Web Audio isn\'t supported in this browser!');
}
this.type = type || 'square';
this.oscillator = this.createOscillator({ kind: this.type });
this.waves = {
'0.125': this.context.createPeriodicWave(pulse['0.125'].real, pulse['0.125'].imag),
'0.25': this.context.createPeriodicWave(pulse['0.25'].real, pulse['0.25'].imag),
'0.5': this.context.createPeriodicWave(pulse['0.5'].real, pulse['0.5'].imag),
'0.75': this.context.createPeriodicWave(pulse['0.75'].real, pulse['0.75'].imag),
};
this.setVolume(0);
this.setPulseWidth(0.5);
this.playing = false;
}
start() {
if (this.playing) {
this.stop();
}
this.playing = true;
this.oscillator.start(0);
}
stop() {
if (this.playing) {
this.setVolume(0);
this.playing = false;
this.oscillator.stop(this.context.currentTime);
this.oscillator = this.createOscillator();
this.setPulseWidth(0.5);
}
}
close() {
this.context.close();
}
createOscillator(options = {}) {
const oscillator = this.context.createOscillator();
if (options.kind) oscillator.type = options.kind;
if (options.frequency) oscillator.frequency.value = options.frequency;
if (options.harmonics) {
const waveform = this.context.createPeriodicWave(
new Float32Array(options.harmonics.real),
new Float32Array(options.harmonics.imag)
)
oscillator.setPeriodicWave(waveform);
}
this.gain = this.context.createGain();
this.gain.gain.value = 0.01;
oscillator.connect(this.gain);
this.gain.connect(this.context.destination);
return oscillator;
}
setPulseWidth(pulseWidth) {
this.oscillator.setPeriodicWave(this.waves[`${pulseWidth}`]);
}
setFrequency(frequency) {
this.oscillator.frequency.value = frequency;
}
changeFrequency(frequency) {
this.oscillator.frequency.setValueAtTime(frequency, this.context.currentTime)
}
setVolume(volume) {
volume = Math.max(0, Math.min(1, volume));
this.gain.gain.value = volume;
}
}
view raw audio.js hosted with ❤ by GitHub

Although we omitted it considerably, we implemented NES Emulator with Rust and WebAssembly like this. The whole code please see the following repositry.

bokuweb/rustynes

If you want to know deeply, you may want to look at the following.

Conclusions

I’ve been really impressed with Rust, and I think that it is one very good choice for building on the WebAssembly. A framework for an advanced browser front end like yew has also been developed and I think that it is also a remarkable language for developers who usually write Javascript.

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay