javascript - 指令 0x3000 和跳转指令在 CHIP-8 上的几乎所有游戏中重复
问题描述
我是仿真新手,想开始编写 CHIP-8 解释器。但是,我面临一个问题。运行游戏时,例如 Brix,它绘制游戏没有问题(桨等),但在完成后,它只是卡在 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)}`);
}
};
解决方案
您的问题似乎是您在 JMP 指令 (0x1nnn) 中分配 PC 后再次递增 PC(您可以在调试输出中看到差异)。所以在当前executeOpcode
循环之后,执行返回this.step
并命中这一行:
this.pc += 2
您应该在将 PC 增加 2 之前添加一个条件检查。应该这样做。
在操作码处理程序中:
case 0x1000: {
this.pc = nnn;
this.advancePC = false;
break;
}
在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;
}
};
推荐阅读
- jquery - 如何在数据属性 jquery 上转义单引号
- boost - CMake 的 find_package 仅包含标头库
- reactjs - 使用函数获取 API 数据并填充提供程序
- php - 如何创建包含上传图像的 Slack 消息?
- ios - 添加依赖后 Flutter 构建失败
- javascript - 为什么我的 var 为 null 并且代码不起作用
- c# - 从具有重复数据的 SQL 结果创建键值对
- c - 错误:编译 stdlib.h 时的未知类型名称“__float128”
- c++ - 仅使用 regex_search 返回第一个匹配项
- c# - 如何告诉编译器将 wwwroot 从一个项目复制到测试项目?