Intro to Emulators: Building a CHIP-8 emulator

EmulatorsCSharpBlazor

In the simplest terms an emulator is hardware or software that enables one computer system to behave like another computer system.

Sort of the "Hello World" program of emulator development is creating a CHIP-8 emulator (opens in a new tab)

CHIP-8 is an interpreted programming language that was originally created to allow video games to be more easily programmed for the microcomputers available. Games such as Pong, Pac-Man, Tetris and many others have been ported and will run on a CHIP-8 virtual machine.

The great thing about CHIP-8 is it has a very simple specification (opens in a new tab) including only 36 instructions which allows it to be easily learned and programmed within a day. This is a great starting point for just getting a simple understanding of how emulators work and will provide the foundation to move on to more complicated machines.

It is also a great way to become familiar with hex, binary and bitwise operations and why they are useful especially if you are someone like myself whose day to day job lives in higher levels of abstraction.

I will be using C# with the Blazor framework (opens in a new tab) which gives the ability to write C# apps in the browser via web assembly. I also found a package Blazorex (opens in a new tab) which is a simple wrapper around HTML canvas that includes everything needed.

Initializing the project

Initiate a new empty blazor wasm project

mkdir CHIP_8 && cd CHIP_8
dotnet new blazorwasm-empty

Install the Blazorex package

dotnet add package Blazorex --version 1.0.2

Per the Blazorex README add the following to index.html

<head>
    <!-- other tags... -->
    <link href="_content/Blazorex/blazorex.css" rel="stylesheet" />
</head>

Index.razor

We will start by initializng our display which will run in a web browser. To do this we will be using a CanvasManager from Blazorex. CanvasManager is a wrapper with a simple API that allows us to define the characteristics and behaviors of the display.

@page "/"
@using Blazorex;
 
<CanvasManager @ref="_canvasManager" />
 
@code
{
    CanvasManager _canvasManager;
 
    static int scale = 15;
    int _width = (64 * scale);
    int _height = (32 * scale);
 
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // The canvas should only be created once.
        if (!firstRender)
        {
            return;
        }
 
        _canvasManager.CreateCanvas("main", new CanvasCreationOptions()
        {
            Hidden = false,
            Width = _width,
            Height = _height,
            OnCanvasReady = OnMainCanvasReady,
            OnFrameReady = OnMainFrameReady,
            OnKeyDown = OnKeyDown,
            OnKeyUp = OnKeyUp,
        });
    }
 
    private async void OnMainCanvasReady(CanvasBase canvasBase)
    {
    }
 
    private void OnMainFrameReady(float timeStamp)
    {
    }
 
    private void OnKeyUp(int index)
    {
    }
 
    private void OnKeyDown(int index)
    {
    }
}

I set some properties including a scale, the height, and width. CHIP-8 is expected to run in a 64x32 pixel resolution at a refresh rate of 60Hz. A scale property is used to make this display larger or smaller depending on the size of your screen.

Next I created the HTML canvas with the CanvasManager passing in and defining some methods that will be needed. These will be implemented shortly.

Running the code at this point should display a blank black screen in the browser.

Processor.cs

Onto the creation of the actual CHIP-8 virtual machine. Looking at the spec we will need to define some properties.

The chip at contains

We need to represent the Pixels that are displayed. To do this I will use a boolean array of length 2048 (64 x 32).

We also need a way to represent key presses. In the CHIP-8 spec there are 16 keys used defined in bytes from 1 to F. For this emulator we will use a boolean array of length 16. A value of true means that the key at that index is currently pressed down.

public class Processor
{
    private readonly ushort MEMORY_START = 0x200;
    private byte[] Memory { get; set; }
    private ushort PC { get; set; }
    private byte[] V { get; set; }
    private ushort I { get; set; }
    private int DelayTimer { get; set; }
    private int SoundTimer { get; set; }
    private ushort[] Stack { get; set; }
    private byte SP { get; set; }
    internal bool[] Pixels { get; set; }
    private bool[] KeysDown { get; set; }
 
    public Processor()
    {
        Pixels = new bool[2048];
 
        Memory = new byte[4096];
        PC = MEMORY_START;
 
        V = new byte[16];
        I = 0;
 
        Stack = new ushort[16];
        SP = 0;
 
        KeysDown = new bool[16];
 
        DelayTimer = 0;
        SoundTimer = 0;
    }
}

The first task that needs to be done is to load the ROM file into the CHIP-8 memory. This is simply copying the ROM data into the Memory array starting at 0x200 (skipping the first 512 bytes as deinfed in the spec)

public void LoadRom(byte[] romData)
{
    Array.Copy(romData, 0, Memory, MEMORY_START, romData.Length);
}

We also need to load fonts into memory.

CHIP-8 draws graphics on screen using sprites. A sprite is a group of bytes which are a binary representation of the desired image. CHIP-8 sprites may be up to 15 bytes, for a possible sprite size of 8x15.

Programs may also refer to a group of sprites representing the hexadecimal digits 1 through F. These sprites are 5 bytes long, or 8x5 pixels. The data for these sprites should be stored in the reserved interpreter area of CHIP-8 memory or in the first 512 bytes. For this implementation I will hard code these bytes and load them into memory in the constructor.

public Processor()
{
    // other code
    LoadFonts();
}
 
private void LoadFonts()
{
    var fonts = new byte[]
    {
        0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
        0x20, 0x60, 0x20, 0x20, 0x70, // 1
        0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
        0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
        0x90, 0x90, 0xF0, 0x10, 0x10, // 4
        0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
        0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
        0xF0, 0x10, 0x20, 0x40, 0x40, // 7
        0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
        0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
        0xF0, 0x90, 0xF0, 0x90, 0x90, // A
        0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
        0xF0, 0x80, 0x80, 0x80, 0xF0, // C
        0xE0, 0x90, 0x90, 0x90, 0xE0, // D
        0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
        0xF0, 0x80, 0xF0, 0x80, 0x80  // F
    };
    Array.Copy(fonts, 0, Memory, 0, fonts.Length);
}

Before working on the actual CPU cycle we also want to connect our Processor to our Display so that we can register key presses. Per the CHIP-8 spec we need to support 16 keys. These keys will map from byte values 0 to F. The actual physical key press can be up to you. For this implementation I will use 1-4, Q-R, A-F, Z-V. This way they keys are in somewhat of a 4x4 sequence.

To create this mapping first I create a dictionary in Index.razor that maps the physical key value to the byte value.

Index.razor

@code 
{
    // other code
    Dictionary<int, byte> keyMap = new Dictionary<int, byte>()
    {
        { 49, 0x1}, // 1
        { 50, 0x2}, // 2
        { 51, 0x3}, // 3
        { 52, 0xc}, // 4
        { 81, 0x4}, // Q
        { 87, 0x5}, // W
        { 69, 0x6}, // E
        { 82, 0xD}, // R
        { 65, 0x7}, // A
        { 83, 0x8}, // S
        { 68, 0x9}, // D
        { 70, 0xE}, // F
        { 90, 0xA}, // Z
        { 88, 0x0}, // X
        { 67, 0xB}, // C
        { 86, 0xF}, // V
    };
 
    // other code
}

We can then implement OnKeyUp and OnKeyDown. These will take the byte values, check to see if they are in our mapping, and if so send the key press to our processor. To do this we will need to create the processor in OnMainCanvasReady.

@code 
{
    // other code
    Processor _processor;
 
    private async void OnMainCanvasReady(CanvasBase canvasBase)
    {
        _processor = new Processor();
    }
 
    private void OnKeyUp(int index)
    {
        if (keyMap.TryGetValue(index, out byte value))
        {
            _processor?.OnKeyUp(value);
        }
    }
 
    private void OnKeyDown(int index)
    {
        if (keyMap.TryGetValue(index, out byte value))
        {
            _processor?.OnKeyDown(value);
        }
    }
}

We then need to implement OnKeyUp and OnKeyDown in our Processor. This will take the byte value and set the value of KeysDown at the byte value index to true for OnKeyDown, and false for OnKeyUp.

Processor.cs

public void OnKeyDown(byte byteValue)
{
    KeysDown[byteValue] = true;
}
 
public void OnKeyUp(byte byteValue)
{
    KeysDown[byteValue] = false;
}

We also now have a processor in which we want to load the ROM file to in Index.razor. To do this I created a GetRomData method. For this implementation I am just going to hardcode the path. This file needs to live in wwwroot so that it is brought in through the browser. Then calling a GET method will fetch the data for the file.

Index.razor

@code
{
    // other code
 
    private async Task<byte[]> GetRomData()
    {
        var client = new HttpClient();
        // you may need to change the hostname to whatever your Blazor default is.
        var byteArray = await client.GetByteArrayAsync("https://localhost:7244/pong");
        return byteArray;
    }
}

This can then be called in the Processor constructor.

@code
{
    // other code
    private async void OnMainCanvasReady(CanvasBase canvasBase)
    {
        var romData = await GetRomData();
        _processor = new Processor(romData);
    }
}

Processor.cs

public class Processor
{
    public Processor(byte[] romData)
    {
        // other code
        LoadRom(romData)
    }
}

Now we can start working on the actual CPU cycle which is where the primary logic of our CHIP-8 occurs. There will be 36 unique possible opcodes with each having a set of instructions has defined by the CHIP-8 spec.

In a cycle the first thing that needs to be fetched is the opcode or instruction which will be 16 bits or 2 bytes long. This opcode comes from our memory and consists of the bytes at the PC index and PC + 1 index. To combine the two left shift the first half 8 bits to make it 16 bits long. This is followed by a bitwise OR with the second half piece will put the second half bits into place.

After taking the opcode we want to increment the PC by 2 since those instructions have now been read.

public void Cycle()
{
    var opCode = (ushort)((Memory[PC] << 8) | Memory[PC + 1]);
    PC += 2;
}

We also need to obtain the following variables, as they will be used in many of the instructions.

If hex format is confusing the simplest way to think of it is each alphanumeric represents 4 bits from 0 to F (15) 0-9 represent numeric values 0-9. A-F represents numeric values 10-15.

public void Cycle()
{
    var opCode = (ushort)((Memory[PC] << 8) | Memory[PC + 1]);
    PC += 2;
 
    var NNN = (ushort)(opCode & 0x0FFF);
    var KK = (byte)(opCode & 0x00FF);
    var N = (byte)(opCode & 0x000F);
    var X = (byte)((opCode & 0x0F00) >> 8);
    var Y = (byte)((opCode & 0x00F0) >> 4);
}

Now to the actual writing of each instruction. To do this we create a switch statement with the top 8 most signficiant bytes of the opcode and create a case for each of the 36 instructions. Some cases will have someoverlap with the top 8 most signficant bytes. For those cases we will check the exact opcode to specify which instruction should be run.

switch (opcode & 0xF000) 
{
}

0nnn - SYS addr

This instruction can be ignored. It is no longer used by modern interpreters

00E0 - CLS

This instruction clears the screen.

For this we will just reset our Pixels array to all false values.

case 0x0000 when opCode == 0x00E0:
    // CLS
    Pixels = new bool[2048];
    break;

00EE - RET

Set the program counter to the address at the top of the stack, then subtract 1 from the stack pointer.

case 0x0000 when opCode == 0x00EE:
    // RET
    PC = Stack[SP];
    SP -= 1;
    break;

1nnn - JP addr

Jump to location NNN. Set PC to the value at NNN

case 0x1000:
    // JP addr
    PC = NNN;
    break;

2nnn - CALL addr

Call subroutine at NNN. Increment the stack pointer, put the current PC at the top of ths tack, set the PC to the value at NNN

case 0x2000:
    // CALL addr
    SP += 1;
    Stack[SP] = PC;
    PC = NNN;
    break;

3xkk - SE Vx, byte

Skip the next instruction if Vx == KK. To skip the next instruction we increment the PC by 2.

case 0x3000:
    // SE Vx, byte
    if (V[X] == KK)
    {
        PC += 2;
    }
    break;

4xkk - SNE Vx, byte

