2

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)}`);
    }
};
  • Why does your code decrement the delay timer by 100 in the `updateTimers` function? AFAIK that is incorrect, and the timer logic should be completely independent of the frame rate, CPU clock speed, etc. – bugcatcher9000 Jul 14 '20 at 16:14

1 Answers1

0

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 to this.step and hits this line:

this.pc += 2

You should just add a conditional check before incrementing the PC by 2. Something like this should do.

In the opcode handler:

case 0x1000: {
    this.pc = nnn;
    this.advancePC = false;
    break;
}

In this.step:

CHIP8.prototype.step = function() {
    if (this.halted) return;
    if (this.haltUntilKeypress) return;

    let opcode = this.decodeOpcode(this.pc);
    this.executeOpcode(opcode, this.pc);
    if (this.advancePC) {
        this.pc += 2;
    }
};
bugcatcher9000
  • 201
  • 2
  • 5
  • Ok, now it's no longer getting stuck in an infinite loop of 0x3000, but there's another problem. On Brix, the game renders fine and actually no longer gets stuck in that 0x3000 loop, but the problem is that the ball never appears on screen and I cannot control the paddle. It seems like its getting stuck in another loop. My code: https://pastebin.com/B8797R5C Screenshot: https://imgur.com/a/vQqyi0q Thanks so much! –  Jul 15 '20 at 15:25
  • 1
    The main code looks pretty good now as far as I can tell without running it, but I suspect there is a problem with your draw method, which is not in that file. – bugcatcher9000 Jul 15 '20 at 18:35
  • My display code: https://pastebin.com/6Y5yNF6T My memory code: https://pastebin.com/BWY0uNV0 My utility functions: https://pastebin.com/P35tzyjd My error function: https://pastebin.com/QKEasCUg My index.js: https://pastebin.com/NehgFbF8 Thanks so much for your help! I hope that it's not annoying that I sent this much code. –  Jul 15 '20 at 18:57
  • 1
    Ah, I actually found the problem in your 0x2000 CALL instruction. You need to push `this.pc + 2` onto the stack instead of just `this.pc`, otherwise, the subroutine will be called again immediately after 0x00EE RET. – bugcatcher9000 Jul 15 '20 at 19:27
  • 1
    FYI, another way to solve that issue is to increment the PC immediately after the fetch, before the instruction is executed. – bugcatcher9000 Jul 15 '20 at 19:41
  • Thanks so much. Right now, the paddle is moving and the ball is bouncing, however, the ball leaves a trail behind it, and so does the paddle. Another problem is that collision detection is not working. Here's a screenshot: https://imgur.com/a/WuPKDKv –  Jul 15 '20 at 19:43
  • 1
    I was having a similar issue when I created mine in C back in April. I'll look into your drawing/collision detection code some more, but check out [my code](https://github.com/VibrantLettuce/Chip-8-Interpreter) for a reference. It's not perfect, but I did fix the trailing and collision detection issues I was having at the time! :) – bugcatcher9000 Jul 15 '20 at 19:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/217920/discussion-between-unpopularising-and-bugcatcher9000). –  Jul 15 '20 at 19:58