├── .editorconfig ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── src └── main.rs ├── wall.ch8 └── wall.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.md] 12 | # double whitespace at end of line 13 | # denotes a line break in Markdown 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.cargo -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "1.2.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 10 | 11 | [[package]] 12 | name = "cfg-if" 13 | version = "1.0.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 16 | 17 | [[package]] 18 | name = "chip8" 19 | version = "0.1.0" 20 | dependencies = [ 21 | "rand", 22 | "termion", 23 | ] 24 | 25 | [[package]] 26 | name = "getrandom" 27 | version = "0.2.3" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 30 | dependencies = [ 31 | "cfg-if", 32 | "libc", 33 | "wasi", 34 | ] 35 | 36 | [[package]] 37 | name = "libc" 38 | version = "0.2.97" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" 41 | 42 | [[package]] 43 | name = "numtoa" 44 | version = "0.1.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 47 | 48 | [[package]] 49 | name = "ppv-lite86" 50 | version = "0.2.10" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 53 | 54 | [[package]] 55 | name = "rand" 56 | version = "0.8.4" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 59 | dependencies = [ 60 | "libc", 61 | "rand_chacha", 62 | "rand_core", 63 | "rand_hc", 64 | ] 65 | 66 | [[package]] 67 | name = "rand_chacha" 68 | version = "0.3.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 71 | dependencies = [ 72 | "ppv-lite86", 73 | "rand_core", 74 | ] 75 | 76 | [[package]] 77 | name = "rand_core" 78 | version = "0.6.3" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 81 | dependencies = [ 82 | "getrandom", 83 | ] 84 | 85 | [[package]] 86 | name = "rand_hc" 87 | version = "0.3.1" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 90 | dependencies = [ 91 | "rand_core", 92 | ] 93 | 94 | [[package]] 95 | name = "redox_syscall" 96 | version = "0.2.9" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" 99 | dependencies = [ 100 | "bitflags", 101 | ] 102 | 103 | [[package]] 104 | name = "redox_termios" 105 | version = "0.1.2" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" 108 | dependencies = [ 109 | "redox_syscall", 110 | ] 111 | 112 | [[package]] 113 | name = "termion" 114 | version = "1.5.6" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" 117 | dependencies = [ 118 | "libc", 119 | "numtoa", 120 | "redox_syscall", 121 | "redox_termios", 122 | ] 123 | 124 | [[package]] 125 | name = "wasi" 126 | version = "0.10.2+wasi-snapshot-preview1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chip8" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | rand = "0.8.0" 8 | termion = "1.5.6" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Daniel Gatis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chip8 2 | 3 | An implementation of the CHIP-8 for Rust in ~350 lines of code. 4 | 5 | 6 | 7 | ## What is CHIP-8? 8 | 9 | CHIP-8 is a virtual machine (along with a supporting programming language). 10 | Since the CHIP-8 VM does not expose the fact that it's running on a host CPU, 11 | in theory the VM could be translated to physical hardware. 12 | 13 | The CHIP-8 VM was used in the late '70s on some computers such as the [Telmac 14 | 1800](https://en.wikipedia.org/wiki/Telmac_1800) and on some calculators in the 15 | 1980's. 16 | 17 | CHIP-8 was mainly used as a gaming platform, and today you can play lots of 18 | games like Pong and Breakout on it. 19 | 20 | ## Running a ROM 21 | 22 | Given a CHIP-8 ROM, you can start the ROM in the emulator like so: 23 | 24 | ```bash 25 | cargo run romfile.ch8 26 | ``` 27 | 28 | ## Where to get a ROM? 29 | [Here](https://github.com/dmatlack/chip8/tree/master/roms). 30 | 31 | ## Information on the emulator 32 | 33 | The input is mapped similarly to most other CHIP-8 emulators I have come across: 34 | 35 | Row 1|Row 2|Row 3|Row 4 36 | -----|-----|-----|----- 37 | 1 - 1|2 - 2|3 - 3|C - 4 38 | 4 - Q|5 - W|6 - E|D - R 39 | 7 - A|8 - S|9 - D|F - 4 40 | A - Z|0 - X|B - C|F - V 41 | 42 | The screen runs at the default resolution of 64x32. 43 | 44 | ## References 45 | 46 | * **Cowgod's Chip-8 Technical Reference:** http://devernay.free.fr/hacks/chip8/C8TECH10.HTM 47 | * **CHIP-8 Wikipedia Page:** https://en.wikipedia.org/wiki/CHIP-8 48 | * **Reddit r/EmuDev commmunity:** https://reddit.com/r/EmuDev 49 | 50 | 51 | ## License 52 | 53 | Copyright (c) 2021-present [Daniel Gatis](https://github.com/danielgatis) 54 | 55 | Licensed under [MIT License](./LICENSE) 56 | 57 | ## Buy me a coffee 58 | Liked some of my work? Buy me a coffee (or more likely a beer) 59 | 60 | Buy Me A Coffee 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use rand; 2 | use rand::Rng; 3 | use std::env; 4 | use std::fmt::Write as _; 5 | use std::fs::File; 6 | use std::io::{stdout, Read, Write}; 7 | use std::process; 8 | use std::{thread, time}; 9 | use termion::{async_stdin, raw::IntoRawMode}; 10 | 11 | fn main() { 12 | let args: Vec<_> = env::args().collect(); 13 | if args.len() != 2 { 14 | eprintln!("Usage: chip8 "); 15 | process::exit(1); 16 | } 17 | 18 | let mut f = File::open(&args[1]).unwrap(); 19 | let mut rom = [0u8; 3584]; 20 | f.read(&mut rom).unwrap(); 21 | 22 | let freq = time::Duration::from_secs_f32(1.0 / 480.0); 23 | 24 | let mut vram = [[0u8; 64]; 32]; 25 | let mut ram = [0u8; 4096]; 26 | let mut stack = [0u16; 16]; 27 | 28 | let mut rv = [0u8; 16]; 29 | let mut ri = 0u16; 30 | let mut pc = 512u16; 31 | let mut sp = 0u16; 32 | 33 | let mut delay_timer = 0u8; 34 | let mut sound_timer = 0u8; 35 | let mut frame_ready = false; 36 | 37 | let mut keypad_x = 0u8; 38 | let mut keypad_waiting = false; 39 | let mut keypad_delay = [time::Instant::now() 40 | .checked_sub(time::Duration::from_secs(5)) 41 | .unwrap(); 16]; 42 | let mut keypad = [false; 16]; 43 | 44 | let fonts: [u8; 80] = [ 45 | 0xF0, 0x90, 0x90, 0x90, 0xF0, 0x20, 0x60, 0x20, 0x20, 0x70, 0xF0, 0x10, 0xF0, 0x80, 0xF0, 46 | 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x90, 0x90, 0xF0, 0x10, 0x10, 0xF0, 0x80, 0xF0, 0x10, 0xF0, 47 | 0xF0, 0x80, 0xF0, 0x90, 0xF0, 0xF0, 0x10, 0x20, 0x40, 0x40, 0xF0, 0x90, 0xF0, 0x90, 0xF0, 48 | 0xF0, 0x90, 0xF0, 0x10, 0xF0, 0xF0, 0x90, 0xF0, 0x90, 0x90, 0xE0, 0x90, 0xE0, 0x90, 0xE0, 49 | 0xF0, 0x80, 0x80, 0x80, 0xF0, 0xE0, 0x90, 0x90, 0x90, 0xE0, 0xF0, 0x80, 0xF0, 0x80, 0xF0, 50 | 0xF0, 0x80, 0xF0, 0x80, 0x80, 51 | ]; 52 | 53 | ram[..fonts.len()].copy_from_slice(&fonts); 54 | ram[512..].copy_from_slice(&rom); 55 | 56 | let mut stdin = async_stdin().bytes(); 57 | let stdout = stdout(); 58 | let mut stdout = stdout.into_raw_mode().unwrap(); 59 | let mut c = 0; 60 | 61 | write!(stdout, "{}{}", termion::cursor::Hide, termion::clear::All).unwrap(); 62 | 63 | loop { 64 | let start = time::Instant::now(); 65 | 66 | let input = match stdin.next() { 67 | Some(Ok(b'1')) => Some(0x01u8), 68 | Some(Ok(b'2')) => Some(0x02u8), 69 | Some(Ok(b'3')) => Some(0x03u8), 70 | Some(Ok(b'4')) => Some(0x0cu8), 71 | Some(Ok(b'q')) => Some(0x04u8), 72 | Some(Ok(b'w')) => Some(0x05u8), 73 | Some(Ok(b'e')) => Some(0x06u8), 74 | Some(Ok(b'r')) => Some(0x0du8), 75 | Some(Ok(b'a')) => Some(0x07u8), 76 | Some(Ok(b's')) => Some(0x08u8), 77 | Some(Ok(b'd')) => Some(0x09u8), 78 | Some(Ok(b'f')) => Some(0x0eu8), 79 | Some(Ok(b'z')) => Some(0x0au8), 80 | Some(Ok(b'x')) => Some(0x00u8), 81 | Some(Ok(b'c')) => Some(0x0bu8), 82 | Some(Ok(b'v')) => Some(0x0fu8), 83 | Some(Ok(3)) => break, 84 | _ => None, 85 | }; 86 | 87 | if let Some(i) = input { 88 | keypad_delay[i as usize] = time::Instant::now(); 89 | } 90 | 91 | for i in 0..keypad.len() { 92 | keypad[i] = keypad_delay[i].elapsed().as_millis() <= 100; 93 | } 94 | 95 | if keypad_waiting { 96 | for i in 0..keypad.len() { 97 | if keypad[i] { 98 | keypad_waiting = false; 99 | rv[keypad_x as usize] = i as u8; 100 | break; 101 | } 102 | } 103 | 104 | continue; 105 | } 106 | 107 | let mut rng = rand::thread_rng(); 108 | 109 | let opcode = (ram[pc as usize] as u16) << 8 | (ram[(pc + 1) as usize] as u16); 110 | let o = ((opcode & 0xF000) >> 12) as u8; 111 | let x = ((opcode & 0x0F00) >> 8) as u8; 112 | let y = ((opcode & 0x00F0) >> 4) as u8; 113 | let n = (opcode & 0x000F) as u8; 114 | let nnn = (opcode & 0x0FFF) as u16; 115 | let nn = (opcode & 0x00FF) as u8; 116 | 117 | match (o, x, y, n) { 118 | (0x00, 0x00, 0x0e, 0x00) => { 119 | vram = [[0u8; 64]; 32]; 120 | frame_ready = true; 121 | pc += 2; 122 | } 123 | (0x00, 0x00, 0x0e, 0x0e) => { 124 | sp -= 1; 125 | pc = stack[sp as usize]; 126 | } 127 | (0x00, _, _, _) => pc += 2, 128 | (0x01, _, _, _) => pc = nnn, 129 | (0x02, _, _, _) => { 130 | stack[sp as usize] = pc + 2; 131 | sp += 1; 132 | pc = nnn; 133 | } 134 | (0x03, _, _, _) => { 135 | if rv[x as usize] == nn { 136 | pc += 4; 137 | } else { 138 | pc += 2; 139 | } 140 | } 141 | (0x04, _, _, _) => { 142 | if rv[x as usize] != nn { 143 | pc += 4; 144 | } else { 145 | pc += 2; 146 | } 147 | } 148 | (0x05, _, _, 0x00) => { 149 | if rv[x as usize] == rv[y as usize] { 150 | pc += 4; 151 | } else { 152 | pc += 2; 153 | } 154 | } 155 | (0x06, _, _, _) => { 156 | rv[x as usize] = nn; 157 | pc += 2; 158 | } 159 | (0x07, _, _, _) => { 160 | rv[x as usize] = (rv[x as usize] as u16 + nn as u16) as u8; 161 | pc += 2; 162 | } 163 | (0x08, _, _, 0x00) => { 164 | rv[x as usize] = rv[y as usize]; 165 | pc += 2; 166 | } 167 | (0x08, _, _, 0x01) => { 168 | rv[x as usize] |= rv[y as usize]; 169 | pc += 2; 170 | } 171 | (0x08, _, _, 0x02) => { 172 | rv[x as usize] &= rv[y as usize]; 173 | pc += 2; 174 | } 175 | (0x08, _, _, 0x03) => { 176 | rv[x as usize] ^= rv[y as usize]; 177 | pc += 2; 178 | } 179 | (0x08, _, _, 0x04) => { 180 | let result = rv[x as usize] as u16 + rv[y as usize] as u16; 181 | rv[0x0f] = if result > 0xff { 1 } else { 0 }; 182 | rv[x as usize] = result as u8; 183 | pc += 2; 184 | } 185 | (0x08, _, _, 0x05) => { 186 | rv[0x0f] = if rv[x as usize] > rv[y as usize] { 187 | 1 188 | } else { 189 | 0 190 | }; 191 | rv[x as usize] = rv[x as usize].wrapping_sub(rv[y as usize]); 192 | pc += 2; 193 | } 194 | (0x08, _, _, 0x06) => { 195 | rv[0x0f] = rv[x as usize] & 1; 196 | rv[x as usize] >>= 1; 197 | pc += 2; 198 | } 199 | (0x08, _, _, 0x07) => { 200 | rv[0x0f] = if rv[y as usize] > rv[x as usize] { 201 | 1 202 | } else { 203 | 0 204 | }; 205 | rv[x as usize] = rv[y as usize].wrapping_sub(rv[x as usize]); 206 | pc += 2; 207 | } 208 | (0x08, _, _, 0x0e) => { 209 | rv[0x0f] = (rv[x as usize] & 0x80) >> 7; 210 | rv[x as usize] <<= 1; 211 | pc += 2; 212 | } 213 | (0x09, _, _, 0x00) => { 214 | if rv[x as usize] != rv[y as usize] { 215 | pc += 4; 216 | } else { 217 | pc += 2; 218 | } 219 | } 220 | (0x0a, _, _, _) => { 221 | ri = nnn; 222 | pc += 2; 223 | } 224 | (0x0b, _, _, _) => { 225 | pc = (rv[0] as u16) + nnn; 226 | } 227 | (0x0c, _, _, _) => { 228 | rv[x as usize] = rng.gen::() & nn; 229 | pc += 2; 230 | } 231 | (0x0d, _, _, _) => { 232 | rv[0x0f] = 0; 233 | for byte in 0..n { 234 | let y = (rv[y as usize] + byte) % 32; 235 | for bit in 0..8 { 236 | let x = (rv[x as usize] + bit) % 64; 237 | let color = ram[(ri + byte as u16) as usize] >> (7 - bit) & 1; 238 | vram[y as usize][x as usize] ^= color; 239 | rv[0x0f] |= color & vram[y as usize][x as usize]; 240 | } 241 | } 242 | frame_ready = true; 243 | pc += 2; 244 | } 245 | (0x0e, _, 0x09, 0x0e) => { 246 | if keypad[rv[x as usize] as usize] { 247 | pc += 4; 248 | } else { 249 | pc += 2; 250 | } 251 | } 252 | (0x0e, _, 0x0a, 0x01) => { 253 | if !keypad[rv[x as usize] as usize] { 254 | pc += 4; 255 | } else { 256 | pc += 2; 257 | } 258 | } 259 | (0x0f, _, 0x00, 0x07) => { 260 | rv[x as usize] = delay_timer; 261 | pc += 2; 262 | } 263 | (0x0f, _, 0x00, 0x0a) => { 264 | keypad_waiting = true; 265 | keypad_x = x; 266 | pc += 2; 267 | } 268 | (0x0f, _, 0x01, 0x05) => { 269 | delay_timer = rv[x as usize]; 270 | pc += 2; 271 | } 272 | (0x0f, _, 0x01, 0x08) => { 273 | sound_timer = rv[x as usize]; 274 | pc += 2; 275 | } 276 | (0x0f, _, 0x01, 0x0e) => { 277 | ri += rv[x as usize] as u16; 278 | rv[0x0f] = if ri > 0x0f00 { 1 } else { 0 }; 279 | pc += 2; 280 | } 281 | (0x0f, _, 0x02, 0x09) => { 282 | ri = rv[x as usize] as u16 * 5; 283 | pc += 2; 284 | } 285 | (0x0f, _, 0x03, 0x03) => { 286 | ram[(ri + 0) as usize] = rv[x as usize] / 100; 287 | ram[(ri + 1) as usize] = rv[x as usize] % 100 / 10; 288 | ram[(ri + 2) as usize] = rv[x as usize] % 10; 289 | pc += 2; 290 | } 291 | (0x0f, _, 0x05, 0x05) => { 292 | for i in 0..x + 1 { 293 | ram[(ri + i as u16) as usize] = rv[i as usize]; 294 | } 295 | pc += 2; 296 | } 297 | (0x0f, _, 0x06, 0x05) => { 298 | for i in 0..x + 1 { 299 | rv[i as usize] = ram[(ri + i as u16) as usize]; 300 | } 301 | pc += 2; 302 | } 303 | _ => { 304 | panic!("invalid opcode: {:#04x}", opcode); 305 | } 306 | } 307 | 308 | if delay_timer > 0 { 309 | delay_timer -= 1; 310 | } 311 | 312 | if sound_timer > 0 { 313 | sound_timer -= 1; 314 | } 315 | 316 | if c % 8 == 0 { 317 | if sound_timer > 0 { 318 | write!(stdout, "\x07").unwrap(); 319 | } 320 | 321 | if frame_ready { 322 | let mut buffer = String::new(); 323 | 324 | for y in 0..vram.len() { 325 | write!(buffer, "{}", termion::cursor::Goto(1, (y + 1) as u16)).unwrap(); 326 | 327 | for x in 0..vram[y].len() { 328 | let fg = if vram[y as usize][x as usize] == 0 { 329 | 0 330 | } else { 331 | 5 332 | }; 333 | write!( 334 | buffer, 335 | "{}{}█", 336 | termion::color::Fg(termion::color::AnsiValue::rgb(fg, fg, fg)), 337 | termion::color::Bg(termion::color::Black) 338 | ) 339 | .unwrap(); 340 | } 341 | } 342 | 343 | write!(stdout, "{}{}", buffer, termion::cursor::Goto(1, 33)).unwrap(); 344 | frame_ready = false; 345 | } 346 | } 347 | 348 | c += 1; 349 | 350 | let runtime = start.elapsed(); 351 | if let Some(remaining) = freq.checked_sub(runtime) { 352 | thread::sleep(remaining); 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /wall.ch8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgatis/chip8/d40af9ffad3a31566aed04d832f2a8c6e8ba0fd4/wall.ch8 -------------------------------------------------------------------------------- /wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgatis/chip8/d40af9ffad3a31566aed04d832f2a8c6e8ba0fd4/wall.png --------------------------------------------------------------------------------