Skip the next instruction if Vx != KK. Same as above except we skip if Vx is not equal to KK

case 0x4000:
    // SNE Vx, byte
    if (V[X] != KK)
    {
        PC += 2;
    }
    break;

5xy0 - SE Vx, Vy

Skip the next instruction if Vx == Vy

case 0x5000:
    // SE Vx, Vy
    if (V[X] == V[Y])
    {
        PC += 2;
    }
    break;

6xkk - LD Vx, byte

Set Vx = KK

case 0x6000:
    // LD Vx, byte
    V[X] = KK;
    break;

7xkk - ADD Vx, byte

Set Vx = Vx + KK

case 0x7000:
    // ADD Vx, byte
    V[X] += KK;
    break;

8xy0 - LD Vx, Vy

Set Vx = Vy

case 0x8000 when (opCode & 0xF) == 0x0:
    // LD Vx, Vy
    V[X] = V[Y];
    break;

8xy1 - OR Vx, Vy

Set Vx = Vx OR Vy

case 0x8000 when (opCode & 0xF) == 0x1:
    // OR Vx, Vy
    V[X] |= V[Y];
    break;

8xy2 - AND Vx, Vy

Set Vx = Vx AND Vy

case 0x8000 when (opCode & 0xF) == 0x2:
    // AND Vx, Vy
    V[X] &= V[Y];
    break;

8xy3 - XOR Vx, Vy

Set Vx = Vx XOR Vy

case 0x8000 when (opCode & 0xF) == 0x3:
    // XOR Vx, Vy
    V[X] ^= V[Y];
    break;

8xy4 - ADD Vx, Vy

Set Vx = Vx + Vy, set VF = carry. The values of Vx and Vy are added together. If the result is greater than 8 bits or 255 VF is set to 1, otherwise 0. The lowest 8 bits of the result is stored in Vx.

case 0x8000 when (opCode & 0xF) == 0x4:
    // ADD Vx, Vy
    var sum = (V[X] += V[Y]);
    V[0xF] = (sum > 255) ? (byte)1 : (byte)0;
    V[X] = (byte)(sum & 0xFF);
    break;

8xy5 - SUB Vx, Vy

Set Vx = Vx - Vy, set VF = NOT borrow. If Vx > Vy then VF is set to 1, otherwise 0. Then Vy is subtracted from Vx and results stored in Vx.

case 0x8000 when (opCode & 0xF) == 0x5:
    // SUB Vx, Vy
    V[0xF] = V[X] > V[Y] ? (byte)1 : (byte)0;
    V[X] -= V[Y];
    break;

8xy6 - SHR Vx {, Vy}

If the least significant bit of Vx is 1, then VF is set to 1, otherwise 0. Then Vx is divided by 2

case 0x8000 when (opCode & 0xF) == 0x6:
    // SHR Vx {, Vy}
    V[0xF] = (byte)(V[X] & 0x1);
 
    // Divide by 2. Right shift by 1
    V[X] >>= 0x1;
    break;

8xy7 - SUBN Vx, Vy

If Vy > Vx, then VF is set to 1, otherwise 0. Then Vx is subtracted from Vy, and the results stored in Vx

case 0x8000 when (opCode & 0xF) == 0x7:
    // SUBN Vx, Vy
    V[0xF] = V[Y] > V[X] ? (byte)1 : (byte)0;
    V[X] = (byte)(V[Y] - V[X]);
    break;

8xyE - SHL Vx {, Vy}k

If the most significant bit of Vx is 1, then VF is set to 1, otherwise to 0. Tnen Vx is multipled by 2.

case 0x8000 when (opCode & 0xF) == 0xE:
    // SHL Vx {, VY}
    V[0xF] = (byte)((V[X] & 0x80) >> 7);
 
    // Multiply by 2. Left shift by 1
    V[X] <<= 0x1;
    break;

9xy0 - SNE Vx, Vy

Skip the next instruction if Vx != Vy

case 0x9000:
    // SNE Vx, Vy
    if (V[X] != V[Y])
    {
        PC += 2;
    }
    break;

Annn - LD I, addr

Set I = NNN

