Last active
August 31, 2018 00:07
-
-
Save ericek111/d00a91b1293e9bffe4ae2945536ab474 to your computer and use it in GitHub Desktop.
[Squirrel] CS:GO CHIP-8 emulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ent_fire first_script runscriptfile first_map/first.nut;ent_fire first_script callscriptfunction ButtonPressed | |
// http://devernay.free.fr/hacks/chip8/C8TECH10.HTM | |
::toHex <- function (val) { | |
return format("%04X", val); | |
} | |
DoIncludeScript("first_map/data.nut", this) | |
class CPU { | |
scope = null; | |
stack = null; | |
reg = null; | |
mach = null; | |
mem = null; | |
debug_disp = null; | |
shouldRun = true; | |
operand = 0; | |
opDesc = ""; | |
constructor(scopeForThis, machine) { | |
mach = machine | |
mem = mach.mem | |
scope = scopeForThis | |
stack = array(16, 0) | |
reg = { | |
V = array(0x10, 0), // V0 - VF | |
I = 0, // index | |
PC = 0x200, // Program Counter | |
SP = 0, // Stack Pointer | |
DT = 0, // delay timer - wastes cycles by subtracting -1 if > 0 | |
ST = 0 // sound timer - (both) decremented at 60 Hz, if > 0, buzzer sounds | |
} | |
} | |
function cycle() { | |
if (!shouldRun) | |
return; | |
// fetch operand | |
operand = mem[reg.PC] << 8 | mem[reg.PC + 1] | |
// move PC to next instruction | |
reg.PC += 2 | |
// execute current instruction | |
exec() | |
if (reg.DT > 0) | |
reg.DT-- | |
if (reg.ST > 0) { | |
reg.ST-- | |
if (reg.ST == 0) | |
mach.playSound() | |
} | |
} | |
function exec() { | |
local x = (operand >> 8) & 0xF; | |
local y = (operand >> 4) & 0xF; | |
opDesc = format("db %02xh %02xh", operand >> 8, operand & 0xFF) | |
switch (operand >> 12) { // 0x0 - 0xF | |
case 0: | |
switch (operand & 0xFF) { | |
// CLS / 00E0 / Clear the display. | |
case 0xE0: | |
opDesc = "CLS" | |
mach.disp.clearScreen() | |
break; | |
// RET / 00EE / Return from a subroutine. | |
case 0xEE: | |
opDesc = "RET" | |
reg.PC = stack[reg.SP--] | |
break; | |
// SYS addr / 0nnn / Jump to a machine code routine at nnn. | |
default: | |
opDesc = format("SYS %xh", operand & 0xFFF) | |
// NOT IMPLEMENTED | |
} | |
break; | |
// JP addr / 1nnn / Jump to location nnn. | |
case 1: | |
opDesc = format("JP %xh", operand & 0xFFF) | |
reg.PC = operand & 0xFFF; | |
break; | |
// CALL addr / 2nnn / Call subroutine at nnn. | |
case 2: | |
opDesc = format("CALL %xh", operand & 0xFFF) | |
stack[reg.SP] = reg.PC | |
reg.SP++ | |
reg.PC = operand & 0xFFF | |
break; | |
// SE Vx, kk / 2xkk / Skip next instruction if VX == kk. | |
case 3: | |
opDesc = format("SE V%X, %d", x, operand & 0xFF) | |
if (reg.V[x] == (operand & 0xFF)) | |
reg.PC += 2 | |
break; | |
// SNE Vx, kk / 3xkk / Skip next instruction if VX != kk. | |
case 4: | |
opDesc = format("SNE V%X, %d", x, operand & 0xFF) | |
if (reg.V[x] != (operand & 0xFF)) | |
reg.PC += 2 | |
break; | |
// SE Vx, Vy / 5xy0 / Skip next instruction if VX == VY. | |
case 5: | |
opDesc = format("SE V%X, V%X", x, y) | |
if (reg.V[x] == reg.V[y]) | |
reg.PC += 2 | |
break; | |
// LD Vx. kk / 6xkk / Set register Vx to value kk. | |
case 6: | |
opDesc = format("LD V%X, %d", x, operand & 0xFF) | |
reg.V[x] = operand & 0xFF | |
break; | |
// ADD Vx, kk / 7xkk / Increment Vx by kk. | |
case 7: | |
opDesc = format("ADD V%X, %d", x, operand & 0xFF) | |
reg.V[x] += operand & 0xFF | |
reg.V[x] = reg.V[x] & 0xFF | |
break; | |
case 8: | |
switch (operand & 0xF) { | |
// LD Vx, Vy / 8xy0 / Store the value of register Vy in Vx. | |
case 0: | |
opDesc = format("LD V%X, V%X", x, y) | |
reg.V[x] = reg.V[y] | |
break; | |
// OR Vx, Vy / 8xy1 / Perform a bitwise OR on regs Vx and Vy. | |
case 1: | |
opDesc = format("OR V%X, V%X", x, y) | |
reg.V[x] = reg.V[x] | reg.V[y] | |
break; | |
// AND Vx, Vy / 8xy2 / Perform a bitwise AND on regs Vx and Vy. | |
case 2: | |
opDesc = format("AND V%X, V%X", x, y) | |
reg.V[x] = reg.V[x] & reg.V[y] | |
break; | |
// XOR Vx, Vy / 8xy3 / Perform a bitwise XOR on regs Vx and Vy. | |
case 3: | |
opDesc = format("XOR V%X, V%X", x, y) | |
reg.V[x] = reg.V[x] ^ reg.V[y] | |
break; | |
// ADD Vx, Vy / 8xy4 / Add Vy to Vx, set VF = carry. | |
case 4: | |
opDesc = format("ADD V%X, V%X", x, y) | |
reg.V[x] += reg.V[y] | |
if (reg.V[x] > 0xFF) | |
reg.V[0xF] = 1 | |
reg.V[x] = reg.V[x] & 0xFF | |
break; | |
// SUB Vx, Vy / 8xy5 / Subtract Vy from Vx, set VF = NOT borrow. | |
case 5: | |
opDesc = format("SUB V%X, V%X", x, y) | |
if (reg.V[x] > reg.V[y]) | |
reg.V[0xF] = 1 | |
reg.V[x] -= reg.V[y] | |
reg.V[x] = reg.V[x] & 0xFF | |
break; | |
// SHR Vx (, Vy) / 8xy6 / Shift Vx one bit right, put lowest bit into VF. | |
case 6: | |
opDesc = format("SHR V%X", x) | |
reg.V[0xF] = reg.V[x] & 1 | |
reg.V[x] = reg.V[x] >>> 1 | |
break; | |
// SUBN Vx, Vy / 8xy7 / Subtract Vx from Vy, store in Vx. VF = NOT borrow. | |
case 7: | |
opDesc = format("SUBN V%X, V%X", x, y) | |
if (reg.V[x] < reg.V[y]) | |
reg.V[0xF] = 1 | |
reg.V[x] = reg.V[y] - reg.V[x] | |
reg.V[x] = reg.V[x] & 0xFF | |
break; | |
// SHL Vx (, Vy) / 8xyE / Shift Vx one bit left, put highest bit into VF. | |
case 0xE: | |
opDesc = format("SHR V%X", x) | |
reg.V[0xF] = (reg.V[x] >> 7) & 1 | |
reg.V[x] = reg.V[x] << 1 | |
break; | |
default: | |
notRecognized(operand) | |
} | |
// SNE Vx, Vy / 9xy0 / Skip next instruction if Vx != Vy. | |
case 9: | |
opDesc = format("SNE V%X, V%X", x, y) | |
if (reg.V[x] != reg.V[y]) | |
reg.PC += 2 | |
break; | |
// LD I, nnn / Annn / Set I to nnn. | |
case 0xA: | |
opDesc = format("LD I, %d", operand & 0xFFF) | |
reg.I = operand & 0xFFF; | |
break; | |
// JP V0, addr / Bnnn / Jump to location V0 + nnn. | |
case 0xB: | |
opDesc = format("JP V0, %03X ", operand & 0xFFF) | |
reg.PC = reg.V[0] + operand & 0xFFF; | |
break; | |
// RND Vx, nn / Cxkk / Set register Vx to (random integer AND kk). | |
case 0xC: | |
opDesc = format("RND V%X, %02X", x, operand & 0xFF) | |
reg.V[x] = RandomInt(0, 255) & (operand & 0xFF) | |
break; | |
// DRW Vx, Vy, n / Dxyn / Display n-byte sprite from memory location I at (Vx, Vy), set VF = collision. | |
case 0xD: | |
opDesc = format("DRW V%X, V%X, %d", x, y, operand & 0xF) | |
reg.V[0xF] = 0 | |
local s = 0 | |
for (local py = 0; py < (operand & 0xF); py++) { | |
s = mem[reg.I + py]; | |
for (local px = 0; px < 8; px++) { | |
if ((s & 0x80) > 0) { | |
if (mach.disp.togglePixel(reg.V[x] + px, reg.V[y] + py)) | |
reg.V[0xF] = 1 | |
} | |
s = s << 1; | |
} | |
} | |
mach.disp.drawFlag = true | |
break; | |
case 0xE: | |
switch (opcode & 0xFF) { | |
// SKP Vx / Ex9E / Skip next instruction if key with index of Vx is pressed. | |
case 0x9E: | |
opDesc = format("SKP V%X", x) | |
// TODO: Vx > 0xF handling | |
if (mach.input.key[reg.V[x]] > 0) | |
reg.PC += 2; | |
break; | |
// SKNP Vx / ExA1 / Skip next instruction if key with index of Vx is NOT pressed. | |
case 0xA1: | |
opDesc = format("SKNP V%X", x) | |
if (mach.input.key[reg.V[x]] == 0) | |
reg.PC += 2; | |
break; | |
default: | |
notRecognized(operand) | |
} | |
case 0xF: | |
switch (operand & 0xFF) { | |
// LD Vx, DT / Fx07 / Store the value of register DT in Vx | |
case 0x07: | |
opDesc = format("LD V%X, DT", x) | |
reg.V[x] = reg.DT; | |
break; | |
// LD Vx, K / Fx0A / Wait for a key press, store value of the key in Vx. | |
case 0x0A: | |
opDesc = format("LD V%X, K", x) | |
// TODO | |
break; | |
// LD DT, Vx / Fx15 / Set register DT to value of Vx. | |
case 0x15: | |
opDesc = format("LD DT, V%X", x) | |
reg.DT = reg.V[x] | |
break; | |
// LD ST, Vx / Fx18 / Set register ST to value of Vx. | |
case 0x18: | |
opDesc = format("LD ST, V%X", x) | |
reg.ST = reg.V[x] | |
break; | |
// ADD I, Vx / Fx1E / Add value of register Vx to I. | |
case 0x1E: | |
opDesc = format("ADD I, V%X", x) | |
reg.I += reg.V[x] | |
break; | |
// LD F, Vx / Fx29 / Set I = location of sprite for digit Vx. | |
case 0x29: | |
opDesc = format("LD F, V%X", x) | |
reg.I = reg.V[x] * 5 | |
break; | |
// LD B, Vx / Fx33 / Store BCD representation of Vx in memory locations I, I+1, and I+2. | |
case 0x33: | |
opDesc = format("LD B, V%X", x) | |
local num = reg.V[x]; | |
for (local i = 3; i > 0; i--) { | |
mem[reg.I + i - 1] = num % 10 | |
num /= 10; | |
} | |
break; | |
// LD [I], Vx / Fx55 / Store registers V0 through Vx in memory starting at location I. | |
case 0x55: | |
opDesc = format("LD [I], V%X", x) | |
for (local i = 0; i <= x; i++) { | |
mem[reg.I + i] = reg.V[i] | |
} | |
break; | |
// LD Vx, [I] / Fx65 / Read registers V0 through Vx from memory starting at location I. | |
case 0x65: | |
opDesc = format("LD V%X, [I]", x) | |
for (local i = 0; i <= x; i++) { | |
reg.V[i] = mem[reg.I + i] | |
} | |
break; | |
default: | |
notRecognized(operand) | |
} | |
break; | |
default: | |
notRecognized(operand) | |
} | |
} | |
function dumpState() { | |
print(format("@ %03X: %02X %02X, ", reg.PC, operand >> 8, operand & 0xFF)) | |
foreach (idx, val in reg.V) { | |
print(format("V%01X: %02X, ", idx, val)) | |
} | |
print(format("I: %02X, ", reg.I)) | |
print(format("PC: %03X, ", reg.PC)) | |
print(format("SP: %d, ", reg.SP)) | |
print(format("DT: %02X, ", reg.DT)) | |
printl(format("ST: %02X", reg.ST)) | |
} | |
function notRecognized(operand) { | |
shouldRun = false; | |
printl("Unknown instruction @ " + toHex(reg.PC) + ": " + toHex(operand) + " > " + opDesc) | |
dumpState() | |
} | |
} | |
class DebugRender { | |
scope = null; | |
mach = null; | |
debug_disp = null; | |
debug_regs = { | |
V = [ | |
Entities.FindByName(null, "reg_V0"), | |
Entities.FindByName(null, "reg_V1"), | |
Entities.FindByName(null, "reg_V2"), | |
Entities.FindByName(null, "reg_V3"), | |
Entities.FindByName(null, "reg_V4"), | |
Entities.FindByName(null, "reg_V5"), | |
Entities.FindByName(null, "reg_V6"), | |
Entities.FindByName(null, "reg_V7"), | |
Entities.FindByName(null, "reg_V8"), | |
Entities.FindByName(null, "reg_V9"), | |
Entities.FindByName(null, "reg_VA"), | |
Entities.FindByName(null, "reg_VB"), | |
Entities.FindByName(null, "reg_VC"), | |
Entities.FindByName(null, "reg_VD"), | |
Entities.FindByName(null, "reg_VE"), | |
Entities.FindByName(null, "reg_VF") | |
], | |
I = Entities.FindByName(null, "reg_I"), | |
PC = Entities.FindByName(null, "reg_PC"), | |
SP = Entities.FindByName(null, "reg_SP"), | |
DT = Entities.FindByName(null, "reg_DT"), | |
ST = Entities.FindByName(null, "reg_ST"), | |
operand = Entities.FindByName(null, "reg_operand") | |
} | |
constructor(scopeForThis, machine) { | |
scope = scopeForThis | |
mach = machine | |
debug_disp = array(mach.disp.HEIGHT) | |
local e = Entities.FindByName(null, "disp_0"); | |
for (local i = 0; i < mach.disp.HEIGHT - 1; i++) { | |
e = Entities.Next(e) | |
debug_disp[i] = e | |
} | |
debug_draw() | |
} | |
function debug_draw() { | |
for (local y = 0; y < mach.disp.HEIGHT; y++) { | |
if (debug_disp[y] == null) | |
break; | |
local line = ""; | |
for (local x = 0; x < mach.disp.WIDTH; x++) { | |
line += mach.disp.buf[y * mach.disp.WIDTH + x] == 0 ? " " : "#"; | |
} | |
debug_disp[y].__KeyValueFromString("message", line) | |
} | |
} | |
function draw() { | |
debug_draw() | |
} | |
function postCycle() { | |
debug_regs.operand.__KeyValueFromString("message", "op: " + mach.cpu.opDesc) | |
} | |
function preCycle() { | |
foreach (idx, value in debug_regs.V) { | |
value.__KeyValueFromString("message", "V" + format("%01X", idx) + ": " + format("%02X", mach.cpu.reg.V[idx])) | |
} | |
debug_regs.DT.__KeyValueFromString("message", "DT: " + format("%02X", mach.cpu.reg.DT)) | |
debug_regs.ST.__KeyValueFromString("message", "ST: " + format("%02X", mach.cpu.reg.ST)) | |
debug_regs.I.__KeyValueFromString("message", "I: " + format("%02X", mach.cpu.reg.I)) | |
debug_regs.PC.__KeyValueFromString("message", "PC: " + format("%03X", mach.cpu.reg.PC)) | |
debug_regs.SP.__KeyValueFromString("message", "SP: " + mach.cpu.reg.SP) | |
} | |
} | |
class Display { | |
buf = null; | |
scope = null; | |
mach = null; | |
WIDTH = 64; | |
HEIGHT = 32; | |
hexChars = [ | |
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 | |
]; | |
drawFlag = false; | |
renderers = null; | |
constructor (scopeForThis, machine) { | |
scope = scopeForThis | |
mach = machine | |
buf = array(WIDTH * HEIGHT, 0) | |
for (local i = 0; i < hexChars.len(); i++) { | |
mach.mem[i] = hexChars[i] | |
} | |
renderers = [] | |
} | |
function addRenderer(r) { | |
renderers.push(r) | |
} | |
function preCycle() { | |
foreach (r in renderers) { | |
r.preCycle() | |
} | |
} | |
function postCycle() { | |
foreach (r in renderers) { | |
r.postCycle() | |
} | |
if (!drawFlag) | |
return; | |
foreach (r in renderers) { | |
r.draw() | |
} | |
drawFlag = false; | |
} | |
function setPixel(x, y, val) { | |
// Wrap around screen if the location exceeds the dimensions | |
if (x > WIDTH) | |
x -= WIDTH | |
else if (x < 0) | |
x += WIDTH | |
if (y > HEIGHT) | |
y -= HEIGHT | |
else if (y < 0) | |
y += HEIGHT | |
buf[y * WIDTH + x] = val | |
} | |
function togglePixel(x, y) { | |
local loc = y * WIDTH + x | |
setPixel(x, y, buf[loc] ^ 1) | |
return buf[loc] == 0 | |
} | |
function clearScreen() { | |
for (local i = 0; i < buf.len(); i++) { | |
buf[i] = 0; | |
} | |
} | |
function dump() { | |
for (local i = 0; i < buf.len(); i++) { | |
if (i % WIDTH == 0) | |
printl("") | |
print(buf[i] == 0 ? "_" : "#") | |
} | |
} | |
} | |
class Input { | |
key = null; | |
scope = null; | |
mach = null; | |
constructor(scopeForThis, machine) { | |
scope = scopeForThis | |
mach = machine | |
key = array(16, 0) | |
} | |
function set(idx, val) { | |
if (idx >= key.len()) { | |
printl("Invalid key: " + idx) | |
return | |
} | |
key[idx] = val; | |
} | |
function press(idx) { | |
key[idx] = 1; | |
} | |
function release(idx) { | |
key[idx] = 0; | |
} | |
} | |
class Machine { | |
mem = null; | |
disp = null; | |
input = null; | |
scope = null; | |
cpu = null; | |
constructor(scopeForThis, rom) { | |
scope = scopeForThis | |
printl("Power ON!") | |
mem = array(0x1000, 0) | |
cpu = scope.CPU(scope, this); | |
disp = scope.Display(scope, this) | |
input = scope.Input(scope, this); | |
disp.addRenderer(scope.DebugRender(scope, this)); | |
for (local i = 0; i < rom.len(); i++) { | |
mem[0x200 + i] = rom[i] | |
} | |
} | |
function cycle() { | |
disp.preCycle(); | |
cpu.cycle() | |
disp.postCycle(); | |
} | |
function playSound() { | |
EmitSound("buttons/blip1") | |
} | |
function start() { | |
cpu.shouldRun = true | |
} | |
function stop() { | |
cpu.shouldRun = false | |
} | |
} | |
mach <- null | |
function Step() { | |
mach.cycle() | |
} | |
function Run() { | |
EntFire("emu_clock", "enable", "") | |
mach.start() | |
} | |
function Stop() { | |
EntFire("emu_clock", "disable", "") | |
mach.stop() | |
} | |
function Reset() { | |
mach = Machine(this, program_data) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment