Я новичок в эмуляции и решил, что начнется написание интерпретатора CHIP-8. Однако я столкнулся с проблемой. При запуске игры, такой как, например, Brix, она dr aws the game не проблема (ракетка, et c.), Но после того, как это будет сделано, она просто застревает в al oop из 0x3000 и после этого , инструкция перехода, которая возвращает к 0x3000. Понятно, что 0x3000 неверно, и поэтому он зацикливается, но я не могу понять, почему это на всю жизнь.
Скриншот игры и Chrome консоли devtools (игра - это Brix, взято из здесь ): https://i.stack.imgur.com/a0wNM.png
На этом снимке экрана в консоли вы можете видеть, что 0x3000 не работает и переходит в прыжок, и этот переход возвращается к 0x3000, и цикл повторяется. Это происходит с большинством, если не со всеми играми. Я подозреваю, что это как-то связано с таймером задержки, поскольку 0x3000 проверяет v0 === 0, но он терпит неудачу и переходит к инструкции перехода.
Вот мой основной класс CHIP-8:
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)}`);
}
};