case 0xA000:
    // LD I, addr
    I = NNN;
    break;

Bnnn - JP V0, addr

Set the PC to NNN + V0

case 0xB000:
    // JP V0, addr
    PC = (byte)(NNN + V[0]);
    break;

Cxkk - RND Vx, byte

Generate a random number from 0 to 255. Set the value of Vx to this random number ANDed with KK

case 0xC000:
    // RND Vx, byte
    var random = new Random();
    var randByte = (random.Next(0, 256) & KK);
    V[X] = (byte)randByte;
    break;

Dxyn - DRW Vx, Vy, nibble

Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision

This is a large one. This will handle setting our Pixels array that we will then display.

Before we start coding the instruction we will need a new Processor property. This will be a boolean called WrapAround. The official CHIP-8 spec does not make it clear if pixels are meant to wrap around if a selected Pixel is outside the limits. Some ROMS will not function properly depending on this setting so it's best if we include way to toggle it.

public class Processor
{
    private bool WrapAround { get; set; }
 
    public Processor(byte[] romData, bool wrapAround)
    {
        // other code
        WrapAround = wrapAround;
    }
}

If you are unfamiliar with sprites it may be helpful to check out this reference (opens in a new tab)

In simple terms it is a group of bytes which are a binary representation of some picture. CHIP-8 sprites in particular are 8 pixels wide and up to 15 lines high.

To start we set a width of 8 since each sprite is 8 pixels wide. We then set a height equal to the value of N or the last nibble of the opcode. We then set VF = 0. This will be used to detect collisions. If we cause a Pixel to be erased we set this value to 1.

case 0xD000:
    // DRW Vx, Vy, nibble
 
    var width = 8;
    var height = N;
 
    V[0xF] = 0;

Our code will then go row by row. At each row we grab the sprite representation at that single row stored in Memory[I + row];

for (var row = 0; row < height; row++)
{
    var sprite = Memory[I + row];
}

We then go column by column. First we check if the sprite is greater than 0. If the value of the whole sprite is 0 we can skip since there are no pixels to draw.

If the sprite is greater than 0 we continue.

for (var row = 0; row < height; row++)
{
    var sprite = Memory[I + row];
    for (var col = 0; col < width; col++)
    {
        if ((sprite & 0x80) > 0)
        {
        }
    }
}

The X and Y coordinates of where we want to draw the sprite are stored in Vx and Vy respectively. To get the exact Pixel we want to flip we add the X coordinate with the column and the Y coordinate with the row.

Depending on the toggle setting a modulo is added (64 for column and 32 for row since the display is 64x32). This will allow pixels to wrap around to the opposite side of the screen if they are out of limits. We will do this here only if WrapAround is set to true. If WrapAround is set to false and the index is out of limits it will be ignored.

To get the Pixel index we multiply y by 64 and add it to x.

Now that we have the index we can check if the Pixel is already set to true. If so, we will be erasing it when we set it to false. Therefore we need to set the VF to 1.

Finally we can flip the boolean and shift the sprite left 1 bit to work through each bit of the sprite.

for (var row = 0; row < height; row++)
{
    var sprite = Memory[I + row];
    for (var col = 0; col < width; col++)
    {
        if ((sprite & 0x80) > 0)
        {
            int x = V[X] + col;
            int y = V[Y] + row;
 
            if (!WrapAround && (x >= 64 || y >= 32))
            {
                continue;
            }
            else
            {
                x %= 64;
                y %= 32;
            }
 
            var index = x + (y * 64);
 
            if (Pixels[index] == true)
            {
                V[0xF] = 1;
            }
 
            Pixels[index] = !Pixels[index];
        }
 
        sprite <<= 1;
    }
 
}
 

The whole thing

