├── index.html ├── roms ├── BLINKY └── BLITZ ├── scripts ├── chip8.js ├── cpu.js ├── keyboard.js ├── renderer.js └── speaker.js ├── server.py └── style.css /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /roms/BLINKY: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericgrandt/chip8-emulator/7d3b6b956080eb2ff3ca25ec21f4a80730f57431/roms/BLINKY -------------------------------------------------------------------------------- /roms/BLITZ: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericgrandt/chip8-emulator/7d3b6b956080eb2ff3ca25ec21f4a80730f57431/roms/BLITZ -------------------------------------------------------------------------------- /scripts/chip8.js: -------------------------------------------------------------------------------- 1 | import Renderer from './renderer.js'; 2 | import Keyboard from './keyboard.js'; 3 | import Speaker from './speaker.js'; 4 | import CPU from './cpu.js'; 5 | 6 | const renderer = new Renderer(10); 7 | const keyboard = new Keyboard(); 8 | const speaker = new Speaker(); 9 | const cpu = new CPU(renderer, keyboard, speaker); 10 | 11 | let loop; 12 | 13 | let fps = 60, fpsInterval, startTime, now, then, elapsed; 14 | 15 | function init() { 16 | fpsInterval = 1000 / fps; 17 | then = Date.now(); 18 | startTime = then; 19 | 20 | cpu.loadSpritesIntoMemory(); 21 | cpu.loadRom('BLINKY'); 22 | loop = requestAnimationFrame(step); 23 | } 24 | 25 | function step() { 26 | now = Date.now(); 27 | elapsed = now - then; 28 | 29 | if (elapsed > fpsInterval) { 30 | cpu.cycle(); 31 | } 32 | 33 | loop = requestAnimationFrame(step); 34 | } 35 | 36 | init(); -------------------------------------------------------------------------------- /scripts/cpu.js: -------------------------------------------------------------------------------- 1 | class CPU { 2 | constructor(renderer, keyboard, speaker) { 3 | this.renderer = renderer; 4 | this.keyboard = keyboard; 5 | this.speaker = speaker; 6 | 7 | // 4KB (4096 bytes) of memory 8 | this.memory = new Uint8Array(4096); 9 | 10 | // 16 8-bit registers 11 | this.v = new Uint8Array(16); 12 | 13 | // Stores memory addresses. Set this to 0 since we aren't storing anything at initialization. 14 | this.i = 0; 15 | 16 | // Timers 17 | this.delayTimer = 0; 18 | this.soundTimer = 0; 19 | 20 | // Program counter. Stores the currently executing address. 21 | this.pc = 0x200; 22 | 23 | // Don't initialize this with a size in order to avoid empty results. 24 | this.stack = new Array(); 25 | 26 | // Some instructions require pausing, such as Fx0A. 27 | this.paused = false; 28 | 29 | this.speed = 10; 30 | } 31 | 32 | loadSpritesIntoMemory() { 33 | // Array of hex values for each sprite. Each sprite is 5 bytes. 34 | // The technical reference provides us with each one of these values. 35 | const sprites = [ 36 | 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 37 | 0x20, 0x60, 0x20, 0x20, 0x70, // 1 38 | 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 39 | 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 40 | 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 41 | 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 42 | 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 43 | 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 44 | 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 45 | 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 46 | 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 47 | 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 48 | 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 49 | 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 50 | 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 51 | 0xF0, 0x80, 0xF0, 0x80, 0x80 // F 52 | ]; 53 | 54 | // According to the technical reference, sprites are stored in the interpreter section of memory starting at hex 0x000 55 | for (let i = 0; i < sprites.length; i++) { 56 | this.memory[i] = sprites[i]; 57 | } 58 | } 59 | 60 | loadProgramIntoMemory(program) { 61 | for (let loc = 0; loc < program.length; loc++) { 62 | this.memory[0x200 + loc] = program[loc]; 63 | } 64 | } 65 | 66 | loadRom(romName) { 67 | var request = new XMLHttpRequest; 68 | var self = this; 69 | 70 | // Handles the response received from sending (request.send()) our request 71 | request.onload = function() { 72 | // If the request response has content 73 | if (request.response) { 74 | // Store the contents of the response in an 8-bit array 75 | let program = new Uint8Array(request.response); 76 | 77 | // Load the ROM/program into memory 78 | self.loadProgramIntoMemory(program); 79 | } 80 | } 81 | 82 | // Initialize a GET request to retrieve the ROM from our roms folder 83 | request.open('GET', 'roms/' + romName); 84 | request.responseType = 'arraybuffer'; 85 | 86 | // Send the GET request 87 | request.send(); 88 | } 89 | 90 | cycle() { 91 | for (let i = 0; i < this.speed; i++) { 92 | if (!this.paused) { 93 | let opcode = (this.memory[this.pc] << 8 | this.memory[this.pc + 1]); 94 | this.executeInstruction(opcode); 95 | } 96 | } 97 | 98 | if (!this.paused) { 99 | this.updateTimers(); 100 | } 101 | 102 | this.playSound(); 103 | this.renderer.render(); 104 | } 105 | 106 | updateTimers() { 107 | if (this.delayTimer > 0) { 108 | this.delayTimer -= 1; 109 | } 110 | 111 | if (this.soundTimer > 0) { 112 | this.soundTimer -= 1; 113 | } 114 | } 115 | 116 | playSound() { 117 | if (this.soundTimer > 0) { 118 | this.speaker.play(440); 119 | } else { 120 | this.speaker.stop(); 121 | } 122 | } 123 | 124 | executeInstruction(opcode) { 125 | // Increment the program counter to prepare it for the next instruction. 126 | // Each instruction is 2 bytes long, so increment it by 2. 127 | this.pc += 2; 128 | 129 | // We only need the 2nd nibble, so grab the value of the 2nd nibble and shift it right 8 bits to get rid of everything but that 2nd nibble. 130 | let x = (opcode & 0x0F00) >> 8; 131 | 132 | // We only need the 3rd nibble, so grab the value of the 3rd nibble and shift it right 4 bits to get rid of everything but that 3rd nibble. 133 | let y = (opcode & 0x00F0) >> 4; 134 | 135 | switch (opcode & 0xF000) { 136 | case 0x0000: 137 | switch (opcode) { 138 | case 0x00E0: 139 | this.renderer.clear(); 140 | break; 141 | case 0x00EE: 142 | this.pc = this.stack.pop(); 143 | break; 144 | } 145 | 146 | break; 147 | case 0x1000: 148 | this.pc = (opcode & 0xFFF); 149 | break; 150 | case 0x2000: 151 | this.stack.push(this.pc); 152 | this.pc = (opcode & 0xFFF); 153 | break; 154 | case 0x3000: 155 | if (this.v[x] === (opcode & 0xFF)) { 156 | this.pc += 2; 157 | } 158 | break; 159 | case 0x4000: 160 | if (this.v[x] !== (opcode & 0xFF)) { 161 | this.pc += 2; 162 | } 163 | break; 164 | case 0x5000: 165 | if (this.v[x] === this.v[y]) { 166 | this.pc += 2; 167 | } 168 | break; 169 | case 0x6000: 170 | this.v[x] = (opcode & 0xFF); 171 | break; 172 | case 0x7000: 173 | this.v[x] += (opcode & 0xFF); 174 | break; 175 | case 0x8000: 176 | switch (opcode & 0xF) { 177 | case 0x0: 178 | this.v[x] = this.v[y]; 179 | break; 180 | case 0x1: 181 | this.v[x] |= this.v[y] 182 | break; 183 | case 0x2: 184 | this.v[x] &= this.v[y]; 185 | break; 186 | case 0x3: 187 | this.v[x] ^= this.v[y]; 188 | break; 189 | case 0x4: 190 | let sum = (this.v[x] += this.v[y]); 191 | 192 | this.v[0xF] = 0; 193 | 194 | if (sum > 0xFF) { 195 | this.v[0xF] = 1; 196 | } 197 | 198 | this.v[x] = sum; 199 | break; 200 | case 0x5: 201 | this.v[0xF] = 0; 202 | 203 | if (this.v[x] > this.v[y]) { 204 | this.v[0xF] = 1; 205 | } 206 | 207 | this.v[x] -= this.v[y]; 208 | break; 209 | case 0x6: 210 | this.v[0xF] = (this.v[x] & 0x1); 211 | 212 | this.v[x] >>= 1; 213 | break; 214 | case 0x7: 215 | this.v[0xF] = 0; 216 | 217 | if (this.v[y] > this.v[x]) { 218 | this.v[0xF] = 1; 219 | } 220 | 221 | this.v[x] = this.v[y] - this.v[x]; 222 | break; 223 | case 0xE: 224 | this.v[0xF] = (this.v[x] & 0x80); 225 | this.v[x] <<= 1; 226 | break; 227 | } 228 | 229 | break; 230 | case 0x9000: 231 | if (this.v[x] !== this.v[y]) { 232 | this.pc += 2; 233 | } 234 | break; 235 | case 0xA000: 236 | this.i = (opcode & 0xFFF); 237 | break; 238 | case 0xB000: 239 | this.pc = (opcode & 0xFFF) + this.v[0]; 240 | break; 241 | case 0xC000: 242 | let rand = Math.floor(Math.random() * 0xFF); 243 | 244 | this.v[x] = rand & (opcode & 0xFF); 245 | break; 246 | case 0xD000: 247 | let width = 8; 248 | let height = (opcode & 0xF); 249 | 250 | this.v[0xF] = 0; 251 | 252 | for (let row = 0; row < height; row++) { 253 | let sprite = this.memory[this.i + row]; 254 | 255 | for (let col = 0; col < width; col++) { 256 | // If the bit (sprite) is not 0, render/erase the pixel 257 | if ((sprite & 0x80) > 0) { 258 | // If setPixel returns 1, which means a pixel was erased, set VF to 1 259 | if (this.renderer.setPixel(this.v[x] + col, this.v[y] + row)) { 260 | this.v[0xF] = 1; 261 | } 262 | } 263 | 264 | // Shift the sprite left 1. This will move the next next col/bit of the sprite into the first position. 265 | // Ex. 10010000 << 1 will become 0010000 266 | sprite <<= 1; 267 | } 268 | } 269 | break; 270 | case 0xE000: 271 | switch (opcode & 0xFF) { 272 | case 0x9E: 273 | if (this.keyboard.isKeyPressed(this.v[x])) { 274 | this.pc += 2; 275 | } 276 | break; 277 | case 0xA1: 278 | if (!this.keyboard.isKeyPressed(this.v[x])) { 279 | this.pc += 2; 280 | } 281 | break; 282 | } 283 | 284 | break; 285 | case 0xF000: 286 | switch (opcode & 0xFF) { 287 | case 0x07: 288 | this.v[x] = this.delayTimer; 289 | break; 290 | case 0x0A: 291 | this.paused = true; 292 | 293 | this.keyboard.onNextKeyPress = function(key) { 294 | this.v[x] = key; 295 | this.paused = false; 296 | }.bind(this); 297 | break; 298 | case 0x15: 299 | this.delayTimer = this.v[x]; 300 | break; 301 | case 0x18: 302 | this.soundTimer = this.v[x]; 303 | break; 304 | case 0x1E: 305 | this.i += this.v[x]; 306 | break; 307 | case 0x29: 308 | this.i = this.v[x] * 5; 309 | break; 310 | case 0x33: 311 | // Get the hundreds digit and place it in I. 312 | this.memory[this.i] = parseInt(this.v[x] / 100); 313 | 314 | // Get tens digit and place it in I+1. Gets a value between 0 and 99, then divides by 10 to give us a value 315 | // between 0 and 9. 316 | this.memory[this.i + 1] = parseInt((this.v[x] % 100) / 10); 317 | 318 | // Get the value of the ones (last) digit and place it in I+2. 0 through 9. 319 | this.memory[this.i + 2] = parseInt(this.v[x] % 10); 320 | break; 321 | case 0x55: 322 | for (let registerIndex = 0; registerIndex <= x; registerIndex++) { 323 | this.memory[this.i + registerIndex] = this.v[registerIndex]; 324 | } 325 | break; 326 | case 0x65: 327 | for (let registerIndex = 0; registerIndex <= x; registerIndex++) { 328 | this.v[registerIndex] = this.memory[this.i + registerIndex]; 329 | } 330 | break; 331 | } 332 | 333 | break; 334 | 335 | default: 336 | throw new Error('Unknown opcode ' + opcode); 337 | } 338 | } 339 | } 340 | 341 | export default CPU; -------------------------------------------------------------------------------- /scripts/keyboard.js: -------------------------------------------------------------------------------- 1 | class Keyboard { 2 | constructor() { 3 | this.KEYMAP = { 4 | 49: 0x1, // 1 5 | 50: 0x2, // 2 6 | 51: 0x3, // 3 7 | 52: 0xc, // 4 8 | 81: 0x4, // Q 9 | 87: 0x5, // W 10 | 69: 0x6, // E 11 | 82: 0xD, // R 12 | 65: 0x7, // A 13 | 83: 0x8, // S 14 | 68: 0x9, // D 15 | 70: 0xE, // F 16 | 90: 0xA, // Z 17 | 88: 0x0, // X 18 | 67: 0xB, // C 19 | 86: 0xF // V 20 | } 21 | 22 | this.keysPressed = []; 23 | 24 | // Some Chip-8 instructions require waiting for the next key press. We initialize this function elsewhere when needed. 25 | this.onNextKeyPress = null; 26 | 27 | window.addEventListener('keydown', this.onKeyDown.bind(this), false); 28 | window.addEventListener('keyup', this.onKeyUp.bind(this), false); 29 | } 30 | 31 | isKeyPressed(keyCode) { 32 | return this.keysPressed[keyCode]; 33 | } 34 | 35 | onKeyDown(event) { 36 | let key = this.KEYMAP[event.which]; 37 | this.keysPressed[key] = true; 38 | 39 | // Make sure onNextKeyPress is initialized and the pressed key is actually mapped to a Chip-8 key 40 | if (this.onNextKeyPress !== null && key !== undefined) { 41 | this.onNextKeyPress(parseInt(key)); 42 | this.onNextKeyPress = null; 43 | } 44 | } 45 | 46 | onKeyUp(event) { 47 | let key = this.KEYMAP[event.which]; 48 | this.keysPressed[key] = false; 49 | } 50 | } 51 | 52 | export default Keyboard; -------------------------------------------------------------------------------- /scripts/renderer.js: -------------------------------------------------------------------------------- 1 | class Renderer { 2 | constructor(scale) { 3 | this.cols = 64; 4 | this.rows = 32; 5 | 6 | this.scale = scale; 7 | 8 | this.canvas = document.querySelector('canvas'); 9 | this.ctx = this.canvas.getContext('2d'); 10 | 11 | this.canvas.width = this.cols * this.scale; 12 | this.canvas.height = this.rows * this.scale; 13 | 14 | this.display = new Array(this.cols * this.rows); 15 | } 16 | 17 | setPixel(x, y) { 18 | if (x > this.cols) { 19 | x -= this.cols; 20 | } else if (x < 0) { 21 | x += this.cols; 22 | } 23 | 24 | if (y > this.rows) { 25 | y -= this.rows; 26 | } else if (y < 0) { 27 | y += this.rows; 28 | } 29 | 30 | let pixelLoc = x + (y * this.cols); 31 | 32 | this.display[pixelLoc] ^= 1; 33 | 34 | return !this.display[pixelLoc]; 35 | } 36 | 37 | clear() { 38 | this.display = new Array(this.cols * this.rows); 39 | } 40 | 41 | render() { 42 | // Clears the display every render cycle. Typical for a render loop. 43 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 44 | 45 | // Loop through our display array 46 | for (let i = 0; i < this.cols * this.rows; i++) { 47 | // Grabs the x position of the pixel based off of `i` 48 | let x = (i % this.cols) * this.scale; 49 | 50 | // Grabs the y position of the pixel based off of `i` 51 | let y = Math.floor(i / this.cols) * this.scale; 52 | 53 | // If the value at this.display[i] == 1, then draw a pixel. 54 | if (this.display[i]) { 55 | // Set the pixel color to black 56 | this.ctx.fillStyle = '#000'; 57 | 58 | // Place a pixel at position (x, y) with a width and height of scale 59 | this.ctx.fillRect(x, y, this.scale, this.scale); 60 | } 61 | } 62 | } 63 | } 64 | 65 | export default Renderer; -------------------------------------------------------------------------------- /scripts/speaker.js: -------------------------------------------------------------------------------- 1 | class Speaker { 2 | constructor() { 3 | const AudioContext = window.AudioContext || window.webkitAudioContext; 4 | 5 | this.audioCtx = new AudioContext(); 6 | 7 | // Create a gain, which will allow us to control the volume 8 | this.gain = this.audioCtx.createGain(); 9 | this.finish = this.audioCtx.destination; 10 | 11 | // Connect the gain to the audio context 12 | this.gain.connect(this.finish); 13 | } 14 | 15 | play(frequency) { 16 | if (this.audioCtx && !this.oscillator) { 17 | this.oscillator = this.audioCtx.createOscillator(); 18 | 19 | // Set the frequency 20 | this.oscillator.frequency.setValueAtTime(frequency || 440, this.audioCtx.currentTime); 21 | 22 | // Square wave 23 | this.oscillator.type = 'square'; 24 | 25 | // Connect the gain and start the sound 26 | this.oscillator.connect(this.gain); 27 | this.oscillator.start(); 28 | } 29 | } 30 | 31 | stop() { 32 | if (this.oscillator) { 33 | this.oscillator.stop(); 34 | this.oscillator.disconnect(); 35 | this.oscillator = null; 36 | } 37 | } 38 | } 39 | 40 | export default Speaker; -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | from http.server import HTTPServer, BaseHTTPRequestHandler 3 | import socketserver 4 | 5 | PORT = 8080 6 | 7 | Handler = http.server.SimpleHTTPRequestHandler 8 | 9 | Handler.extensions_map = { 10 | '.manifest': 'text/cache-manifest', 11 | '.html': 'text/html', 12 | '.png': 'image/png', 13 | '.jpg': 'image/jpg', 14 | '.svg': 'image/svg+xml', 15 | '.css': 'text/css', 16 | '.js': 'application/x-javascript', 17 | '': 'application/octet-stream', # Default 18 | } 19 | 20 | httpd = socketserver.TCPServer(("", PORT), Handler) 21 | 22 | print("serving at port", PORT) 23 | httpd.serve_forever() -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | border: 2px solid black; 3 | } --------------------------------------------------------------------------------