2 releases

0.1.1 Sep 10, 2023
0.1.0 Sep 8, 2023

#47 in Games

31 downloads per month

MPL-2.0 license

5.5MB
7K SLoC

Gameboy emulator written in Rust to WebAssembly

Disclaimer 1.0: This project does not endorse or promote any type of piracy activity. The act of build or install an emulator is not illegal. As many others emulators that have been created over past decades, this project is just a study case.

Disclaimer 1.1: Altough the emulator already runs a good amount of games, it is still under development and you can track it development by checking Tests section.

WebGL with WebAssembly Desktop GUI with Rust
Demo Web with Assembly First demo desktop GUI Second demo desktop GUI
Playing Cookies Bakery by leafthief Playing Unearthed by benjelter
Firefox 116.0.3 (64-bit) macOS 11.6.2, 16GB, Quad-Core Intel Core i7, 1,2 GHz
make web make desktop

WebAssembly quick demo

WebAssembly quick demo

Internals

GameBoy Internals GameBoy Internals

Emulators and a bit of Game Boy history...

An emulator typically enables the host system to run software or use peripheral devices designed for the guest system. Emulation refers to the ability of a computer program in an electronic device to emulate (or imitate) another program or device. In the case of the Game Boy most of the work involves dealing with 8-bit buses around (a variant of) the Z80 CPU. The Game Boy CPU is a hybrid between the Intel 8080 and the Zilog Z80. In computing, an emulator is hardware or software that enables one computer system (called the host) to behave like another computer system (called the guest).

About the Z80...

Zilog Z80

The Z80 is an 8-bit microprocessor introduced by Zilog as the startup company's first product. The Z80 was conceived by Federico Faggin in late 1974 and developed by him and his 11 employees starting in early 1975. The first working samples were delivered in March 1976, and it was officially introduced on the market in July 1976. With the revenue from the Z80, the company built its own chip factories and grew to over a thousand employees over the following two years.

That means manipulating a lot of individual bytes, especially while navigating through huge banks of ROM and RAM. The Game Boy is a pretty simple architecture — getting button input requires reading specific memory addresses, writing pixels to the screen involves pushing bytes to specific places in VRAM.

CPU          - 8-bit (Similar to the Z80 processor)
Clock Speed  - 4.194304MHz (4.295454MHz for SGB, max. 8.4MHz for CGB)
Work RAM     - 8K Byte (32K Byte for CGB)
Video RAM    - 8K Byte (16K Byte for CGB)
Screen Size  - 2.6"
Resolution   - 160x144 (20x18 tiles)
Max sprites  - Max 40 per screen, 10 per line
Sprite sizes - 8x8 or 8x16
Palettes     - 1x4 BG, 2x3 OBJ (for CGB: 8x4 BG, 8x3 OBJ)
Colors       - 4 grayshades (32768 colors for CGB)
Horiz Sync   - 9198 KHz (9420 KHz for SGB)
Vert Sync    - 59.73 Hz (61.17 Hz for SGB)
Sound        - 4 channels with stereo sound
Power        - DC6V 0.7W (DC3V 0.7W for GB Pocket, DC3V 0.6W for CGB)

The Game Boy has four operation buttons labeled "A", "B", "SELECT", and "START", and a directional pad (d-pad). There is a volume control dial on the right side of the device and a similar dial on the left side to adjust the contrast. At the top of the Game Boy, a sliding on-off switch and the slot for the Game Boy cartridges are located. The on-off switch includes a physical lockout to prevent users from either inserting or removing a cartridge while the unit is switched on. Nintendo recommends users leave a cartridge in the slot to prevent dust and dirt from entering the system.

pub enum Button {
    A,
    B,
    Left,
    Right,
    Up,
    Down,
    Start,
    Select,
}

The Game Boy contains optional input or output connectors. On the left side of the system is an external 3.5 mm × 1.35 mm DC power supply jack that allows users to use an external rechargeable battery pack or AC adapter (sold separately) instead of four AA batteries. The Game Boy requires 6 V DC of at least 150 mA. A 3.5 mm stereo headphone jack is located on the bottom side of the unit which allows users to listen to the audio with the bundled headphones or external speakers.

The right side of the device offers a port that allows a user to connect to another Game Boy system via a link cable, provided both users are playing games that support connecting to each other (most often, only copies of the same game, although for example, the Pokémon games can connect between different generations). The port can also be used to connect a Game Boy Printer. The link cable was originally designed for players to play head-to-head two-player games such as in Tetris. However, game developer Satoshi Tajiri later used the link cable technology as a method of communication and networking in the popular Pokémon video game series.

Anyway, enough about the Game Boy history.

The Z80 was designed to be binary compatible with the already existing Intel 8080. This means that the instruction set found in the 8080 was also implemented by the Z80 (in essence, the 8080 can be seen as a subset of the Z80). The Game Boy’s custom hybrid chip official name is Sharp LR35902.

LR35902 ~ High level architecture

CPU (src/cpu)

A central processing unit (CPU), also called a central processor, main processor or just processor, is the electronic circuitry that executes instructions comprising a computer program. The CPU performs basic arithmetic, logic, controlling, and input/output (I/O) operations specified by the instructions in the program. This contrasts with external components such as main memory and I/O circuitry, and specialized processors such as graphics processing units (GPUs).

The Game Boy CPU is composed of 8 different "registers". Registers are responsible for holding on to little pieces of data that the CPU can manipulate when it executes various instructions. The Game Boy's CPU is an 8-Bit CPU, meaning that each of its registers can hold 8 bits (1 byte) of data. The CPU has 8 different registers labled as a, b, c, d, e, f, h, l.