case 0xD000:
    // DRW Vx, Vy, nibble
    var width = 8;
    var height = N;
 
    V[0xF] = 0;
 
    for (var row = 0; row < height; row++)
    {
        var sprite = Memory[I + row];
 
        for (var col = 0; col < width; col++)
        {
            if ((sprite & 0x80) > 0)
            {
                int x = V[X] + col;
                int y = V[Y] + row;
 
                if (!WrapAround && (x >= 64 || y >= 32))
                {
                    continue;
                }
                else
                {
                    x %= 64;
                    y %= 32;
                }
 
                var index = x + (y * 64);
 
                if (Pixels[index] == true)
                {
                    V[0xF] = 1;
                }
 
                Pixels[index] = !Pixels[index];
            }
 
            sprite <<= 1;
        }
    }
 
    break;

Ex9E - SKP Vx

Skip the next instruction if key with the value of Vx is pressed

case 0xE000 when (opCode & 0xFF) == 0x9E:
    // SKP Vx
    if (KeysDown[V[X]])
    {
        PC += 2;
    }
    break;

ExA1 - SKNP Vx

Skip next instruction if key with the value of Vx is not pressed

case 0xE000 when (opCode & 0xFF) == 0xA1:
    // SKNP Vx
    if (!KeysDown[V[X]])
    {
        PC += 2;
    }
    break;

Fx07 - LD Vx, DT

Set Vx = delay timer value

case 0xF000 when (opCode & 0xFF) == 0x07:  
    // LD Vx, DT
    V[X] = (byte)DelayTimer;
    break;

Fx0A - LD Vx, K

Wait for a key press. Store the value of the key in Vx

For this we need to be a bit creative in how we approach it. We need the program to pause while waiting for the next key press. The way I decided to implement it was first introducing 3 new properties into our Processor. This includes a boolean Paused, a boolean WaitingForPress, and a byte WaitingForPressRegisterIndex. Create and instantiate all of these. Paused and WaitingForPress can be set to false, and WaitingForRegisterIndex can be set to 0.

public class Processor
{
    // other code
    public Processor(byte[] romData, bool wrapAround)
    {
        // other code
        WaitingForPress = false;
        WaitingForPressRegisterIndex = 0;
        Paused = false;
    }
}

If Paused is set to true. We want our cycle to just return before doing anything. This will simulate being paused.

public void Cycle()
{
    if (Paused)
    {
        return;
    }
 
    // other code
}

Next we want to modify OnKeyDown. We set the value at the byte value index to true, however we also check to see If WaitingForPress is set to true. If it is we store the bytevalue in the register at index WaitingForPressRegisterIndex which is what the opcode wants us to do. We can then set Paused and WaitingForPress back to false. The value of WaitingForPressRegisterIndex does not matter since it will change when we need to pause again.

public void OnKeyDown(byte byteValue)
{
    KeysDown[byteValue] = true;
 
    if (WaitingForPress)
    {
        Paused = false;
        WaitingForPress = false;
        V[WaitingForPressRegisterIndex] = byteValue;
    }
}

In the instruction itself we just need to set Paused equal to true, WaitingForPress equal to true, WaitingForPressRegisterIndex equal to the value of X and break.

case 0xF000 when (opCode & 0xFF) == 0x0A:
    // LD Vx, K
    Paused = true;
    WaitingForPress = true;
    WaitingForPressRegisterIndex = X;
    break;

Fx15 - LD DT, Vx

Set delay timer = Vx

case 0xF000 when (opCode & 0xFF) == 0x15:  
    // LD DT, Vx
    DelayTimer = V[X];
    break;

Fx18 - LD ST, Vx

Set sound timer = Vx

case 0xF000 when (opCode & 0xFF) == 0x18:  
    // LD ST, Vx
    SoundTimer = V[X];
    break;

Fx1E - ADD I, Vx

Set I = I + Vx

case 0xF000 when (opCode & 0xFF) == 0x1E:  
    // ADD I, Vx
    I = (ushort)(I + V[X]);
    break;

Fx29 - LD F, Vx

Set I = location of the hexadecimal sprite at Vx. We multiply it by 5 because each hexadecimal sprite is 5 bytes long

case 0xF000 when (opCode & 0xFF) == 0x29:  
    // LD F, Vx
    I = (ushort)(V[X] * 5);
    break;

Fx33 - LD B, Vx

Take the decimal value of Vx, place the hundreds digit in memory at location in I, the tens digit at location I + 1, and the ones digit at location I + 2

case 0xF000 when (opCode & 0xFF) == 0x33:  
    // LD B, Vx
    var value = V[X];
    Memory[I] = (byte)(value / 100);
    Memory[I + 1] = (byte)(value % 100 / 10);
    Memory[I + 2] = (byte)(value % 10);
    break;

Fx55 - LD [I], Vx

Copy the values of registers V0 through Vx into memory, starting at address I.

case 0xF000 when (opCode & 0xFF) == 0x55:  
    // LD [I], Vx
    for (var i = 0; i <= X; i++)
    {
        Memory[I + i] = V[i];
    }
    break;

Fx65 - LD Vx, [I]

Read values from memory starting at location I into registers V0 through Vx

case 0xF000 when (opCode & 0xFF) == 0x65:  
    // LD Vx, [I]
    for (var i = 0; i <= X; i++)
    {
        V[i] = Memory[I + i];
    }
    break;

Finally we will set a default case statement which throw a new exception. If the CHIP-8 specification is correct this should never be hit.

default:
    throw new Exception($"OpCode not found at PC {PC}");

and that's it for our CHIP-8 instructions.

One final piece of logic that we also must do is the decrement the timers. We have two timers the delay timer and the sound timer that are subtracted by 1 at a rate of 60hz.

For this we will create a DecrementTimers method that reduces the timers by 1 if they are greater than 0. Since cycle is called at a rate of 60hz we can call this at the end of our Cycle method.

private void Cycle()
{
    // Instruction code above
    default:
        throw new Exception($"OpCode not found at PC {PC}");
 
    DecrementTimers();
}
 
private void DecrementTimers()
{ 
    if (DelayTimer > 0) 
    {
        DelayTimer -= 1;
    }
 
    if (SoundTimer > 0)
    {
        SoundTimer -= 1;
    }
}

When the SoundTimer goes to 0 we need to play a beep sound. This was a bit tricky to implement since we need to do this in the browser. The way I was able to implement it was first find a simple beep wav file. A free one I found is included in the Github. This file needs to be in the wwwroot directory.

beep.wave (opens in a new tab)

Next I created a js directory under wwwroot and included a JavaScript file called PlaySound.js. In this file I included the code.

PlaySound.js

window.PlaySound = function () {
    document.getElementById('sound').play();
}

I then added this file as well as an audio html element with the Id "sound" pointed at the wav file in index.html

index.html

<body>
// other code
    <script src="/js/PlaySound.js"></script>
    <audio id="sound" src="beep.wav" />
</body>

Finaly we need a progrmatic way to trigger that function. Blazor has a way to call JavaScript functions in the Razor page. To do this first IJSRuntime needs to be brought in. Then we can invoke the JavaScript function. I created a seperate method for this which we will then pass to the Processor.

Index.razor

@page "/"
@using Blazorex;
@inject IJSRuntime JS
 
@code
{
    // other code
    private async void OnMainCanvasReady(CanvasBase canvasBase)
    {
        _processor = new Processor(true, this.PlaySound);
        // other code
    }
 
    // other code
 
    private async Task PlaySound()
    { 
        await JS.InvokeAsync<string>("PlaySound"); // this calls "window.PlaySound()"
    }
}

Processor will need to be modified to include a PlaySound function property, as well as modify the constructor so that it can take in that property from Index.razor. We can then call this function anytime SoundTimer goes to 0.

Processor.cs

public class Processor
{
    // other code
    Func<Task> PlaySound { get; set; }
 
    public Processor(byte[] romData, bool wrapAround, Func<Task> playSound)
    {
        // other code
        PlaySound = playSound;
    }
 
    private void DecrementTimers()
    {
        if (DelayTimer > 0)
        {
            DelayTimer -= 1;
        }
 
        if (SoundTimer > 0)
        {
            SoundTimer -= 1;
            if (SoundTimer == 0)
            {
                PlaySound();
            }
        }
    }
}

Finally the last thing I want to do is actually allow the user to toggle the speed of the interpreter. The CHIP-8 spec isn't clear on how many cycles per second the processor should run at. In my experience a single cycle per frame is much too slow for a good experience. We will add a Speed property which will be passed in our constructor to specify the speed of the interpreter.

