├── .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 |
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
--------------------------------------------------------------------------------