Each of them is one byte, so each one can hold a value from 0 to 255. An u8 can do that job for us, since it can store between 0 and (2^8 - 1)

$$ u8PossibleValues=(2^8) $$

assert_eq!(u8::MIN, 0);
assert_eq!(u8::MAX, 255);

Example below:

pub struct Registers {
    pub a: u8,
    pub b: u8,
    pub c: u8,
    pub d: u8,
    pub e: u8,
    pub h: u8,
    pub l: u8
}

If there are 256 possible values in the first byte of an instruction, that makes for 256 possible instructions in the basic table. That table is detailed in the Gameboy Z80 opcode map: http://imrannazar.com/Gameboy-Z80-Opcode-Map.

Memory (src/mmu)

MMU map

The Game Boy has a 16-bit address-BUS. Giving an addressable range of 65535 Bytes (or 64kB). All hardware components of the Game Boy including RAM, ROM, Video RAM and I /O Ports are memory mapped as shown below.

Memory map
64kB
Memory map:
  • 0x0000 - 0x00FF: Boot ROM
  • 0x0000 - 0x3FFF: Game ROM Bank 0
  • 0x4000 - 0x7FFF: Game ROM Bank N
  • 0x8000 - 0x97FF: Tile RAM
  • 0x9800 - 0x9FFF: Background Map
  • 0xA000 - 0xBFFF: Cartridge RAM
  • 0xC000 - 0xDFFF: Working RAM
  • 0xE000 - 0xFDFF: Echo RAM
  • 0xFE00 - 0xFE9F: OAM (Object Atribute Memory)
  • 0xFEA0 - 0xFEFF: Unused
  • 0xFF00 - 0xFF7F: I/O Registers
  • 0xFF80 - 0xFFFE: High RAM Area
  • 0xFFFF: Interrupt Enabled Register

More information: https://rylev.github.io/DMG-01/public/book/memory_map.html

You can set the map the initial memory struct with something like this:

fn power_on(&mut self) {
    self.write_byte(0xFF05, 0);
    self.write_byte(0xFF06, 0);
    self.write_byte(0xFF07, 0);
    self.write_byte(0xFF10, 0x80);
    self.write_byte(0xFF11, 0xBF);
    self.write_byte(0xFF12, 0xF3);
    self.write_byte(0xFF14, 0xBF);
    self.write_byte(0xFF16, 0x3F);
    self.write_byte(0xFF16, 0x3F);
    self.write_byte(0xFF17, 0);
    self.write_byte(0xFF19, 0xBF);
    self.write_byte(0xFF1A, 0x7F);
    self.write_byte(0xFF1B, 0xFF);
    self.write_byte(0xFF1C, 0x9F);
    self.write_byte(0xFF1E, 0xFF);
    self.write_byte(0xFF20, 0xFF);
    self.write_byte(0xFF21, 0);
    self.write_byte(0xFF22, 0);
    self.write_byte(0xFF23, 0xBF);
    self.write_byte(0xFF24, 0x77);
    self.write_byte(0xFF25, 0xF3);
    self.write_byte(0xFF26, 0xF1);
    self.write_byte(0xFF40, 0x91);
    self.write_byte(0xFF42, 0);
    self.write_byte(0xFF43, 0);
    self.write_byte(0xFF45, 0);
    self.write_byte(0xFF47, 0xFC);
    self.write_byte(0xFF48, 0xFF);
    self.write_byte(0xFF49, 0xFF);
    self.write_byte(0xFF4A, 0);
    self.write_byte(0xFF4B, 0);
}

GPU (src/gpu)

The screen resolution of the original Game Boy is 160×144 pixels:

pub const WIDTH: usize = 160;
pub const HEIGHT: usize = 144;

https://github.com/raphamorim/gameboy/blob/main/src/gb.rs#L3-L4

[...] TODO: write here more about the GPU.

Usage examples

Web ~ JavaScript/WASM usage:

  1. Install lr35902 with Yarn or NPM:
npm install lr35902
# yarn install lr35902
  1. There you go:
import { render } from 'lr35902';

const romFile = document.querySelector('#rom-file');
romFile.addEventListener('change', function readFile() {
  loadRom(this.files[0]);
});

async function loadRom(file) {
  const { size, name } = file;
  const arrayBuffer = await file.arrayBuffer();
  const u8View = new Uint8Array(arrayBuffer);
  render(rom);
}

Desktop ~ Rust usage:

tl;dr: You can see the destop example in the example folder (/examples/desktop)

  1. Add lr35902 as dependency and make sure to have desktop as feature on:
[dependencies]
gameboy = { version = "0.1.0", features = ["desktop"] }
  1. There you go:

Default scale is 1 (160x144).

use gameboy::gameboy::{Gameboy, RenderMode::Desktop};

fn main() {
    let mut gb = Gameboy::new();
    match gb.load_rom("./sample-rom.gb") {
        Ok(..) => {
            gb.render(Desktop);
        }
        Err(err) => {
            println!("{:?}", err);
        }
    }
}

Tests

The tests are based on Blargg's Gameboy hardware test ROMs.

Link to the repository: https://github.com/retrio/gb-test-roms.git

Tests (originally ran in Openemu) LR35902 Status
CPU instructions test
Bits Unused test Failing
Halt Bug test Failing
Bits bank1 test Failing
GGB Sound test Failing

Resources & References

Dependencies