To do this we first introduce two new properties on Processor.cs. This will be a Counter int property and a Speed int property. The Speed property will be passed into the constructor, while the Counter property will be set to 0.

public class Processor
{
    // other code
    private int Counter { get; set; }
    private int Speed { get; set; }
 
    public Processor(byte[] romData, bool wrapAround, Func<Task> playSound, int speed)
    {
        Speed = speed;
        Counter = 0;
    }
}

The idea is the speed property will be equivalent to the amount of cycles per frame. In our Index.razor we will call a for loop with "Speed" iterations of calling cycle. We create the Counter property so we can count the amount of times cycle is called. Then when the amount of cycles is equal to the Speed we can properly decrement the timers since these are meant to decrement at a rate of 60hz.

First we increment Count on every cycle, and modify to only call DecrementTimers when Counter modulo Speed is equal to 0.

public void Cycle()
{
    // other code
 
    if (Counter % Speed == 0)
    {
        DecrementTimers();
    }
 
    Counter += 1;
}

Then in Index.razor we can set a speed value to whatever we want and pass it into the processor. 10 works pretty well in my experience.

Then In OnMainFrameReady we loop and call processor cycle for an amount of times equivalent to speed.

Index.razor

@code
{
    // other code
    static int speed = 10;
 
    private async void OnMainCanvasReady(CanvasBase canvasBase)
    {
        // other code
        _processor = new Processor(false, this.PlaySound, speed);
    }
 
    private void OnMainFrameReady(float timeStamp)
    {
        for (var i = 0; i < speed; i++)
        {
            _processor?.Cycle();
        }
    }
}

and that is it for our Processor.

The final task we need to do is paint the Pixels onto our canvas on each frame. We will accomplish this using a Render method in Index.razor.

First we need to set up a few things. In Index.razor we need to create a IRenderContext reference which will be set to canvasBase.RenderContext. Then we need to specify our FillStyle and LineWidth which will be the color and thickness of the pixels.

Index.razor

@code
{
    // other code
    IRenderContext _context;
 
    private async void OnMainCanvasReady(CanvasBase canvasBase)
    {
        // other code
        _context = canvasBase.RenderContext;
        _context.FillStyle = "rgb(255, 255, 255)";  // white
        _context.LineWidth = 1;
    }
}

Then we can start creating the Render function which will be called in OnMainFrameReady.

This Render function will simply clear the canvas, iterate over the Pixels array from our processor, check for values of true, and paint these values onto the canvas.

private void OnMainFrameReady(float timeStamp)
{
    // other code
 
    Render();
}
 
 
private void Render()
{
    var buffer = _processor?.Pixels;
 
    if (buffer != null)
    {
        _context.ClearRect(0, 0, _width, _height);
 
        for (int y = 0; y < 32; y++)
        {
            for (int x = 0; x < 64; x++)
            {
                if (buffer[y * 64 + x])
                {
                    _context.FillRect(x * scale, y * scale, 1 * scale, 1 * scale);
                }
            }
        }
    }
}

and that should be everything. I included a few ROM files in the github that you can try running, others shoud be easy to find with a google search.

ROM files (opens in a new tab)

I recommend running the IBM rom first, as it has only a few instructions and will be obvious if there are any mistakes.

CHIP-8 running the IBM rom in the browser

If you are seeing some things off check out the github code and really analayze the opcode instructions. It is usually very small one off errors in the opcode instructions that can cause large incorrect behaviors. I haven't checked all ROM files so this implementation may have some bugs as well.

Alternatively it may be helpful to build a simple debugger. This would be something that would allow you to manually step through each instruction and output the registers, or pixels to look for errors. I personally haven't implemented something like this yet but I may in a future blog post.

Full Github source code (opens in a new tab)

If you enjoyed building this and want to try something harder I recommend trying to create an Intel 8080 processor emulator, and if you really want a challenge a NES or Gameboy emulator.

I highly recommend the subreddit /r/emudev (opens in a new tab) for more resources and ideas.