Lately I've had interest in an emulation. During my free time I worked in this chip 8 emulator.
I believe I have a good chip 8 implementation but my canvas use and draw functions may need a review.
Chip 8 Implementation:
var chip8 = function(){
var version = "1.0.0";
var chip8_fontset = new Uint8Array(
[
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
]
);
var chip8 = {
initialized: false,
opcode : 0,
memory : 0,
V : 0,
I : 0,
pc : 0,
gfx : 0,
delay_timer : 0,
sound_timer : 0,
stack : 0,
sp : 0,
key : 0,
romSize: 0,
dFlags: {
d: false,
x: 0,
y: 0,
w: 0,
h: 0
},
shouldRaisePC : true,
version: version
};
var self = chip8;
chip8.initialize = function() {
self.memory = new Uint8Array(4096);
self.V = new Uint8Array(16);
self.pc = 0x200;
self.opcode = 0;
self.I = 0;
self.sp = 0;
self.gfx = new Uint8Array(64 * 32);
self.stack = new Uint16Array(16);
self.key = new Uint8Array(16);
// Clear display
// Clear stack
// clear register V0-VF
// clear memory
// all those are not needed since we create new arrays for them :)
// load fontset
for(var i = 0; i < 80; i++){
self.memory[i] = chip8_fontset[i];
}
self.initialized = true;
};
chip8.emulateCycle = function() {
var s = self;
var opcode = s.memory[s.pc] << 8 | s.memory[s.pc + 1];
var i;
var X = (opcode & 0x0f00) >> 8;
var Y = (opcode & 0x00f0) >> 4;
var NNN = opcode & 0x0fff;
var NN = opcode & 0x00ff;
var N = opcode & 0x000f;
var VX = s.V[X];
var VY = s.V[Y];
var keycode;
var keystate;
if(s.delay_timer > 0)
--s.delay_timer;
if(s.sound_timer > 0)
--s.sound_timer;
console.log();
switch(opcode & 0xF000){
case 0x0000: // multiple things can happen there
switch(opcode & 0x000F){
case 0x0000: // 0x00e0: clears the screen
// do something there
logOpCode(opcode, "[sc]!!screen cleared!!");
for(i = 0; i < s.gfx.length; i++){
s.gfx[i] = 0;
}
s.dFlags.d = true;
s.dFlags.x = 0;
s.dFlags.y = 0;
s.dFlags.w = 64;
s.dFlags.h = 32;
break;
case 0x000e: // 0x00ee: returns from subroutine
logOpCode(opcode, "[st]stack decrement!!");
console.log("[st]old sp: " + s.sp + " old pc: " + s.pc);
s.pc = s.stack[--s.sp];
// I've commented this to stop an infinite loop of stack incremente and decrement
//s.shouldRaisePC = false;
console.log("[st]new sp: " + s.sp + " new pc " + s.pc);
break;
default:
logUnknowOp(opcode, s.pc);
break;
}
break;
// 1NNN: jumps to adress NNN
case 0x1000:
logOpCode(opcode, "[cf]goto");
s.pc = NNN;
s.shouldRaisePC = false;
console.log("[cf]Jumped to: " + s.pc);
break;
//0x2NNN: call subroutine at address NNN
case 0x2000:
logOpCode(opcode, "[st]stack increment!!");
console.log("[st]Stack[" + s.sp +"] = " + s.pc);
s.stack[s.sp++] = s.pc;
s.pc = NNN;
s.shouldRaisePC = false;
console.log("[st]changed pc: " + s.pc);
break;
// 0x3XNN: skips next instruction if VX==NN
case 0x3000:
logOpCode(opcode, "[cf]skip VX==NN");
console.log("[cf]VX: " + VX + " NN: "+ NN);
if(VX === NN){
s.pc+=4;
s.shouldRaisePC = false;
console.log("[cf]Skiped to pc: " + s.pc);
}
break;
// 0x4XNN: skips next instruction if VX!=NN
case 0x4000:
logOpCode(opcode, "[cf]skip !=");
console.log("[cf]VX: " + VX + " NN: "+ NN);
if(VX !== NN){
s.pc+=4;
s.shouldRaisePC = false;
console.log("[cf]Skiped to pc: " + s.pc);
}
break;
// 0x5XY0: skips instruction if VX==VY
case 0x5000:
logOpCode(opcode, "[cf]skip VX==VY");
console.log("[cf]VX: " + VX + " VY: "+ VY);
if(VX === VY){
s.pc+=4;
s.shouldRaisePC = false;
console.log("[cf]Skiped to pc: " + s.pc);
}
break;
// 6XNN: sets VX to NN
case 0x6000:
logOpCode(opcode, "[va]vx=nn" );
s.V[X] = NN;
console.log("[va]V["+ X+"]="+ NN);
break;
// 7XNN: adds to VX NN
case 0x7000:
logOpCode(opcode, "[va]vx+=nn" );
console.log("[va]V["+X+"]="+s.V[X]+" + " + NN);
s.V[X] += NN;
break;
case 0x8000: // multiple thing there
switch(opcode & 0x000f){
// 0x8XY0: assing vx to the value of vy
case 0x0000:
logOpCode(opcode, "[mt]VX=VY");
s.V[X] = VY;
console.log("[mt]V["+ X +"]="+ VY);
break;
// 0x8XY1: sets VX to (VX or VY)
case 0x0001:
logOpCode(opcode, "[bo]VX=VX|VY");
var OR = VX | VY;
s.V[X] = OR;
console.log("[bo]" + VX.toString(2));
console.log("[bo]" + VY.toString(2));
console.log("[bo]" + OR.toString(2));
break;
// 0x8XY2: VX = (VX and VY)
case 0x0002:
logOpCode(opcode, "[bo]VX=VX&VY");
var AND = VX & VY;
s.V[X] = AND;
console.log("[bo]" + VX.toString(2));
console.log("[bo]" + VY.toString(2));
console.log("[bo]" + AND.toString(2));
break;
// 0x8XY2: VX = (VX xor VY)
case 0x0003:
logOpCode(opcode, "[bo]VX=VX^VY");
var XOR = VX ^ VY;
s.V[X] = XOR;
console.log("[bo]" + VX.toString(2));
console.log("[bo]" + VY.toString(2));
console.log("[bo]" + XOR.toString(2));
break;
// 0x8XY4: vx += vy, sets vf to 1 if carry, 0 if not
case 0x0004:
logOpCode(opcode, "[mt]VX+=VY");
if(VY > (0xff - VX)){
s.V[0xf] = 1;
} else {
s.V[0xf] = 0;
}
s.V[X] += s.V[Y];
break;
// 0x8XY5: vx -= vy, sets vf to 0 if borrow, 1 if not
case 0x0005:
logOpCode(opcode, "[mt]VX-=VY");
s.V[0xf] = 1;
if(VY > VX)
s.V[0xf] = 0;
s.V[X] -= VY;
console.log("[mt]VX: " + VX + " VY: " + VY);
break;
//8XY6: VF = lsb, vx = vx >> 1
case 0x0006:
logOpCode(opcode, "[bo]VX = VX>>1");
var lsb = VY & 1;
s.V[X] = VY >> 1;
s.V[0xf]=lsb;
console.log("[bo]VX:" + VY + ">>1: " + (VY >> 1).toString(2) + " lsb: " + lsb );
break;
// 0x8XY7: vx=vy - vx, sets vf to 0 if borrow, 1 if not
case 0x0007:
logOpCode(opcode, "[mt]VX=VY-VX");
s.V[0xf] = 1;
if(VX>VY)
s.V[0xf] = 0;
s.V[X] = VY - VX;
console.log("[mt]VY: " + VY + " VX: " + VX);
break;
//8XYE: VF = msb, vx = VX << 1
// GOTTA add a simulation for the quirksss
case 0x000E:
logOpCode(opcode, "[bo]VX = VX<<1");
var msb = VX & 128;
s.V[X] = VX << 1;
s.V[0xf] = msb;
console.log("[bo]VX:" + VX + "<<1: " + (VX << 1).toString(2) + " msb: " + msb );
break;
default:
logUnknowOp(opcode, s.pc);
break;
}
break;
// 9XY0: skips iv VX!=VY
case 0x9000:
logOpCode(opcode, "[cf]skip VX!=VY");
VX = s.V[X];
VY = s.V[Y];
if(VX !== VY){
s.pc += 4;
s.shouldRaisePC = false;
console.log("[cf] pc skipped to: " + s.pc);
}
break;
// ANNN: sets I to the address NNN
case 0xa000:
// Execute opcode
logOpCode(opcode, "[mem]I=NNN");
s.I = opcode & 0x0FFF;
console.log("[mem]New I: " + s.I);
break;
// BNNN: jumps to NNN + V0
case 0xb000:
logOpCode(opcode, "[cf]pc=NNN + v0");
s.pc = s.V[0x0] + NNN;
s.shouldRaisePC = false;
console.log("[cf]new pc: " + s.pc);
break;
// CXNN: VX = rand & NN
case 0xc000:
logOpCode(opcode, "[rnd]VX = rand & NN");
var rnd = Math.floor(Math.random() * 256) & NN;
s.V[X] = rnd;
console.log("V[" + X +"] = " +rnd);
break;
// DXYN draws sprite
case 0xd000:
logOpCode(opcode, "[gfx]!!!Fucking draw!!!");
var xStart = s.V[X];
var yStart = s.V[Y];
console.log("[gfx]I: "+ s.I +" x: " + xStart + " y: " + yStart + " h: " + N);
var pixel;
s.V[0xF] = 0;
for(var yline = 0; yline < N; yline++){
pixel = s.memory[s.I + yline];
for(var xline = 0; xline < 8; xline++){
if((pixel & (128 >> xline)) !== 0) {
var gfxOffset = xStart + xline +((yStart + yline) * 64);
s.gfx[gfxOffset] ^= 1;
if(s.gfx[gfxOffset] === 0) s.V[0xf] = 1;
}
}
}
s.dFlags.d = true;
s.dFlags.x = xStart;
s.dFlags.y = yStart;
s.dFlags.w = 8;
s.dFlags.h = N;
break;
// keyboard things
case 0xe000:
switch(opcode & 0x00ff){
// EX9E: Skips the next instruction
// if the key stored in VX is pressed
case 0x009e:
logOpCode(opcode, "[kb]skip if vx pressed");
keycode = s.V[X];
keystate = s.key[keycode];
console.log("[kb]we are at pc: " + s.pc);
console.log("[kb]key["+ keycode +"] : " + keystate);
console.log("[kb]we should skip to:" + (s.pc+4));
if(keystate!==0){
s.pc += 4;
s.shouldRaisePC = false;
console.log("[kb]so we skiped to pc: " + s.pc);
}
break;
// EXA1: Skips the next instruction
// if the key stored in VX is not pressed
case 0x00a1:
logOpCode(opcode, "[kb]skip if vx not pressed");
keycode = s.V[X];
keystate = s.key[keycode];
console.log("[kb]key["+ keycode +"] : " + keystate);
if(keystate===0){
s.pc += 4;
s.shouldRaisePC = false;
console.log("[kb]so we skiped to pc: " + s.pc);
}
break;
default:
logUnknowOp(opcode, s.pc);
break;
}
break;
// multiple things
case 0xf000:
switch(opcode & 0x00ff){
// FX07: VX = delay timer
case 0x0007:
logOpCode(opcode, "[tm]VX = delay");
s.V[X] = s.delay_timer;
console.log("[tm]V["+X+"] = " +s.delay_timer);
break;
// FX0A: wait for key press (blocking)
case 0x000a:
logOpCode(opcode, "[kb]wait for keypress");
s.shouldRaisePC = false;
for(i = 0; i < s.key.length; i++){
if(s.key[i] === 1){
s.shouldRaisePC = true;
s.V[X] = i;
console.log("[kb]found at: " + s.key[i]);
break;
}
}
break;
// FX15: delay = VX
case 0x0015:
logOpCode(opcode, "[tm]delay = VX");
VX = s.V[X];
s.delay_timer = VX;
console.log("[tm]delay = " + VX);
break;
// FX18: sound = VX
case 0x0018:
logOpCode(opcode, "[tm]sound = VX");
VX = s.V[X];
s.sound_timer = VX;
console.log("[tm]sound = " + VX);
break;
// FX1E: I += VX
case 0x001e:
logOpCode(opcode, "[mem]I += VX");
VX = s.V[X];
s.I += VX;
s.V[0xf] = 0;
if(s.I > 0xfff)
s.V[0xf] = 1;
console.log("[mem]now I = " + s.I);
break;
// FX29: I = to font char of VX 0-f
case 0x0029:
logOpCode(opcode, "[mem]I = font char");
VX = s.V[X];
s.I = VX * 5;
console.log("[mem]now I = " + s.I);
break;
// FX33: too long to be described there check the chip8
// instruction set.
case 0x0033:
VX = s.V[X];
logOpCode(opcode);
s.memory[s.I] = Math.floor(VX / 100);
s.memory[s.I+1] = Math.floor(VX / 10) % 10;
s.memory[s.I+2] = (VX % 100) % 10;
break;
// FX55: starting at I = from V0 to VX
case 0x0055:
logOpCode(opcode, "[mem]starting at I = V0 to VX");
for(i = 0; i<=X; i++){
s.memory[s.I + i] = s.V[i];
}
s.I += VX + 1;
break;
// FX65: starting at I = from V0 to VX
case 0x0065:
logOpCode(opcode, "[mem]V0 to VX = starting at I");
for(i = 0; i<=X; i++){
s.V[i] = s.memory[s.I + i];
}
s.I += VX + 1;
break;
default:
logUnknowOp(opcode, s.pc);
break;
}
break;
default:
logUnknowOp(opcode, s.pc);
break;
}
console.log("old pc:" + s.pc);
if(s.shouldRaisePC)
s.pc += 2;
s.shouldRaisePC = true;
console.log("new pc:" + s.pc);
};
/**
*
*
*/
chip8.loadRom = function() {
s = self;
if(!s.initialized) {
alert("first initialize the instace of chip8");
return;
}
var file = document.getElementById("rom").files;
console.log("Rom name: " + file[0].name);
var view;
if(file.length < 1) {
alert("Please select a rom file");
return;
}
var reader = new FileReader();
reader.onload = function(e){
view = new DataView(e.target.result);
s.romSize = view.byteLength;
for(var i = 0; i < view.byteLength; i++){
s.memory[0x200 + i] = view.getUint8(i);
}
console.log("finished rom load to memory, loaded: "+ view.byteLength + " bytes");
};
reader.readAsArrayBuffer(file[0]);
};
var logOpCode = function(opcode, extra ){
extra = (typeof extra !== 'undefined') ? extra : "";
console.log("pc: " + self.pc + " know opcode: " + opcode.toString(16).toUpperCase() + extra);
};
var logUnknowOp = function(opcode, pc){
console.log("Unknown opcode: " + opcode.toString(16).toUpperCase() + " at pc: "+ pc);
};
logOpCode = function() {};
logUnknowOp = function(){};
return chip8;
};
Main loop, canvas draw and keystrokes:
// some setup
var c8 = chip8();
var ctx;
var control = {
fps : 120,
intervalId: 0
};
// key map
// Keypad Keyboard
// +-+-+-+-+ +-+-+-+-+
// |1|2|3|C| |1|2|3|4|
// +-+-+-+-+ +-+-+-+-+
// |4|5|6|D| |Q|W|E|R|
// +-+-+-+-+ => +-+-+-+-+
// |7|8|9|E| |A|S|D|F|
// +-+-+-+-+ +-+-+-+-+
// |A|0|B|F| |Z|X|C|V|
// +-+-+-+-+ +-+-+-+-+
var keyMap = {
49:0x1, 50:0x2, 51:0x3, 52:0xc,
81:0x4, 87:0x5, 69:0x6, 82:0xd,
65:0x7, 83:0x8, 68:0x9, 70:0xe,
90:0xa, 88:0x0, 67:0xb, 86:0xf
};
var keyDown = function(e){
var keycode = keyMap[e.keyCode];
if (typeof keycode !== 'undefined') {
c8.key[keycode] = 1;
}
};
var keyUp = function(e){
var keycode = keyMap[e.keyCode];
if (typeof keycode !== 'undefined') {
c8.key[keycode] = 0;
}
};
window.addEventListener("keydown", keyDown, false);
window.addEventListener("keyup", keyUp, false);
// our callback function that handles the udpates
var tick = function(){
// the keys are event bounded so no need for special code there
// look for window.addEventListener("keydown", keyDown, false);
c8.emulateCycle();
if(c8.dFlags.d)
requestAnimationFrame(draw.draw);
if(c8.pc > (c8.romSize + 0x200)){
console.log("exiting execution since theres no thing to be accesed outside of rom memory");
clearInterval(control.intervalId);
}
};
// load the room and starts the emulation
document.getElementById("load").onclick = function(){
console.clear();
//Set up render system and register input callbacks
initGFX();
// initialize things
c8.initialize();
c8.loadRom();
control.intervalId = setInterval(tick, 1000 / control.fps);
};
// stops emulation
document.getElementById("stop").onclick = function(){
clearInterval(control.intervalId)
};
// Canvas creation
var initGFX = function() {
var c = document.getElementById("screen");
ctx = c.getContext("2d");
draw.clearScren();
};
// Canvas abstraction
var draw = {};
// draws a point
draw.point = function(x, y, color){
var scaleFactor = 4;
ctx.fillStyle = color;
// in this case 4 cuz ive set the scale of the screen to 4x
ctx.fillRect(x * scaleFactor,y * scaleFactor, scaleFactor, scaleFactor);
};
// clear the screen
draw.clearScren = function() {
var scaleFactor = 4;
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, 64 * scaleFactor, 32 * scaleFactor)
};
// this is run everytime the draw flag is activated
draw.draw = function() {
var x = s.dFlags.x;
var y = s.dFlags.y;
var w = s.dFlags.w;
var h = s.dFlags.h;
var color;
for(var i = 0; i < w; i++){
for(var j = 0; j < h; j++){
var offset = x + i +((y + j) * 64);
if(c8.gfx[offset] === 1){
color = "#000";
} else {
color = "#fff";
}
draw.point(x + i, y + j, color);
}
}
s.dFlags.d = false;
};
//translates to uint into an opcode number
var t2Uint = function(a, b){
return (a << 8) | b;
};
//checks the opcode at certain memory spot
var g2mUint = function(start){
return t2Uint(c8.memory[start], c8.memory[start + 1]);
};
//gets the uint at certain memoery spot
var g1mUint = function(start){
return c8.memory[start];
};
//checks the sprite at certain memory spot
var gsprite = function(start, height){
for(var i = 0; i < height; i++){
console.log( g1mUint(start + (i*2) ).toString(2) );
}
};
The HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<input type="file" id="rom" name="rom" />
<a id="load" href="#">Load Rom</a>
<a id="stop" href="#">Stop</a>
<br />
<canvas id="screen" width="256" height="128" style="border: 1px solid black;"></canvas>
<script src="chip8.js"></script>
<script src="main.js"></script>
</body>
</html>