Introduction
Creating a Chip8 emulator is a great introduction to learning the basics of emulator creation. Topics such as bitwise operations, bit graphic programming, and reading technical manuals are all necessary things to start on the path of emulator creation.
What is CHIP8
Chip8 is a lot more than I can describe in a few words, but what we will be building is a virtual machine. That means we will be building an emulator that emulates the said virtual machine. The emulator(virtual machine) will then be used to run a few old games that you might or might not know.
If you want to read a detailed technical document, which I have used as a resource, you can read this document: Cowgod's Chip-8 Technical Reference v1.0
Bitwise operations
Understanding bitwise operation is very vital to emulator development. The hardware in a console that performs the bit operations are what we emulate when creating emulators. We have to understand how to manipulate bits and perform bitwise operations. The end goal is to create a copy of the hardware (in software) that performs the same bit operations and can make the game run.
AND (&)
In a bitwise AND operation, a bit is only equal to 1 if both bits are equal to 1. If either is 0 then it will be equal to 0. For example:
111101111111
& 000010100111
____________
000000100111
As you can see when we perform an AND (&), everywhere the 1s align, the result becomes a 1. Otherwise, the result is 0. Now you might be wondering, but why is this useful? Having the ability to manipulate bits like this is extremely useful to find out a value in memory. For example:
var bin1 = 0b000000001011; //0xb (11 in decimal)
var compareBin = 0b000000001111 // 0xF (15 in decimal)
// will equal to 11
var returnedVal = bin1 & compareBin;
we can see that the last 4 bits are not equal to 1111 because the value returned was 11. We also got the original value back (11) from memory which we can now store to manipulate.
OR (|)
In a bitwise OR operation, a bit is equal to 1 if one of them is equal to 1. If both are 0 then it will be a 0. For example:
000001001000
| 000010100111
____________
000011101111
As long as one of the top or bottom binary is 1 it will be equal to 1 in the resulting binary.
XOR (^)
In a bitwise XOR operation, a bit is equal to 1 if only one of them is equal to 1. if both are 1 or both are 0 then it will be 0. For example:
000001001111
^ 000010100111
____________
000011101000
If they are not the opposite, it will equal to 0.
NOT (~)
The NOT bitwise operation will just reverse all the bits. For example:
000001111111
Will become:
111110000000
Shift (>> and <<)
The shift bitwise operators shift by a certain amount of bit. For example:
000000001111 << 1
Will become:
000000011110
As you can see, we have shifted it to the left by 1 bit. The right shift does the same thing:
000000011110 >> 1
Will become:
000000001111
Now that we got all the bitwise operations down and know how to manipulate, extract, and move bits from a specific location in memory, we can move on to start programming!
Project structure
Chip8 project
│ Index.html
│ Stylesheet.css
│ GameRomNames.js
└───Javascript
│ │ chip8wrapper.js
│ │ script.js
│ └───utils
│ │ autobinder.js
│ │ instruction.js
│ └───models
│ │ chip8.js
│ │ disassembler.js
│ │ keyboard.js
│ │ screen.js
│ │ sound.js
I would suggest you create those files and folders so that you can keep up as we go along without having to consistently check for which file you have to add next, or which file to add into next.
I will explain the files as we go along and program it rather than explaining it here, but I want you to always keep the structure in mind so you know where the piece fits in.
Programming the emulator
Let's start with programming the chip and all its properties. I Will chronologically follow Cowgod's technical documentation, which means we will start with programming the chip8's various memories.
Instruction.js
The technical document tells us:
All instructions are 2 bytes long
This means that every instruction is 16 bits (ones or zeros) long such as:
1111000011110000
The document then gives us the following information about the instructions and their structure.
nnn or addr - A 12-bit value, the lowest 12 bits of the instruction
n or nibble - A 4-bit value, the lowest 4 bits of the instruction
x - A 4-bit value, the lower 4 bits of the high byte of the instruction
y - A 4-bit value, the upper 4 bits of the low byte of the instruction
kk or byte - An 8-bit value, the lowest 8 bits of the instruction
NNN or ADDR
the lowest 12 bit of the instruction, for example:
1111(000011110000)
the part in parenthesis is the NNN or ADDR of the instruction, so whenever an instruction wishes to retrieve or manipulate the NNN or ADDR, we have to manipulate that part of the instruction.
N or Nibble
The lowest 4 bits of the instruction, for example:
111100001111(0000)
X
The lowest 4 bits of the high byte of the instruction, for example:
1111(0000)|11110000
Notice the bar dividing the first and second byte.
Y
The upper 4 bits of the low byte of the instruction, for example:
11110000|(1111)0000
Again notice the bar dividing the first and second byte.
KK or byte
The lowest 8 bit of the instruction, for example:
11110000(11110000)
Now that we know what those things mean, let us continue. We need to make a class that can store the instruction, and perform the manipulation to get the ADDR, N, Y, X, and KK out.
export default class Instruction {
constructor(instructionCode) {
this.instructionCode = instructionCode || 0;
}
getInstructionCode() {
return this.instructionCode;
}
setInstructionCode(instructionCode) {
this.instructionCode = instructionCode;
}
// return the first 4 bits of the highest byte
// and shift to the right by 12
getCategory() {
return (this.instructionCode & 0xF000) >> 12;
}
// returns the ADDR/NNN by using a bitwise AND operation
getAddr() {
return this.instructionCode & 0x0FFF;
}
// returns the X by using a bitwise AND operation
// shifts the bits to the right by 8
getX() {
return (this.instructionCode & 0x0F00) >> 8;
}
// return the Y using a bitwise AND operation
// shift the bits to the right by 4
getY() {
return (this.instructionCode & 0x00f0) >> 4;
}
// return the KK using a bitwise AND operation
getKK() {
return this.instructionCode & 0x00FF;
}
// return the N/Nibble using a bitwise AND operation
getSubCategory() {
return this.instructionCode & 0x000F;
}
}
As we can see the code is simple, but it will be very helpful in making sure we don't have to repeat the same bitwise operations over and over again.
Autobinder.js
export default function autoBind(obj) {
const keys = Reflect.ownKeys(obj.constructor.prototype);
keys.forEach((key) => {
if (typeof obj[key] === 'function') {
obj[key] = obj[key].bind(obj);
}
});
return obj;
}
This is a small self-made function that just binds "this" to every function outside of the class constructor. Alternatively you can use the Context-Binder package to do the same for you but with more options
Chip8.js
Memory and Registers
We learn from reading the documentation (section 2.1 and 2.2) that:
- CHIP8 has 4096 bytes of memory
- Program starts at the location 0x200(512)
- CHIP8 has 16 general purpose 8-bit register
- CHIP8 has a 16-bit register that is used to store memory addresses
- CHIP8 has 2 8-bit registers, for the delay and sound timers
- CHIP8 has a stack
- CHIP2 also has 2 "pseudo-registers". The program counter (PC) and the stack pointer (SP)
And we can see how those things are implemented here:
import Instruction from '../utils/instruction.js';
import Screen from './screen.js';
import Keyboard from './keyboard.js'
import Sound from './sound.js';
export default class Chip8 {
constructor(screen, keyboard, sound) {
// initialize new instances of the models
this.screen = screen || new Screen();
this.keyboard = keyboard || new Keyboard();
this.sound = sound || new Sound();
this.instruction = new Instruction();
// default starter settings
// cycle execution speed
this.speed = 10;
// sound tracker
this.soundOff = true;
}
resetState() {
// 4096 byte RAM
this.memory = new Uint8Array(1024 * 4);
// the program counter register, starts at 512 (0x200)
this.programCounter = 0x200;
// 16 8-bit registers
this.v = new Uint8Array(16);
// stack
this.stack = [];
// stack pointer
// points to topmost level of the stack
this.stackPointer = 0;
// register for storing memory addresses
this.i = 0;
// registers for sound and delay timers
this.delayTimer = 0;
this.soundTimer = 0;
// game paused state tracker
this.pause = false;
// clear the screen and keyboard
// insert font into the cpu
this.screen.clearScreen();
this.keyboard.clear();
this.loadFontsIntoState();
}
}
Just as a note, although the descriptions in the documentation give a detailed overview of how many bits each register has to be, this all becomes unnecessary in JS due to its nature of being a high-level language.
Emulating a cpu cycle
We have to emulate the CPU cycle, this is the way we do it:
emulateCycle() {
for (let i = 0; i < this.speed; i += 1) {
// if the game is not paused
if (!this.pause) {
// each instruction is 2 bytes (16bit) long
// read value in memory of current PC
// bit-shift to left by 8
const firstByte = this.memory[this.programCounter] << 8;
// read next value (PC++) in memory
const secondByte = this.memory[this.programCounter + 1];
// add both values together by bitwise OR
// to create a 16bit (2 byte) instruction
// this is because the memory of chip8 is only 8 bit (1byte)
this.instruction.setInstructionCode(firstByte | secondByte);
// execute instruction
this.performInstruction(this.instruction);
}
}
if (!this.pause) {
this.updateTimers();
}
}
If the bit part is still confusing, imagine the memory is linear like this:
11110000 00001111 .... rest of memory
So we take out the first byte (8 bits), and shift to the left by 8:
var x = this.memory[this.programCounter] << 8;
Now the variable x is equal to 11111000000000000. Then we take the value of the position right after:
var y = this.memory[this.programCounter + 1];
which is 00001111 and we bitwise OR it together:
1111000000000000
| 00001111
________________
1111111100000000
Now we have our first instruction to execute.
Instruction sets
performInstruction(instructionCode) {
// to signal that this instruction executed and move to next
this.programCounter += 2;
switch (instructionCode.getCategory()) {
case 0x0: this.operationCode0(instructionCode); break;
case 0x1: this.operationCode1(instructionCode); break;
case 0x2: this.operationCode2(instructionCode); break;
case 0x3: this.operationCode3(instructionCode); break;
case 0x4: this.operationCode4(instructionCode); break;
case 0x5: this.operationCode5(instructionCode); break;
case 0x6: this.operationCode6(instructionCode); break;
case 0x7: this.operationCode7(instructionCode); break;
case 0x8: this.operationCode8(instructionCode); break;
case 0x9: this.operationCode9(instructionCode); break;
case 0xA: this.operationCodeA(instructionCode); break;
case 0xB: this.operationCodeB(instructionCode); break;
case 0xC: this.operationCodeC(instructionCode); break;
case 0xD: this.operationCodeD(instructionCode); break;
case 0xE: this.operationCodeE(instructionCode); break;
case 0xF: this.operationCodeF(instructionCode); break;
default: {
let instCode = instructionCode.getInstructionCode().toString(16);
throw new Error(`Unknown opcode ${instCode}`);
}
}
}
We use this function to redirect to a specific instruction based on the category of the instruction. We also add 2 to the program counter (PC) to signal that this instruction has been performed so the next CPU cycle does not repeat the same instruction. Now that we have set up the memory and registers, emulated the cycle, and made a function to redirect the specific instruction, we have to create the functions that will perform the instructions.
00E0 - CLS and 00EE - RET
operationCode0(instruction) {
switch (instruction.getKK()) {
case 0xE0: this.screen.clearScreen(); break;
// return from subroutine
case 0xEE:
this.stackPointer = this.stackPointer -= 1;
// sets program counter to address at top of stack
this.programCounter = this.stack[this.stackPointer];
break;
default: break;
}
}
We first get the KK of the instruction (last 2 bits) and based on them we perform either CLS or RET (which you can read about in the documentation). You might notice that the documentation says:
The interpreter sets the program counter to the address at the top of the stack,
then subtracts 1 from the stack pointer
We subtract one before we assign the value to the program counter. The reason for that is the documentation is thinking in terms of pointers, so whether you subtract one before you assign the value to the program counter or after, it would remain the same as they would have the same memory address. However, we don't use pointers as we are working in JS.
1nnn - JP addr
This one is rather simple, we set the program counter to bits at NNN.
operationCode1(instruction) {
this.programCounter = instruction.getAddr();
}
Since we have made the helper functions, we can just use it to get NNN out of the instruction.
2nnn - CALL addr
operationCode2(instruction) {
// increment stack pointer
this.stackPointer += 1;
// put the program counter on the top of the stack
this.stack[this.stackPointer] = this.programCounter;
// program counter is set to NNN
this.programCounter = instruction.getAddr();
}
3xkk - SE Vx, byte
operationCode3(instruction) {
// compare register Vx to KK
if (this.v[instruction.getX()] === instruction.getKK()) {
// skip to next instruction
this.programCounter += 2;
}
}
If the register Vx is equal to KK, then skip to the next instruction by incrementing the program counter by 2.
4xkk - SNE Vx, byte
operationCode4(instruction) {
if (this.v[instruction.getX()] !== instruction.getKK()) {
this.programCounter += 2;
}
}
This one is simple like the previous one. If Vx is not equal to KK, then skip to the next instruction.
5xy0 - SE Vx, Vy
operationCode5(instruction) {
if (this.v[instruction.getX()] === this.v[instruction.getY()]) {
this.programCounter += 2;
}
}
If X is equal to Y, we skip to the next instruction.
6xkk - LD Vx, byte
operationCode6(instruction) {
this.v[instruction.getX()] = instruction.getKK();
}
Put the value KK into the register Vx.
7xkk - ADD Vx, byte
operationCode7(instruction) {
const val = instruction.getKK() + this.v[instruction.getX()];
this.v[instruction.getX()] = val;
}
Add the value of the KK and Vx register and store it in the Vx register.
8xy* instructions
operationCode8(instruction) {
const x = instruction.getX();
const y = instruction.getY();
switch (instruction.getSubCategory()) {
// store Vy in Vx
case 0x0: this.v[x] = this.v[y]; break;
// perform bitwise OR and store value in Vx
case 0x1: this.v[x] |= this.v[y]; break;
// perform bitwise AND and store value in Vx
case 0x2: this.v[x] &= this.v[y]; break;
// perform bitwise XOR and store value in Vx
case 0x3: this.v[x] ^= this.v[y]; break;
case 0x4:
// add Vx and Vy
this.v[x] += this.v[y];
// if Vx is greater than 255
if (this.v[x] > 0xFF) {
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
break;
case 0x5:
// if Vx is greater than Vx
if (this.v[x] > this.v[y]) {
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
// subtract Vy from Vx
this.v[x] -= this.v[y];
break;
case 0x6:
// get the last bit of Vx and assign
this.v[0xF] = this.v[x] & 0x1;
// use bitwise shift to divide by 2
this.v[x] >>= 1;
break;
case 0x7:
if (this.v[y] > this.v[x]) {
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
// subtract Vx from Vy and store result in Vx
this.v[x] = this.v[y] - this.v[x];
break;
case 0xE:
// if the most significant bit of Vx is 1
// then set Vf to 1, otherwise 0
this.v[0xF] = +(this.v[x] & 0x80);
// use bitwise shift to multiply by 2
this.v[x] = this.v[x] << 1;
break;
default: {
let instCode = instruction.getInstructionCode().toString(16);
throw new Error(`Unknown opcode ${instCode}`);
}
}
}
In case 0x4. The reason we don't do anything about the "only the lowest 8 bits of the result are kept" is because when we add Vx and Vy together, then it becomes 8 bits only (our registers are 8 bit).
9xy0 - SNE Vx, Vy
operationCode9(instruction) {
if (this.v[instruction.getX()] !== this.v[instruction.getY()]) {
this.programCounter += 2;
}
}
If Vx is not the same as Vy then skip to the next instruction.
Annn - LD I, addr
operationCodeA(instruction) {
this.i = instruction.getAddr();
}
The I register is set to the value stored in NNN.
Bnnn - JP V0, addr
operationCodeB(instruction) {
this.programCounter = instruction.getAddr() + this.v[0x0];
}
The program counter is set to NNN + V0.
Cxkk - RND Vx, byte
operationCodeC(instruction) {
const val = Math.floor(Math.random() * 0xFF);
// bitwise AND it with KK and assign to vX
this.v[instruction.getX()] = val & instruction.getKK();
}
Generate a pseudo-random number (0-255) and assign it to Vx.
Dxyn - DRW Vx, Vy, nibble
operationCodeD(instruction) {
let sprite;
const width = 8;
const height = instruction.getSubCategory();
const xPortion = instruction.getX();
const yPortion = instruction.getY();
this.v[0xF] = 0;
// read N bytes from memory
// to know where to draw vertically along the Y axis
for (let y = 0; y < height; y += 1) {
sprite = this.memory[this.i + y];
// draw certain pixels horizontally along the X axis
for (let x = 0; x < width; x += 1) {
// if sprite is to be drawn
if ((sprite & 0x80) > 0) {
// if no pixel was erased
if (this.screen.setPixels(this.v[xPortion] + x, this.v[yPortion] + y)) {
this.v[0xF] = 0;
} else {
this.v[0xF] = 1;
}
}
sprite <<= 1;
}
// this part is for blink reduction
this.screen.vfFrame = this.v[0xF];
this.screen.render();
}
}
We read N-bytes from the instruction because the Y-axis (vertical) of the sprite can be anything from 1 to 16 pixels. However, the width of the sprite is set at 8 pixels wide. Then for each pixel in the sprite, we insert it into the display, and afterward, check if a pixel was erased or not to set the Vf register while simultaneously adding the pixel on the screen. Then the screen of the chip8 is rendered.
ExA** instructions
operationCodeE(instruction) {
switch (instruction.getKK()) {
// skip next instruction if key with value Vx is pressed
case 0x9E:
if (this.keyboard.isKeyPressed(this.v[instruction.getX()])) {
this.programCounter += 2;
}
break;
// skip next instruction if key with the value Vx is not pressed
case 0xA1:
if (!this.keyboard.isKeyPressed(this.v[instruction.getX()])) {
this.programCounter += 2;
}
break;
default: {
let instCode = instruction.getInstructionCode().toString(16);
throw new Error(`Unknown opcode ${instCode}`);
}
}
}
In this instruction function, we just skip if certain keys were pressed or not.
Fx** instructions
To keep the code clean and simple, the Fx** instruction set is also separated.
operationCodeF(instruction) {
switch (instruction.getKK()) {
case 0x07: this.operationCodeF07(instruction); break;
case 0x0A: this.operationCodeF0A(instruction); break;
case 0x15: this.operationCodeF15(instruction); break;
case 0x18: this.operationCodeF18(instruction); break;
case 0x1E: this.operationCodeF1E(instruction); break;
case 0x29: this.operationCodeF29(instruction); break;
case 0x33: this.operationCodeF33(instruction); break;
case 0x55: this.operationCodeF55(instruction); break;
case 0x65: this.operationCodeF65(instruction); break;
default: {
let instCode = instruction.getInstructionCode().toString(16);
throw new Error(`Unknown opcode ${instCode}`);
}
}
}
Fx07 - LD Vx, DT
operationCodeF07(instruction) {
this.v[instruction.getX()] = this.delayTimer;
}
Assign the delay register to the Vx register
Fx0A - LD Vx, K
operationCodeF0A(instruction) {
// stop all execution
this.pause = true;
// wait for next key pressed
this.keyboard.onNextKeyPress = function onNextKeyPress(key) {
// store value of the key in vx
this.v[instruction.getX()] = key;
// continue execution
this.pause = false;
}.bind(this);
}
If this is a little complicated and does not make sense to you, what happens is we essentially stop the execution, and then change the "onNextKeyPress" function on the keyboard object. So when the next key is clicked, it will execute this function we declared here. We also bind "this" to the function that we assign to the "onNextKeyPress" so it can have access to the registers.
Fx15 - LD DT, Vx
operationCodeF15(instruction) {
this.delayTimer = this.v[instruction.getX()];
}
Set the "delayTimer" register to the value in Vx.
Fx18 - LD ST, Vx
operationCodeF18(instruction) {
this.soundTimer = this.v[instruction.getX()];
}
Set the "soundTimer" register to the value in Vx.
Fx1E - ADD I, Vx
operationCodeF1E(instruction) {
this.i += this.v[instruction.getX()];
}
Set the I register to the value of I + Vx.
Fx29 - LD F, Vx
operationCodeF29(instruction) {
this.i = this.v[instruction.getX()] * 5;
}
Set the I register to the value of Vx. It's multiplied by five because a sprite is 5 bytes long.
Fx33 - LD B, Vx
operationCodeF33(instruction) {
let number = this.v[instruction.getX()];
for (let i = 3; i > 0; i -= 1) {
// parse and assign the first (from right) number
this.memory[this.i + i - 1] = parseInt(number % 10, 10);
// divide by 10 to shave off a decimal
number /= 10;
}
}
We place the numbers in Vx into I location in memory starting from furthest right first. Then one by one place them backwardly such as I+3, I+2...
Fx55 - LD [I], Vx
operationCodeF55(instruction) {
for (let i = 0; i <= instruction.getX(); i += 1) {
this.memory[this.i + i] = this.v[i];
}
}
Copy the value of the registers starting from V0 to Vx. Each value is copied over to location I + register value in memory.
Fx65 - LD Vx, [I]
operationCodeF65(instruction) {
for (let i = 0; i <= instruction.getX(); i += 1) {
this.v[i] = this.memory[this.i + i];
}
}
Copy the values from memory starting at I to the register V0 to Vx.
Sound and delay timers
updateTimers() {
if (this.delayTimer > 0) {
this.delayTimer -= 1;
}
if (this.soundTimer > 0) {
if (this.soundTimer === 1) {
if (!this.soundOff) {
this.sound.start();
}
}
this.soundTimer -= 1;
}
}
The delay timer is active whenever the delay timer register is not zero. The timer does nothing more than subtract 1 in each cycle if its bigger than 0. When it reaches 0 it's deactivated.
The sound timer aspect of the function is a bit different than what's stated in the technical documentation and the reason for that is just purely decision making. You can program it in the same way as specified in the documentation, however, this one does just as fine (if not better). We only let the sound play when we hit 1 because we don't want the sound to continue playing for many seconds and minutes.
When the sound is started, it will also automatically turn itself off after the beep, instead of relying on the sound timer being at 0.
Loading fonts
loadFontsIntoState() {
const fonts = [
// 0
0xF0, 0x90, 0x90, 0x90, 0xF0,
// 1
0x20, 0x60, 0x20, 0x20, 0x70,
// 2
0xF0, 0x10, 0xF0, 0x80, 0xF0,
// 3
0xF0, 0x10, 0xF0, 0x10, 0xF0,
// 4
0x90, 0x90, 0xF0, 0x10, 0x10,
// 5
0xF0, 0x80, 0xF0, 0x10, 0xF0,
// 6
0xF0, 0x80, 0xF0, 0x90, 0xF0,
// 7
0xF0, 0x10, 0x20, 0x40, 0x40,
// 8
0xF0, 0x90, 0xF0, 0x90, 0xF0,
// 9
0xF0, 0x90, 0xF0, 0x10, 0xF0,
// A
0xF0, 0x90, 0xF0, 0x90, 0x90,
// B
0xE0, 0x90, 0xE0, 0x90, 0xE0,
// C
0xF0, 0x80, 0x80, 0x80, 0xF0,
// D
0xE0, 0x90, 0x90, 0x90, 0xE0,
// E
0xF0, 0x80, 0xF0, 0x80, 0xF0,
// F
0xF0, 0x80, 0xF0, 0x80, 0x80,
];
for (let i = 0; i < fonts.length; i += 1) {
this.memory[i] = fonts[i];
}
}
We load the fonts into memory starting from location 0 in memory. The font hexadecimal representation can be found in the technical documentation.
keyboard.js
import autoBind from "../utils/autobinder.js";
export default class Keyboard {
constructor() {
autoBind(this);
this.keysPressed = [];
this.onNextKeyPress = function () {};
// custom mapping from key pressed to hex
// does not follow the technical reference mapping
this.keyMapping = {
0x0: 'X',
0x1: '1',
0x2: '2',
0x3: '3',
0x4: 'Q',
0x5: 'W',
0x6: 'E',
0x7: 'A',
0x8: 'S',
0x9: 'D',
0xA: 'Z',
0xB: 'C',
0xC: '4',
0xD: 'R',
0xE: 'F',
0xF: 'V',
};
}
}
We set up the key mapping for our keyboard and then initiate some variables. We make sure to bind all the functions to the class as well so that it can be used by event handlers as well.
Key is pressed
keyDown(event) {
// decode from code to string
const key = String.fromCharCode(event.which);
// insert into keysPressed dictionary
this.keysPressed[key] = true;
// find out which key from the mapping was pressed
Object.entries(this.keyMapping).forEach(([oKey, oVal]) => {
const keyCode = this.keyMapping[oVal];
// if key pressed exists
if (keyCode === key) {
// execute the onNextKeyPress then change the variable value
this.onNextKeyPress(parseInt(oKey, 10));
this.onNextKeyPress = function () {};
}
});
}
We run the "onNextKeyPress" function and then change its value. We set the value of the function in the chip8 model, considering the execution of the function can change, we want to make sure we don't execute the wrong function causing issues.
Key press is lifted
keyUp(event) {
const key = String.fromCharCode(event.which);
this.keysPressed[key] = false;
}
We set the value in the "keysPressed" dictionary to false, so we know it's not pressed.
Is the key pressed
isKeyPressed(keyCode) {
const keyPressed = this.keyMapping[keyCode];
// return the values truthy value
return !!this.keysPressed[keyPressed];
}
The double exclamation is the way to find the truthy value. Because if something does not exist, and with only one exclamation mark, it would return "true" despite it being "false", so we add the extra exclamation mark.
Clear the pressed keys dictionary
clear() {
this.keysPressed = [];
}
Clear the dictionary of keys pressed.
Screen.js
export default class Screen {
constructor() {
this.displayWidth = 64;
this.displayHeight = 32;
this.resolution = this.displayHeight * this.displayWidth;
// the display memory array
this.screen = new Array(this.resolution);
this.canvas = null;
this.blinkReductionLevel = 0;
this.vfFrame = 0;
this.skipped = 0;
}
}
In the constructor, we set up the screen width, height, and resolution so we can fill it out and attach it to the canvas.
Attach the canvas
setCanvas(context, scale) {
this.scale = scale || 10;
this.width = context.canvas.width = this.displayWidth * this.scale;
this.height = context.canvas.height = this.displayHeight * this.scale;
this.canvas = context;
}
We get the context passed and we then set its width and height. We scale up the width and height so it looks bigger when we look at it than the actual size in the chip8.
Set pixels on screen
setPixels(x, y) {
// if pixels overflow dimensions
// wrap around
if (x > this.displayWidth) {
x -= this.displayWidth;
}
if (x < 0) {
x += this.displayWidth;
}
if (y > this.displayHeight) {
y -= this.displayHeight;
}
if (y < 0) {
y += this.displayHeight;
}
// get the exact location of the bit on the (x, y) axis
const location = x + (y * this.displayWidth);
// XOR it into the screen
this.screen[location] ^= 1;
return this.screen[location];
}
First, we make sure to wrap around if we overflow, in both directions. Then we find the exact location of the pixel and bitwise XOR it into the screen.
Blink reduction
blinkReduction() {
// if there is a blink reduction level
if (this.blinkReductionLevel) {
// if blink reduction is level 2
// and there is more than 1 skip and vfframe is 0
if (this.blinkReductionLevel === 2 && this.skipped > 1 && !this.vfFrame) {
// if it was skipped over then we reduce the skipped number
if (this.skipped === 2) {
this.skipped = 0;
} else {
this.skipped -= 1;
}
return true;
}
// if Vf frame is 1
// we skip rendering this time
if (this.vfFrame) {
this.skipped += 1;
return true;
}
this.skipped = 0;
}
return false;
}
We reduce blinking by not inserting a pixel or updating the screen if the Vf register is set to 1. And we set that in the Dxyn instruction.
Render the screen
render() {
// if we have to skip a render
// due to blink reduction skip
// then return
if (this.blinkReduction()) return;
let x;
let y;
// clear the screen
this.canvas.clearRect(0, 0, this.width, this.height);
// rerender whole canvas based on the resolution and display memory
for (let i = 0; i < this.resolution; i += 1) {
x = (i % this.displayWidth) * this.scale;
y = Math.floor(i / this.displayWidth) * this.scale;
if (this.screen[i]) {
this.canvas.fillStyle = '#FFFFFF';
this.canvas.fillRect(x, y, this.scale, this.scale);
}
}
}
With this function, we just re-render the screen to reflect our display/resolution.
Clear the screen
clearScreen() {
for (let i = 0; i < this.screen.length; i += 1) {
this.screen[i] = 0;
}
}
Clears the screen as the name suggests.
Sound.js
import autoBind from './../utils/autobinder.js';
export default class Sound {
constructor(ctxClass, frequency) {
autoBind(this);
this.CtxClass = ctxClass;
this.frequency = frequency;
if (ctxClass) {
this.context = new this.CtxClass();
// creates GainNode to control volume
this.gain = this.context.createGain();
this.gain.connect(this.context.destination);
}
}
}
We set up the audio context for the browser so we can use it to play a sound. We can also use a different context and different frequencies. You can read more about it here
Start
start() {
this.oscillator = this.context.createOscillator();
this.oscillator.frequency.value = this.frequency || 440;
this.oscillator.type = this.oscillator.TRIANGLE;
this.oscillator.connect(this.gain);
this.oscillator.start();
setTimeout(this.stop, 100);
}
We create an oscillator to play the sound. Then we stop it after 100 milliseconds. It's better this way so it can play the sound and then stop by itself than if we start and stop it by the chip8 instructions. Makes the sound less annoying. Notice we use the triangle sound here but you can change this to whichever one you prefer.
Stop
stop() {
this.oscillator.stop();
this.oscillator.disconnect();
}
We simply stop the sound and disconnect.
Disassembler.js
export default function disassemble(instruction) {
switch (instruction.getCatagory()) {
case 0x0:
switch (instruction.getSubCatagory()) {
case 0xE0: return ['00E0', 'Display', 'Clear screen'];
case 0xEE: return ['00EE', 'Flow', 'Returns from subroutine'];
default: break;
}
break;
case 0x1: return ['1NNN', 'FLow', `Jumps to address 0x${instruction.getAddr().toString(16)}`];
case 0x2: return ['2NNN', 'Flow', `Calls subroutine at 0x${instruction.getAddr().toString(16)}`];
case 0x3: return ['3XNN', 'Cond', `Skips next instruction if V[0x${instruction.getX().toString(16)}] == 0x${instruction.getKK().toString(16)}`];
case 0x4: return ['4XNN', 'Cond', `skips next instruction if V[0x${instruction.getX().toString(16)}] != 0x${instruction.getKK().toString(16)}`];
case 0x5: return ['5XY0', 'Cond', `Skips next instruction if V[0x${instruction.getX().toString(16)}] == V[0x${instruction.getY().toString(16)}]`];
case 0x6: return ['6XNN', 'Const', `Sets V[0x${instruction.getX().toString(16)}] = 0x${instruction.getKK().toString(16)}`];
case 0x7: return ['7XNN', 'Const', `Adds v[0x${instruction.getX().toString(16)}] += 0x${instruction.getKK().toString(16)}`];
case 0x8:
switch (instruction.getSubCatagory()) {
case 0x0: return ['8XY0', 'Assign', `Assign V[0x${instruction.getX().toString(16)}] = V[0x${instruction.getY().toString(16)}]`];
case 0x1: return ['8XY1', 'BitOp', `Assigns V[0x${instruction.getX().toString(16)}] |= V[0x${instruction.getY().toString(16)}]`];
case 0x2: return ['8XY2', 'BitOp', `Assigns V[0x${instruction.getX().toString(16)}] &= v[0x${instruction.getY().toString(16)}]`];
case 0x3: return ['8XY3', 'BitOp', `Assigns V[0x${instruction.getX().toString(16)}] ^= v[0x${instruction.getY().toString(16)}]`];
case 0x4: return ['8XY4', 'Math', `Adds V[0x${instruction.getX().toString(16)}] += V[0x${instruction.getX().toString(16)}]`];
case 0x5: return ['8XY5', 'Math', `Subtracts v[0x${instruction.getX().toString(16)}] -= v[0x${instruction.getY().toString(16)}]`];
case 0x6: return ['8XY6', 'BitOp', `Shift v[0x${instruction.getX().toString(16)}] >> 1. Set v[${15}] = v[0x${instruction.getX().toString(16)}] & 1`];
case 0x7: return ['8XY7', 'Math', `v[0x${instruction.getX().toString(16)}] = v[0x${instruction.getY().toString(16)}] - v[0x${instruction.getX().toString(16)}]`];
case 0xE: return ['8XYE', 'BitOp', `Shift v[0x${instruction.getX().toString(16)}] << 1. Set v[${15}] = v[0x0${instruction.getX().toString(16)}] & 1`];
default: break;
}
break;
case 0x9: return ['9XY0', 'Cond', `Skips next instruction if v[0x${instruction.getX().toString(16)}] != v[0x${instruction.getY().toString(16)}]`];
case 0xA: return ['ANNN', 'MEM', `Sets I to address 0x${instruction.getAddr().toString(16)}`];
case 0xB: return ['BNNN', 'Flow', `Jumps to address 0x${instruction.getAddr().toString(16)} + v[0x${0}]`];
case 0xC: return ['CxNN', 'Rand', `Sets v[0x${instruction.getX().toString(16)}] = random(0,255) & 0x${instruction.getKK().toString(16)}`];
case 0xD:
return [
'DXYN',
'Disp',
`Draw sprite at (v[0x${instruction.getX().toString(16)}], v[0x${instruction.getY().toString(16)}]) with ${instruction.getSubCatagory().toString(16)} of height and 8 of width from memory location I`];
case 0xE:
switch (instruction.getSubCatagory()) {
case 0x1: return ['EXA1', 'KeyOp', `Skips next instruction if keyPressed() == v[0x${instruction.getX().toString(16)}]`];
case 0xE: return ['EX9E', 'KeyOp', `Skips next instruction if keyPressed != v[0x${instruction.getX().toString(16)}]`];
default: break;
}
break;
case 0xf:
switch (instruction.getKK()) {
case 0x07: return ['FX07', 'Timer', `Sets v[0x${instruction.getX().toString(16)}] = delay_timer`];
case 0x0A: return ['FX0A', 'KeyOp', `Sets v[0x${instruction.getX().toString(16)}] = KeyPressed()`];
case 0x15: return ['FX15', 'Timer', `Sets delay_timer = v[0x${instruction.getX().toString(16)}]`];
case 0x18: return ['FX18', 'Sound', `Sets sound_timer = v[0x${instruction.getX().toString(16)}]`];
case 0x1E: return ['FX1E', 'MEM', `Adds I += v[0x${instruction.getX().toString(16)}]`];
case 0x29: return ['FX29', 'MEM', `Sets I = v[0x${instruction.getX().toString(16)}] * 5`];
case 0x33: return ['FX33', 'BCD', `Sets I, I+1, I+2 to v[0x${instruction.getX().toString(16)}]`];
case 0x55: return ['FX55', 'BCD', `Empties all registers from v[0x0] to v[0x${instruction.getX().toString(16)}]`];
case 0x65: return ['FX65', 'BCD', `Sets all registers from v[0x0] to v[0x${instruction.getX().toString(16)}]`];
default: break;
}
break;
default: break;
}
}
The disassembler is just one giant switch statement that computes the register values and returns an array of error related strings. This is very useful for debugging purposes and nothing more.
Chip8wrapper.js
import Chip8 from './models/chip8.js';
import Sound from './models/sound.js';
import autoBind from './utils/autobinder.js';
export default class Chip8Wrapper {
constructor(debugFunc) {
autoBind(this);
this.debugFunc = debugFunc || function () {};
this.loop = 0;
this.chip8 = new Chip8();
this.ROMS = [];
this.keyDownEvent = this.chip8.keyboard.keyDown;
this.keyUpEvent = this.chip8.keyboard.keyUp;
this.debugNumBase = 16;
}
}
The "Chip8Wrapper" is a wrapper class around our Chip8 model that helps with extra functionality such as debug number base, running a debug function, having the ROM names in an array, etc. It's what our front-end button clicks will interact with.
Emulator cycle Loop
emulateCycle() {
this.chip8.emulateCycle();
}
emuCycleLoop() {
this.emulateCycle();
this.debugFunc(this.chip8, this.debugNumBase);
this.loop = requestAnimationFrame(this.emuCycleLoop);
}
The "emulateCycle" function will emulate a cycle in the chip8 model. The "emuCycleLoop" function emulates the cycle, runs the debug function, then creates a loop by calling the "requestAnimationFrame". The "requestAnimationFrame" function calls the function we pass in to be executed before the next animation repaint, and for it to continue in a loop, the function we pass to it must call it again passing itself.
Start and stop emulator cycle loop
startEmuCycleLoop() {
this.loop = requestAnimationFrame(this.emuCycleLoop);
}
stopEmuCycleloop() {
cancelAnimationFrame(this.loop);
}
These two functions just start or stop the loop we have made earlier. We have these functions attached to buttons on the front-end so we can start/stop the Chip8.
Pause Emulator
pauseEmu() {
if (this.chip8.pause === false) {
this.chip8.pause = true;
} else {
this.chip8.pause = false;
}
}
The function made to pause the chip8 in its track wherever that is. The state will all be kept as it is so you can restart it from where you left off.
Change emulator speed
setEmuSpeed(speed) {
this.chip8.speed = speed;
}
Just a setter function for the emulator cycle speed.
Enable emulator sound and blink reduction
setEmuSoundEnabled(sound) {
this.chip8.soundOff = sound;
}
setEmuScreenBlinkLevel(blinkLevel) {
if (blinkLevel < 0 || blinkLevel > 3) throw new Error('invalid blink level');
this.chip8.screen.blinkReductionLevel = blinkLevel;
}
Setter functions for the chip8 sound and blink level.
Change debug number-base and function
setEmuDebugNumBase(numBase) {
if (numBase !== 10 || numBase !== 16) throw new Error('Invalid number base');
this.debugNumBase = numBase;
}
setEmuDebugFunc(debugFunc) {
this.debugFunc = debugFunc;
}
Setter functions for the debug number base and debug function.
Change emulator sound and canvas context
setEmuSoundCtx(context) {
this.chip8.sound = new Sound(context);
}
setEmuCanvasCtx(canvas) {
this.chip8.screen.setCanvas(canvas);
}
Setter functions for the screen canvas and sound context.
Emulator key down
emuKeyDown(charCode) {
this.chip8.keyboard.keyDown({ which: charCode });
}
This is just the wrapper function that calls the keyboard "keyDown" function with the character code passed in. This is so it can be used as an interface.
Fetch the ROM names
async loadROMNames() {
const fetchUrl = 'https://balend.github.io/chip8-emulator-js/roms/names.txt'
const romNames = await fetch(fetchUrl);
// resolve body to UTF-8 string
const bodyString = await romNames.text();
// split string by comma
// trim and turn each string to uppercase
const names = bodyString.split(',').map((name) => name.trim().toUpperCase());
// filter out empty strings
const filteredNames = names.filter((name) => name !== '');
this.ROMS = filteredNames;
}
We fetch the ROM names from the names.txt file we generate and then, attach it to the chip8 ROMs property for use on the front-end ROM selection.
Load ROM into the emulator
async loadROM(name) {
// fetch the ROM data
const url = `https://balend.github.io/chip8-emulator-js/roms/${name.toLowerCase()}`;
const ROMData = await fetch(url);
// reset the state and load ROM d ata into memory
this.stopemuCycleloop();
this.chip8.resetState();
this.chip8.loadROM(new Uint8Array(await ROMData.arrayBuffer()));
// start the cycle
this.startEmuCycleLoop();
}
We fetch the desired ROM using the fetch API and load the ROM data into the Chip8 memory.
Programming the front-end
Script.js
import Chip8Wrapper from './chip8wrapper.js';
(async function () {
const canvas = document.querySelector('canvas');
const romSelector = document.getElementById('rom_selector');
const speedSelector = document.getElementById('speed_selector');
const blinkSelector = document.getElementById('blink_selector');
const numBaseSelector = document.getElementById('numBase_selector');
const debugRegCheckbox = document.getElementById('register-debug-checkbox');
const pcCheckbox = document.getElementById('pc-checkbox');
const spCheckbox = document.getElementById('sp-checkbox');
const iCheckbox = document.getElementById('i-checkbox');
const soundOffCheckbox = document.getElementById('sound-off-checkbox');
const soundOnCheckbox = document.getElementById('sound-on-checkbox');
const btnStart = document.getElementById('btn-start');
const btnPause = document.getElementById('btn-pause');
const btnStep = document.getElementById('btn-step');
const btnKeys = document.querySelectorAll('#keyBtn');
const txtElements = document.querySelectorAll('p, h5, select, button, h1, th, span');
const registerTable = document.getElementById('register-table');
const registerTableData = registerTable.getElementsByTagName('td');
const debugExtras = document.getElementById('debug-extra');
// to check if there are exactly 0 or extra debug elements are shown
let rowCounter = 0;
// start up the emulator and assign things needed
const emulator = new Chip8Wrapper();
emulator.setEmuCanvasCtx(canvas.getContext('2d'));
await emulator.loadROMNames();
emulator.setEmuSoundCtx(window.AudioContext
|| window.webkitAudioContext
|| window.mozAudioContext
|| window.oAudioContext
|| window.msAudioContext);
/**
* Create or remove table cells
* @param {string} id id to assign cell after creation
* @param {boolean} checked whether a checkbox is checked or not
* @param {number} cellWidth width size of the cell to create
* @param {number} creationNumBase to determine what the cell should contain
*/
const extraDebugCheckbox = function (id, checked, cellWidth, creationNumBase) {
if (checked) {
const thTr = document.getElementById('extra-debug-th-tr');
const tdTr = document.getElementById('extra-debug-td-tr');
const headerCell = thTr.insertCell();
headerCell.id = `${id}-th`;
headerCell.classList.add('uppercase');
headerCell.width = cellWidth;
headerCell.innerHTML = id;
const dataCell = tdTr.insertCell();
dataCell.width = cellWidth;
dataCell.id = `${id}-td`;
if (creationNumBase === 10) {
dataCell.innerHTML = '00';
} else {
dataCell.innerHTML = '0x';
}
rowCounter += 1;
} else {
const Th = document.getElementById(`${id}-th`);
const Td = document.getElementById(`${id}-td`);
Th.remove();
Td.remove();
rowCounter -= 1;
}
const extraDebugResterTable = document.getElementById('extra-register-table');
if (rowCounter === 0) {
extraDebugResterTable.classList.add('hidden');
} else if (rowCounter > 0) {
extraDebugResterTable.classList.remove('hidden');
}
};
debugRegCheckbox.addEventListener('change', (event) => {
if (event.target.checked) {
// show the tables
registerTable.classList.remove('hidden');
debugExtras.classList.remove('hidden');
emulator.setEmuDebugFunc((state, numBase) => {
let prefix = '';
if (numBase === 16) {
prefix = '0x';
}
// write out each V register from 0 to f to the cells
for (let i = 0; i < 15; i += 1) {
registerTableData[i].innerHTML = `${prefix}${state.v[i].toString(numBase)}`;
}
// write out the stack pointer, program counter, and i addresses and registers
const sp = document.getElementById('sp-td');
if (sp) {
sp.innerHTML = `${prefix}${state.stackPointer.toString(numBase)}`;
}
const pc = document.getElementById('pc-td');
if (pc) {
pc.innerHTML = `${prefix}${state.programCounter.toString(numBase)}`;
}
const MemAddr = document.getElementById('i-td');
if (sp) {
MemAddr.innerHTML = `${prefix}${state.i.toString(numBase)}`;
}
});
} else {
registerTable.classList.add('hidden');
debugExtras.classList.add('hidden');
emulator.setEmuDebugCallback(() => {});
}
});
soundOffCheckbox.addEventListener('change', (event) => {
if (event.target.checked) {
emulator.setEmuSoundEnabled(true);
}
});
soundOnCheckbox.addEventListener('change', (event) => {
if (event.target.checked) {
emulator.setEmuSoundEnabled(false);
}
});
pcCheckbox.addEventListener('change', (event) => {
extraDebugCheckbox('pc', event.target.checked, '85px', emulator.debugNumBase);
});
spCheckbox.addEventListener('change', (event) => {
extraDebugCheckbox('sp', event.target.checked, '85px', emulator.debugNumBase);
});
iCheckbox.addEventListener('change', (event) => {
extraDebugCheckbox('i', event.target.checked, '80px', emulator.debugNumBase);
});
numBaseSelector.addEventListener('change', (event) => {
const numBase = parseInt(event.target.value, 10);
const extraDebugTable = document.getElementById('extra-register-table').getElementsByTagName('td');
if (numBase === 10) {
for (let i = 0; i < registerTableData.length; i += 1) {
registerTableData[i].innerHTML = '0';
}
for (let i = 0; i < extraDebugTable.length; i += 1) {
extraDebugTable[i].innerHTML = '0';
}
} else if (numBase === 16) {
for (let i = 0; i < registerTableData.length; i += 1) {
registerTableData[i].innerHTML = '0x';
}
for (let i = 0; i < extraDebugTable.length; i += 1) {
extraDebugTable[i].innerHTML = '0x';
}
}
emulator.setEmuDebugNumBase(numBase);
});
romSelector.addEventListener('change', () => {
if (btnStart.textContent !== 'START') {
btnPause.click();
btnStart.textContent = 'START';
}
});
for (let i = 0, romsCount = emulator.ROMS.length; i < romsCount; i += 1) {
const option = document.createElement('option');
const rom = emulator.ROMS[i];
option.value = option.innerHTML = rom;
romSelector.appendChild(option);
}
blinkSelector.addEventListener('change', (event) => {
emulator.setEmuScreenBlinkLevel(event.target.value);
});
speedSelector.addEventListener('change', (event) => {
emulator.setEmuSpeed(parseInt(event.target.value, 10));
});
window.addEventListener('keydown', emulator.keyDownEvent, false);
window.addEventListener('keyup', emulator.keyUpEvent, false);
btnStart.addEventListener('click', () => {
const name = romSelector.options[romSelector.selectedIndex].text;
emulator.loadROM(name);
canvas.focus();
if (btnStart.textContent === 'START') {
btnStart.textContent = 'RESTART';
}
if (btnPause.textContent === 'RESUME') {
btnPause.classList.remove('is-primary');
btnPause.textContent = 'PAUSE';
btnStep.classList.add('is-disabled');
}
}, false);
btnPause.addEventListener('click', () => {
if (btnPause.textContent === 'PAUSE') {
btnPause.textContent = 'RESUME';
btnPause.classList.add('is-primary');
btnStep.classList.remove('is-disabled');
} else {
btnPause.classList.remove('is-primary');
btnPause.textContent = 'PAUSE';
btnStep.classList.add('is-disabled');
}
emulator.pauseEmu();
}, false);
btnStep.addEventListener('click', () => {
emulator.pauseEmu();
emulator.emulateCycle();
emulator.pauseEmu();
});
btnKeys.forEach((keyBtn) => {
keyBtn.addEventListener('mousedown', () => {
emulator.emuKeyDown(keyBtn.value.charCodeAt(0));
});
keyBtn.addEventListener('mouseup', () => {
emulator.emuKeyUp(keyBtn.value.charCodeAt(0));
});
});
txtElements.forEach((element) => {
if (element.tagName === 'SELECT') {
for (let i = 0; i < element.children.length; i += 1) {
element.children[i].classList.add('uppercase');
}
}
element.classList.add('uppercase');
});
}());
I don't have much commented and I don't feel like this needs any explanation. We do the most basic things in front-end JS, attaching button click listeners, finding certain elements and change their properties, use the Chip8 wrapper with button click listeners to change the chip8 model properties, etc. There is no advanced or complicated JavaScript going on here.
GameRomNames.js
const fs = require('fs');
const pathToFolder = './roms';
const pathToFileWithNames = './roms/names.txt';
fs.readdir(pathToFolder, (error, files) => {
if (error) {
console.log(error);
} else {
// for each rom in the roms folder
// add name to names.txt file
files.forEach(file => {
if (file !== 'names') {
fs.appendFile(pathToFileWithNames, `${file},`, (err) => {
if (err) {
console.log(err);
}
});
}
});
}
});
This file is a node.js script used to create our "names.txt" file. It's not used for anything else besides that.
Index.html
<!DOCTYPE html>
<html>
<head>
<title>chip8 emulator in js</title>
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
<link href="https://unpkg.com/nes.css/css/nes.css" rel="stylesheet" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link href="./stylesheet.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="flex-row space-between">
<di class="flex-row">
<i class="nes-icon is-medium heart header-icon-margin"></i>
<h1 class="header-txt">Made By <a class="header-txt" href="https://github.com/BalenD">BalenD<i
class="nes-icon github is-medium"></i></a></h1>
</di>
<div>
<h1 class="header-txt"><a class="header-txt" href="https://github.com/BalenD/chip8-emulator-js">Source Code<i
class="nes-icon coin is-medium"></i></a></h1>
</div>
</div>
</header>
<div class="container">
<div class="flex-row pad-top">
<div>
<table id="register-table" class="hidden nes-table is-bordered is-centered">
<tr>
<th>v[0]</th>
<th>v[1]</th>
<th>v[2]</th>
</tr>
<tr>
<td>0x</td>
<td>0x</td>
<td>0x</td>
</tr>
<tr>
<th>v[3]</th>
<th>v[4]</th>
<th>v[5]</th>
</tr>
<tr>
<td>0x</td>
<td>0x</td>
<td>0x</td>
</tr>
<tr>
<th>v[7]</th>
<th>v[8]</th>
<th>v[9]</th>
</tr>
<tr>
<td>0x</td>
<td>0x</td>
<td>0x</td>
</tr>
<tr>
<th>v[a]</th>
<th>v[b]</th>
<th>v[c]</th>
</tr>
<tr>
<td>0x</td>
<td>0x</td>
<td>0x</td>
</tr>
<tr>
<th>v[d]</th>
<th>v[e]</th>
<th>v[f]</th>
</tr>
<tr>
<td>0x</td>
<td>0x</td>
<td>0x</td>
</tr>
</tr>
</table>
<table id="extra-register-table" class="hidden nes-table is-bordered is-centered">
<tr id="extra-debug-th-tr"></tr>
<tr id="extra-debug-td-tr"></tr>
</table>
</div>
<div class="flex-column">
<button id="btn-start" class="nes-btn">start</button>
<button id="btn-pause" class="nes-btn">pause</button>
<button id="btn-step" class="nes-btn is-disabled">step</button>
</div>
</div>
<div class="header-icon-margin">
<canvas></canvas>
</div>
<div class="flex-column selection-and-key-area">
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>select which game to play</p>
</div>
</div>
game
</h5>
<div class="nes-select setWidth">
<select id="rom_selector">
</select>
</div>
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>the speed which the emulator runs a cycle. This might help if its too fast or slow in your browser</p>
</div>
</div>
cycle speed
</h5>
<div class="nes-select setWidth">
<select id='speed_selector'>
<option value="1">1</option>
<option value="5">5</option>
<option selected="selected" value="10">10</option>
<option value="16">16</option>
<option value="30">30</option>
<option value="60">60</option>
<option value="144">144</option>
</select>
</div>
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>Use these keys to play instead of the keyboard if you want to</p>
</div>
</div>
keys
</h5>
<table style="font-size: 30px;">
<tr>
<td><button id="keyBtn" value="1" class="nes-btn">1</button></td>
<td><button id="keyBtn" value="2" class="nes-btn">2</button></td>
<td><button id="keyBtn" value="3" class="nes-btn">3</button></td>
<td><button id="keyBtn" value="4" class="nes-btn">4</button></td>
</tr>
<tr>
<td><button id="keyBtn" value="Q" class="nes-btn">q</button></td>
<td><button id="keyBtn" value="W" class="nes-btn">w</button></td>
<td><button id="keyBtn" value="E" class="nes-btn">e</button></td>
<td><button id="keyBtn" value="R" class="nes-btn">r</button></td>
</tr>
<tr>
<td><button id="keyBtn" value="A" class="nes-btn">a</button></td>
<td><button id="keyBtn" value="S" class="nes-btn">s</button></td>
<td><button id="keyBtn" value="D" class="nes-btn">d</button></td>
<td><button id="keyBtn" value="F" class="nes-btn">f</button></td>
</tr>
<tr>
<td><button id="keyBtn" value="Z" class="nes-btn">z</button></td>
<td><button id="keyBtn" value="X" class="nes-btn">x</button></td>
<td><button id="keyBtn" value="C" class="nes-btn">c</button></td>
<td><button id="keyBtn" value="V" class="nes-btn">v</button></td>
</tr>
</table>
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>reduce blinking by vF frame skipping, this might cause lag/slow down for certain games. None is
recommended</p>
</div>
</div>
blinking
</h5>
<div class="nes-select setWidth">
<select id='blink_selector'>
<option selected="selected" value="0">NONE</option>
<option value="1">VF FRAME</option>
<option value="2">VF FRAME++</option>
</select>
</div>
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>select which debugging information to display</p>
</div>
</div>
Debugging
</h5>
<div class="flex-column">
<label>
<input id="register-debug-checkbox" type="checkbox" class="nes-checkbox" />
<span>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>display whats inside of the the 16 8-bit general purpose registers</p>
</div>
</div>
registers
</span>
</label>
<label>
<input id="debug-inst-checkbox" type="checkbox" class="nes-checkbox" />
<span>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>[DISABLED] display a history of ran instructions</p>
</div>
</div>
<span class="nes-text is-disabled">instructions</span>
</span>
</label>
</div>
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>disable or enable sound</p>
</div>
</div>
sound
</h5>
<div class="flex-row">
<label>
<input id="sound-off-checkbox" type="radio" class="nes-radio" name="answer" checked />
<span>off</span>
</label>
<label>
<input id="sound-on-checkbox" type="radio" class="nes-radio" name="answer" />
<span class="nes-text is-error">on</span>
</label>
</div>
<div id="debug-extra" class="hidden">
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>extra debugging information like the stack pointer, program pointer, etc</p>
</div>
</div>
extra debugging
</h5>
<div>
<label>
<input id="pc-checkbox" type="checkbox" class="nes-checkbox" />
<span>PC</span>
</label>
<label>
<input id="sp-checkbox" type="checkbox" class="nes-checkbox" />
<span>SP</span>
</label>
<label>
<input id="i-checkbox" type="checkbox" class="nes-checkbox" />
<span>I</span>
</label>
</div>
<h5>
<div class="tooltip">
<i class="nes-icon coin is-small"></i>
<div class="tooltip-text nes-container is-rounded">
<p>select to see number in hexadecimal or decimal</p>
</div>
</div>
number base
</h5>
<div class="nes-select setWidth">
<select id='numBase_selector'>
<option selected="selected" value="16">Hex</option>
<option value="10">decimal</option>
</select>
</div>
<div id="stars-area" style="display: flex;">
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<div class="flex-column">
<div>
<i class="nes-icon is-small star"></i>
</div>
<div>
<i class="nes-icon is-small star"></i>
</div>
</div>
</div>
<div id="stars-area" style="display: flex;">
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<i class="nes-icon is-medium star"></i>
<div class="flex-column">
<div>
<i class="nes-icon is-small star"></i>
</div>
<div>
<i class="nes-icon is-small star"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="./javascript/script.js"></script>
</body>
</html>
The only important part of the Index.html file is:
<canvas></canvas>
For our Chip8 to run on graphically. The other aspects of the file are all visual details and implementations, however, note that we are using a google font and we are also using the ness.css framework to style everything.
Stylesheet.css
body {
display: flex;
min-height: 100vh;
flex-direction: column;
margin: 0;
background-color: teal;
}
header, footer {
background: #242222;
height: 10vh;
}
header{
padding: 1em;
}
canvas {
border: 10px solid;
display: block;
margin: 0 auto;
float: left;
height: 520px;
width: 840px;
}
a:hover {
color: white;
text-decoration: none;
}
.container {
display: flex;
margin: 0px;
justify-content: left;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-column {
display: flex;
flex-direction: column;
}
.space-between {
justify-content: space-between;
}
.header-txt {
margin: 10px;
color: white;
}
.selection-and-key-area {
height: 542px;
padding-top: 10px;
flex-wrap: wrap;
}
.selection-and-key-area>* {
margin: 5px;
}
.header-icon-margin {
margin: 10px;
}
.popup {
visibility: hidden;
}
.popup:hover{
visibility: visible;
}
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black;
}
.tooltip .tooltip-text {
visibility: hidden;
position: absolute;
width: fit-content;
z-index: 1;
background: white;
}
.tooltip:hover .tooltip-text {
visibility: visible;
}
.uppercase {
text-transform: uppercase;
}
.pad-top {
padding-top: 20px;
}
.hidden {
visibility: hidden;
}
.setWidth {
width: 250px;
}
There are again just stylistic choices and implementations. You can copy-paste it, change it, or do as you wish with it.
Conclusion
In conclusion. It is pretty easy to build a Chip8 in javascript and can be a very fun and interesting challenge. The most difficult part about getting into emulator construction seems to be learning about the bitwise operation, memory management, and documentation finding and reading. Overall though, implementing Chip8 in any language makes you learn a lot of interesting and rarely used techniques of the language, which is very interesting and fun, especially if you are into learning languages in and out.