I am new to emulation and figured writing a CHIP-8 interpreter would get be started. However, I am facing an issue. When running a game, like Brix for example, it draws the game no problem (the paddle, etc.) but, after it is done, it just gets stuck in a loop of 0x3000 and after that, a jump instruction that jumps back to the 0x3000. It is clear that 0x3000 is false and that is why it is looping, but I can't figure why that is for the life of me.
Screenshot of the game and the Chrome devtools console (the game is Brix, taken from here): https://i.stack.imgur.com/a0wNM.png
In that screenshot, in the console, you can see the 0x3000 is failing and going to a jump, and that jump goes back to 0x3000, and the cycle repeats. This happens with most, if not all games. I suspect is has something to do with the delay timer, since 0x3000 is checking for v0 === 0, but it fails, and goes to the jump instruction.
Here is my main CHIP-8 class:
import { createMemory } from './memory.js';
import Display from './display.js';
import { CHIP8Error } from './error.js';
import { wait, toHex } from './utility.js';
export default class CHIP8 { constructor() {} }
CHIP8.prototype.init = function(displayX=64, displayY=32) {
this.display = new Display();
this.memory = createMemory(0xFFF, 'buffer', false); // Fill does not work with buffer
this.v = createMemory(0xF, 'uint8', 0);
this.I = 0;
this.stack = createMemory(0xF, 'uint16', 0);
this.halted = 1;
// Thanks to https://codereview.stackexchange.com/questions/190905/chip-8-emulator-in-javascript for the keymap
this.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
};
this.pressedKeys = {};
this.sp = 0;
this.pc = 0;
this.dt = 0;
this.st = 0;
this.display.init(displayX, displayY);
const fonts = [
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
];
fonts.forEach((v, i) => {
this.memory[i] = v;
});
};
CHIP8.prototype.load = function(program, programStart=0x200) {
for (let i = 0; i < program.length; i++) {
this.memory[programStart + i] = program[i];
}
this.pc = programStart;
this.programStart = programStart;
this.programEnd = programStart + program.length;
this.halted = 0;
};
CHIP8.prototype.updateTimers = function() {
// TODO: This function may not be complete
if (this.dt > 0) {
this.dt -= 100;
}
if (this.st > 0) {
// TODO: Add the sound
this.st--;
}
};
CHIP8.prototype.decodeOpcode = function(addr) {
let opcode = this.memory[addr];
opcode <<= 8;
opcode |= this.memory[addr+1];
return opcode;
};
CHIP8.prototype.step = function() {
if (this.halted) return;
if (this.haltUntilKeypress) return;
let opcode = this.decodeOpcode(this.pc);
this.executeOpcode(opcode, this.pc);
this.pc += 2;
};
CHIP8.prototype.tick = function() {
this.step();
this.updateTimers();
this.renderer.draw(this.display);
};
CHIP8.prototype.setSingleSteppingEnabled = function(enable=true) {
if (enable) {
this.noLoop = true;
} else {
this.noLoop = false;
}
};
CHIP8.prototype.run = async function() {
if (!this.renderer) {
CHIP8Error('Renderer not defined. Use setRenderer on the CHIP8 object in order to do so', true, undefined);
return;
}
while (this.pc <= this.programEnd) {
if (this.noLoop) {
await wait(1);
continue;
}
this.tick();
await wait(1000/60);
}
console.log('[CPU] Execution finished');
};
CHIP8.prototype.setRenderer = function(renderer) {
this.renderer = renderer;
// TODO: Move the init call somewhere else
this.renderer.init();
};
// Keyboard events
// NOTE: Need to be bound by user, with a .bind() to the chip8 instance!
// NOTE: The function getKeyboardEvent will do the .bind() for you, but will not actually bind the event
CHIP8.prototype.keydown = function(e) {
// Only for browsers
let keycode = e.keyCode;
let key = this.keyMap[keycode];
if (key) {
this.pressedKeys[key] = 1;
if (this.haltUntilKeypress) {
this.v[this.haltUntilKeypress] = key;
this.haltUntilKeypress = undefined;
}
console.log(`[CPU] [KEYBOARD EVENT] Keydown: ${key}`);
}
};
CHIP8.prototype.keyup = function(e) {
// Only for browsers
let keycode = e.keyCode;
let key = this.keyMap[keycode];
if (key) {
this.pressedKeys[key] = 0;
console.log(`[CPU] [KEYBOARD EVENT] Keyup: ${key}`);
}
};
CHIP8.prototype.getKeyboardEvent = function(event) {
switch (event) {
case 'keydown': {
return this.keydown.bind(this);
}
case 'keyup': {
return this.keyup.bind(this);
}
}
return;
};
CHIP8.prototype.dumpToConsole = function() {
console.warn('[DUMP] BEGIN DUMP');
console.log('[DUMP] Vx registers', this.v);
console.log('[DUMP] I register', toHex(this.I), 'Stack pointer', toHex(this.sp), 'Program counter', toHex(this.pc), 'ST', toHex(this.st), 'DT', toHex(this.dt));
console.log('[DUMP] Memory', this.memory);
console.log('[DUMP] Video memory', this.display.displayMemory);
console.log('[DUMP] Pressed keys', this.pressedKeys);
console.warn('[DUMP] END DUMP');
};
CHIP8.prototype.executeOpcode = function(opcode, addr) {
let firstNibble = opcode & 0xF000;
const nnn = opcode & 0x0FFF;
const n = opcode & 0x000F;
const x = (opcode & 0x0F00) >> 8;
const y = (opcode & 0x00F0) >> 4;
const kk = (opcode & 0x00FF);
console.log(`[CPU] [OPCODE] [EXECUTE] Opcode ${toHex(opcode)} at ${toHex(addr)}: firstNibble: ${toHex(firstNibble)}, nnn: ${toHex(nnn)}, n: ${toHex(n)}, x: ${toHex(x)}, y: ${toHex(y)}, kk: ${toHex(kk)}`);
switch (firstNibble) {
case 0x0000: {
switch (nnn) {
case 0x0E0: {
let displayX = this.display.xs;
let displayY = this.display.ys;
this.display.init(displayX, displayY);
this.renderer.clear();
break;
}
case 0x0EE: {
this.pc = this.stack[this.sp];
this.sp--;
break;
}
}
break;
}
case 0x1000: {
this.pc = nnn;
break;
}
case 0x2000: {
this.sp++;
this.stack[this.sp] = this.pc;
this.pc = nnn;
break;
}
case 0x3000: {
if (this.v[x] == kk) {
this.pc += 2;
}
break;
}
case 0x4000: {
if (this.v[x] !== kk) {
this.pc += 2;
}
break;
}
case 0x5000: {
if (this.v[x] === this.v[y]) {
this.pc += 2;
}
break;
}
case 0x6000: {
this.v[x] = kk;
break;
}
case 0x7000: {
this.v[x] += kk;
if (this.v[x] > 255) {
this.v[x] -= 256;
}
break;
}
case 0x8000: {
switch (n) {
case 0x0: {
this.v[x] = this.v[y];
break;
}
case 0x1: {
this.v[x] |= this.v[y];
break;
}
case 0x2: {
this.v[x] &= this.v[y];
break;
}
case 0x3: {
this.v[x] ^= this.v[y];
break;
}
case 0x4: {
this.v[x] += this.v[y];
if (this.v[x] > 255) {
this.v[x] -= 256;
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
break;
}
case 0x5: {
if (this.v[x] > this.v[y]) {
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
this.v[x] -= this.v[y];
if (this.v[x] < 0) {
this.v[x] += 256;
}
break;
}
case 0x6: {
this.v[0xF] = this.v[x] & 0x1;
this.v[x] >>= 1;
break;
}
case 0x7: {
if (this.v[x] > this.v[y]) {
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
this.v[x] = this.v[y] - this.v[x];
if (this.v[x] < 0) {
this.v[x] += 256;
}
break;
}
case 0xE: {
if (this.v[x] & 0x80) {
this.v[0xF] = 1;
} else {
this.v[0xF] = 0;
}
this.v[x] <<= 1;
if (this.v[x] > 255) {
this.v[x] -= 256;
}
break;
}
}
break;
}
case 0x9000: {
if (this.v[x] !== this.v[y]) {
this.pc += 2;
}
break;
}
case 0xA000: {
this.I = nnn;
break;
}
case 0xB000: {
this.pc = nnn + this.v[0x0];
break;
}
case 0xC000: {
this.v[x] = Math.floor(Math.random() * 256);
this.v[x] &= kk;
break;
}
case 0xD000: {
let xVal = this.v[x];
let yVal = this.v[y];
let height = n;
for (let i = 0; i < height; i++) {
let sprite = this.memory[this.I + i];
for (let j = 0; j < 8; j++) {
if ((sprite & 0x80) > 0) {
if (this.display.setPixel(xVal + j, yVal + i)) {
this.v[0xF] = 1;
}
}
sprite <<= 1;
}
}
break;
}
case 0xE000: {
switch (kk) {
case 0x9E: {
if (this.pressedKeys[this.v[x]] === 1) {
this.pc += 2;
}
break;
}
case 0xA1: {
if (this.pressedKeys[this.v[x]] !== 1) {
this.pc += 2;
}
break;
}
}
break;
}
case 0xF000: {
switch (kk) {
case 0x07: {
this.v[x] = this.dt;
break;
}
case 0x0A: {
this.haltUntilKeypress = x;
break;
}
case 0x15: {
this.dt = this.v[x];
break;
}
case 0x18: {
this.st = this.v[x];
break;
}
case 0x1E: {
this.I += this.v[x];
break;
}
case 0x29: {
this.I = this.v[x] * 5;
break;
}
case 0x33: {
// Thanks to github.com/reu/chip8.js
this.memory[this.I] = parseInt(this.v[x] / 100);
this.memory[this.I + 1] = parseInt(this.v[x] % 100 / 10);
this.memory[this.I + 2] = this.v[x] % 10;
break;
}
case 0x55: {
for (let i = 0; i <= x; i++) {
this.memory[this.i + i] = this.v[i];
}
break;
}
case 0x65: {
for (let i = 0; i <= x; i++) {
this.v[i] = this.memory[this.I + i];
}
break;
}
}
break;
}
default: {
CHIP8Error(`Invalid opcode ${toHex(opcode)} at address ${toHex(addr)}`, true, undefined);
break;
}
}
if (this.pc !== addr) {
console.log(`Jump to ${toHex(this.pc)}`);
}
};
It appears that your issue is that you are incrementing the PC again after assigning it in the JMP instruction (0x1nnn) (You can see the discrepancy in your debug output). So after the current
executeOpcode
cycle, the execution returns tothis.step
and hits this line:You should just add a conditional check before incrementing the PC by 2. Something like this should do.
In the opcode handler:
In
this.step
: