├── roms ├── BLITZ ├── BRIX ├── GUESS ├── MAZE ├── PONG ├── PONG2 ├── TANK ├── UFO ├── VBRIX ├── VERS ├── BLINKY ├── HIDDEN ├── KALEID ├── MERLIN ├── MISSILE ├── PUZZLE ├── SYZYGY ├── TETRIS ├── TICTAC ├── WIPEOFF ├── 15PUZZLE ├── CONNECT4 └── INVADERS ├── src ├── img │ ├── PONG2.png │ ├── PONG2_raw.png │ ├── init_tree.png │ ├── font_diagram.png │ ├── input_layout.png │ └── font_diagram.svg ├── metadata.yaml ├── 9-changes.md ├── 1-intro.md ├── 8-opcodes.md ├── 2-basics.md ├── 4-methods.md ├── 3-setup.md ├── 7-wasm.md ├── 6-frontend.md └── 5-instr.md ├── book.toml ├── .gitignore ├── code ├── chip8_core │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── desktop │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── Cargo.lock ├── wasm │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── web │ ├── index.html │ └── index.js ├── Makefile ├── README.md └── LICENSE /roms/BLITZ: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/BLITZ -------------------------------------------------------------------------------- /roms/BRIX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/BRIX -------------------------------------------------------------------------------- /roms/GUESS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/GUESS -------------------------------------------------------------------------------- /roms/MAZE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/MAZE -------------------------------------------------------------------------------- /roms/PONG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/PONG -------------------------------------------------------------------------------- /roms/PONG2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/PONG2 -------------------------------------------------------------------------------- /roms/TANK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/TANK -------------------------------------------------------------------------------- /roms/UFO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/UFO -------------------------------------------------------------------------------- /roms/VBRIX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/VBRIX -------------------------------------------------------------------------------- /roms/VERS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/VERS -------------------------------------------------------------------------------- /roms/BLINKY: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/BLINKY -------------------------------------------------------------------------------- /roms/HIDDEN: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/HIDDEN -------------------------------------------------------------------------------- /roms/KALEID: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/KALEID -------------------------------------------------------------------------------- /roms/MERLIN: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/MERLIN -------------------------------------------------------------------------------- /roms/MISSILE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/MISSILE -------------------------------------------------------------------------------- /roms/PUZZLE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/PUZZLE -------------------------------------------------------------------------------- /roms/SYZYGY: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/SYZYGY -------------------------------------------------------------------------------- /roms/TETRIS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/TETRIS -------------------------------------------------------------------------------- /roms/TICTAC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/TICTAC -------------------------------------------------------------------------------- /roms/WIPEOFF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/WIPEOFF -------------------------------------------------------------------------------- /roms/15PUZZLE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/15PUZZLE -------------------------------------------------------------------------------- /roms/CONNECT4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/CONNECT4 -------------------------------------------------------------------------------- /roms/INVADERS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/roms/INVADERS -------------------------------------------------------------------------------- /src/img/PONG2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/src/img/PONG2.png -------------------------------------------------------------------------------- /src/img/PONG2_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/src/img/PONG2_raw.png -------------------------------------------------------------------------------- /src/img/init_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/src/img/init_tree.png -------------------------------------------------------------------------------- /src/img/font_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/src/img/font_diagram.png -------------------------------------------------------------------------------- /src/img/input_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquova/chip8-book/HEAD/src/img/input_layout.png -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["aquova"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Chip-8 Emulation in Rust" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust files 2 | **/target/* 3 | **/pkg/* 4 | *.lock 5 | *.epub 6 | 7 | # Build products 8 | **/wasm.js 9 | *.wasm 10 | 11 | # Editor files 12 | *.code-workspace 13 | 14 | # Book files 15 | *.pdf 16 | -------------------------------------------------------------------------------- /code/chip8_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chip8_core" 3 | version = "0.1.0" 4 | authors = ["aquova "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | rand = { version = "^0.7.3", features = ["wasm-bindgen"] } 9 | -------------------------------------------------------------------------------- /code/desktop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "desktop" 3 | version = "0.1.0" 4 | authors = ["aquova "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | chip8_core = { path = "../chip8_core" } 9 | sdl2 = "^0.34.3" 10 | -------------------------------------------------------------------------------- /src/metadata.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | - \vspace{6cm} 4 | - An Introduction to Chip-8 Emulation using the Rust Programming Language 5 | author: by aquova 6 | date: 7 | - 15 February 2023 8 | - \newpage 9 | keywords: [emulation, chip-8, emudev, rust, webassembly] 10 | geometry: margin=0.75in 11 | numbersections: true 12 | toc: true 13 | toc-title: Table of Contents 14 | toccolor: black 15 | links-as-notes: false 16 | header-includes: 17 | - \usepackage{hyperref} 18 | - \hypersetup{colorlinks=true, 19 | allcolors=blue} 20 | --- 21 | -------------------------------------------------------------------------------- /code/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm" 3 | version = "0.1.0" 4 | authors = ["aquova "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | chip8_core = { path = "../chip8_core" } 9 | js-sys = "^0.3.46" 10 | wasm-bindgen = "^0.2.69" 11 | 12 | [dependencies.web-sys] 13 | version = "^0.3.46" 14 | features = [ 15 | "CanvasRenderingContext2d", 16 | "Document", 17 | "Element", 18 | "HtmlCanvasElement", 19 | "ImageData", 20 | "KeyboardEvent", 21 | "Window" 22 | ] 23 | 24 | [lib] 25 | crate-type = ["cdylib"] 26 | -------------------------------------------------------------------------------- /src/9-changes.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.0 4 | - Initial release 5 | - Includes source code for Chip-8 emulator as well as PDF document discussing its development 6 | 7 | ## Version 1.01 8 | - [Fix typo: Stack is a LIFO system, not a FIFO](https://github.com/aquova/chip8-book/issues/6) 9 | - [Added missing bug fixes from source code into the book](https://github.com/aquova/chip8-book/pull/5) 10 | - [Added ePub generation](https://github.com/aquova/chip8-book/pull/4) 11 | 12 | ## Version 1.1 13 | - Updated much of the prose for better flow (special thanks to KDR for editing). 14 | -------------------------------------------------------------------------------- /code/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chip-8 Emulator 5 | 6 | 12 | 13 | 14 |

My Chip-8 Emulator

15 | 16 | 17 |
18 | If you see this message, then your browser doesn't support HTML5 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pdf: 2 | cd src && \ 3 | pandoc -s -o chip8.pdf \ 4 | 1-intro.md \ 5 | 2-basics.md \ 6 | 3-setup.md \ 7 | 4-methods.md \ 8 | 5-instr.md \ 9 | 6-frontend.md \ 10 | 7-wasm.md \ 11 | 8-opcodes.md \ 12 | 9-changes.md \ 13 | metadata.yaml 14 | 15 | epub: 16 | cd src && \ 17 | pandoc -s -o chip8.epub \ 18 | 1-intro.md \ 19 | 2-basics.md \ 20 | 3-setup.md \ 21 | 4-methods.md \ 22 | 5-instr.md \ 23 | 6-frontend.md \ 24 | 7-wasm.md \ 25 | 8-opcodes.md \ 26 | 9-changes.md \ 27 | metadata.yaml 28 | 29 | desktop: 30 | cd code/desktop && \ 31 | cargo build --release 32 | 33 | web: 34 | cd code/wasm && \ 35 | wasm-pack build --target web && \ 36 | mv pkg/wasm_bg.wasm ../web && \ 37 | mv pkg/wasm.js ../web 38 | 39 | clean: clean_pdf clean_desktop clean_web 40 | 41 | clean_pdf: 42 | rm -f src/chip8.pdf 43 | 44 | clean_epub: 45 | rm -f src/chip8.epub 46 | 47 | clean_desktop: 48 | cd code/desktop && \ 49 | cargo clean 50 | 51 | clean_web: 52 | rm -f code/web/wasm_bg.wasm && \ 53 | rm -f code/web/wasm.js && \ 54 | cd code/wasm && \ 55 | cargo clean 56 | 57 | .PHONY: pdf desktop web clean 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An Introduction to Chip-8 Emulation using the Rust Programming Language 2 | 3 | https://github.com/aquova/chip8-book 4 | 5 | This is a introductory tutorial for how to develop your first Chip-8 emulator using the Rust language, targeting both desktop computers and web browsers via WebAssembly. This assumes no prior emulation experience, and only basic knowledge of Rust. The guide first gives a general overview of the different components of what emulation is, how all of the parts of the emulated system work, and what steps the emulation developer needs to implement them. Following this is a step-by-step walkthrough of the implementation of a Chip-8 emulator, describing each section of code and why it is needed. 6 | 7 | - Source code for the completed emulator is found in `code` 8 | - Source code for the PDF book is in `src` 9 | - Sample Chip-8 ROMs are in `roms` 10 | 11 | You can download the latest copy of the book here: https://github.com/aquova/chip8-book/releases 12 | 13 | To build a copy of the PDF yourself, first install [pandoc](https://pandoc.org/) then run `make pdf` (or `make epub` for an ePub version). Details on how to setup the build environment for the source code are provided in the PDF, but once installed the completed emulator can be built with `make desktop` or `make web` 14 | 15 | ## Credits 16 | 17 | The provided Chip-8 games are supplied from [Zophar's Domain](https://www.zophar.net/pdroms/chip8/chip-8-games-pack.html). Original author unknown. 18 | -------------------------------------------------------------------------------- /code/web/index.js: -------------------------------------------------------------------------------- 1 | import init, * as wasm from "./wasm.js" 2 | 3 | const WIDTH = 64 4 | const HEIGHT = 32 5 | const SCALE = 15 6 | const TICKS_PER_FRAME = 10 7 | let anim_frame = 0 8 | 9 | const canvas = document.getElementById("canvas") 10 | canvas.width = WIDTH * SCALE 11 | canvas.height = HEIGHT * SCALE 12 | 13 | const ctx = canvas.getContext("2d") 14 | ctx.fillStyle = "black" 15 | ctx.fillRect(0, 0, WIDTH * SCALE, HEIGHT * SCALE) 16 | 17 | const input = document.getElementById("fileinput") 18 | 19 | async function run() { 20 | await init() 21 | let chip8 = new wasm.EmuWasm() 22 | 23 | document.addEventListener("keydown", function(evt) { 24 | chip8.keypress(evt, true) 25 | }) 26 | 27 | document.addEventListener("keyup", function(evt) { 28 | chip8.keypress(evt, false) 29 | }) 30 | 31 | input.addEventListener("change", function(evt) { 32 | // Stop previous game from rendering, if one exists 33 | if (anim_frame != 0) { 34 | window.cancelAnimationFrame(anim_frame) 35 | } 36 | 37 | let file = evt.target.files[0] 38 | if (!file) { 39 | alert("Failed to read file") 40 | return 41 | } 42 | 43 | // Load in game as Uint8Array, send to .wasm, start main loop 44 | let fr = new FileReader() 45 | fr.onload = function(e) { 46 | let buffer = fr.result 47 | const rom = new Uint8Array(buffer) 48 | chip8.reset() 49 | chip8.load_game(rom) 50 | mainloop(chip8) 51 | } 52 | fr.readAsArrayBuffer(file) 53 | }, false) 54 | } 55 | 56 | function mainloop(chip8) { 57 | // Only draw every few ticks 58 | for (let i = 0; i < TICKS_PER_FRAME; i++) { 59 | chip8.tick() 60 | } 61 | chip8.tick_timers() 62 | 63 | // Clear the canvas before drawing 64 | ctx.fillStyle = "black" 65 | ctx.fillRect(0, 0, WIDTH * SCALE, HEIGHT * SCALE) 66 | // Set the draw color back to white before we render our frame 67 | ctx.fillStyle = "white" 68 | chip8.draw_screen(SCALE) 69 | 70 | anim_frame = window.requestAnimationFrame(() => { 71 | mainloop(chip8) 72 | }) 73 | } 74 | 75 | run().catch(console.error) 76 | -------------------------------------------------------------------------------- /code/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use chip8_core::*; 2 | use js_sys::Uint8Array; 3 | use wasm_bindgen::prelude::*; 4 | use wasm_bindgen::JsCast; 5 | use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, KeyboardEvent}; 6 | 7 | #[wasm_bindgen] 8 | pub struct EmuWasm { 9 | chip8: Emu, 10 | ctx: CanvasRenderingContext2d, 11 | } 12 | 13 | #[wasm_bindgen] 14 | impl EmuWasm { 15 | #[wasm_bindgen(constructor)] 16 | pub fn new() -> Result { 17 | let chip8 = Emu::new(); 18 | 19 | let document = web_sys::window().unwrap().document().unwrap(); 20 | let canvas = document.get_element_by_id("canvas").unwrap(); 21 | let canvas: HtmlCanvasElement = canvas 22 | .dyn_into::() 23 | .map_err(|_| ()) 24 | .unwrap(); 25 | 26 | let ctx = canvas.get_context("2d") 27 | .unwrap().unwrap() 28 | .dyn_into::() 29 | .unwrap(); 30 | 31 | Ok(EmuWasm{chip8, ctx}) 32 | } 33 | 34 | #[wasm_bindgen] 35 | pub fn reset(&mut self) { 36 | self.chip8.reset(); 37 | } 38 | 39 | #[wasm_bindgen] 40 | pub fn tick(&mut self) { 41 | self.chip8.tick(); 42 | } 43 | 44 | #[wasm_bindgen] 45 | pub fn tick_timers(&mut self) { 46 | self.chip8.tick_timers(); 47 | } 48 | 49 | #[wasm_bindgen] 50 | pub fn keypress(&mut self, evt: KeyboardEvent, pressed: bool) { 51 | let key = evt.key(); 52 | if let Some(k) = key2btn(&key) { 53 | self.chip8.keypress(k, pressed); 54 | } 55 | } 56 | 57 | #[wasm_bindgen] 58 | pub fn load_game(&mut self, data: Uint8Array) { 59 | self.chip8.load(&data.to_vec()); 60 | } 61 | 62 | #[wasm_bindgen] 63 | pub fn draw_screen(&mut self, scale: usize) { 64 | let disp = self.chip8.get_display(); 65 | for i in 0..(SCREEN_WIDTH * SCREEN_HEIGHT) { 66 | if disp[i] { 67 | let x = i % SCREEN_WIDTH; 68 | let y = i / SCREEN_WIDTH; 69 | self.ctx.fill_rect( 70 | (x * scale) as f64, 71 | (y * scale) as f64, 72 | scale as f64, 73 | scale as f64 74 | ); 75 | } 76 | } 77 | } 78 | } 79 | 80 | fn key2btn(key: &str) -> Option { 81 | match key { 82 | "1" => Some(0x1), 83 | "2" => Some(0x2), 84 | "3" => Some(0x3), 85 | "4" => Some(0xC), 86 | "q" => Some(0x4), 87 | "w" => Some(0x5), 88 | "e" => Some(0x6), 89 | "r" => Some(0xD), 90 | "a" => Some(0x7), 91 | "s" => Some(0x8), 92 | "d" => Some(0x9), 93 | "f" => Some(0xE), 94 | "z" => Some(0xA), 95 | "x" => Some(0x0), 96 | "c" => Some(0xB), 97 | "v" => Some(0xF), 98 | _ => None, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /code/desktop/src/main.rs: -------------------------------------------------------------------------------- 1 | use chip8_core::*; 2 | 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::env; 6 | 7 | use sdl2::event::Event; 8 | use sdl2::keyboard::Keycode; 9 | use sdl2::pixels::Color; 10 | use sdl2::rect::Rect; 11 | use sdl2::render::Canvas; 12 | use sdl2::video::Window; 13 | 14 | const SCALE: u32 = 15; 15 | const WINDOW_WIDTH: u32 = (SCREEN_WIDTH as u32) * SCALE; 16 | const WINDOW_HEIGHT: u32 = (SCREEN_HEIGHT as u32) * SCALE; 17 | const TICKS_PER_FRAME: usize = 10; 18 | 19 | fn main() { 20 | let args: Vec<_> = env::args().collect(); 21 | if args.len() != 2 { 22 | println!("Usage: cargo run path/to/game"); 23 | return; 24 | } 25 | 26 | // Setup SDL 27 | let sdl_context = sdl2::init().unwrap(); 28 | let video_subsystem = sdl_context.video().unwrap(); 29 | let window = video_subsystem 30 | .window("Chip-8 Emulator", WINDOW_WIDTH, WINDOW_HEIGHT) 31 | .position_centered() 32 | .opengl() 33 | .build() 34 | .unwrap(); 35 | 36 | let mut canvas = window.into_canvas().present_vsync().build().unwrap(); 37 | canvas.clear(); 38 | canvas.present(); 39 | 40 | let mut event_pump = sdl_context.event_pump().unwrap(); 41 | 42 | let mut chip8 = Emu::new(); 43 | 44 | let mut rom = File::open(&args[1]).expect("Unable to open file"); 45 | let mut buffer = Vec::new(); 46 | 47 | rom.read_to_end(&mut buffer).unwrap(); 48 | chip8.load(&buffer); 49 | 50 | 'gameloop: loop { 51 | for evt in event_pump.poll_iter() { 52 | match evt { 53 | Event::Quit{..} | Event::KeyDown{keycode: Some(Keycode::Escape), ..}=> { 54 | break 'gameloop; 55 | }, 56 | Event::KeyDown{keycode: Some(key), ..} => { 57 | if let Some(k) = key2btn(key) { 58 | chip8.keypress(k, true); 59 | } 60 | }, 61 | Event::KeyUp{keycode: Some(key), ..} => { 62 | if let Some(k) = key2btn(key) { 63 | chip8.keypress(k, false); 64 | } 65 | }, 66 | _ => () 67 | } 68 | } 69 | 70 | for _ in 0..TICKS_PER_FRAME { 71 | chip8.tick(); 72 | } 73 | chip8.tick_timers(); 74 | draw_screen(&chip8, &mut canvas); 75 | } 76 | } 77 | 78 | fn draw_screen(emu: &Emu, canvas: &mut Canvas) { 79 | // Clear canvas as black 80 | canvas.set_draw_color(Color::RGB(0, 0, 0)); 81 | canvas.clear(); 82 | 83 | let screen_buf = emu.get_display(); 84 | // Now set draw color to white, iterate through each point and see if it should be drawn 85 | canvas.set_draw_color(Color::RGB(255, 255, 255)); 86 | for (i, pixel) in screen_buf.iter().enumerate() { 87 | if *pixel { 88 | // Convert our 1D array's index into a 2D (x,y) position 89 | let x = (i % SCREEN_WIDTH) as u32; 90 | let y = (i / SCREEN_WIDTH) as u32; 91 | 92 | // Draw a rectangle at (x,y), scaled up by our SCALE value 93 | let rect = Rect::new((x * SCALE) as i32, (y * SCALE) as i32, SCALE, SCALE); 94 | canvas.fill_rect(rect).unwrap(); 95 | } 96 | } 97 | canvas.present(); 98 | } 99 | 100 | /* 101 | Keyboard Chip-8 102 | +---+---+---+---+ +---+---+---+---+ 103 | | 1 | 2 | 3 | 4 | | 1 | 2 | 3 | C | 104 | +---+---+---+---+ +---+---+---+---+ 105 | | Q | W | E | R | | 4 | 5 | 6 | D | 106 | +---+---+---+---+ => +---+---+---+---+ 107 | | A | S | D | F | | 7 | 8 | 9 | E | 108 | +---+---+---+---+ +---+---+---+---+ 109 | | Z | X | C | V | | A | 0 | B | F | 110 | +---+---+---+---+ +---+---+---+---+ 111 | */ 112 | 113 | fn key2btn(key: Keycode) -> Option { 114 | match key { 115 | Keycode::Num1 => Some(0x1), 116 | Keycode::Num2 => Some(0x2), 117 | Keycode::Num3 => Some(0x3), 118 | Keycode::Num4 => Some(0xC), 119 | Keycode::Q => Some(0x4), 120 | Keycode::W => Some(0x5), 121 | Keycode::E => Some(0x6), 122 | Keycode::R => Some(0xD), 123 | Keycode::A => Some(0x7), 124 | Keycode::S => Some(0x8), 125 | Keycode::D => Some(0x9), 126 | Keycode::F => Some(0xE), 127 | Keycode::Z => Some(0xA), 128 | Keycode::X => Some(0x0), 129 | Keycode::C => Some(0xB), 130 | Keycode::V => Some(0xF), 131 | _ => None, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/1-intro.md: -------------------------------------------------------------------------------- 1 | \newpage 2 | 3 | # An Introduction to Video Game Emulation with Rust 4 | 5 | Developing a video game emulator is becoming an increasingly popular hobby project for developers. It requires knowledge of low-level hardware, modern programming languages, and graphics systems to successfully create one. It is an excellent learning project; not only does it have clear goals, but it also very rewarding to successfully play games on an emulator you've written yourself. I am still a relatively new emulation developer, but I wouldn't have been able to reach the place I am now if it weren't for excellent guides and tutorials online. To that end, I wanted to give back to the community by writing a guide with some of the tricks I picked up, in hopes it is useful for someone else. 6 | 7 | ## Intro to Chip-8 8 | 9 | Our target system is the [Chip-8](https://en.wikipedia.org/wiki/CHIP-8). The Chip-8 has become the "Hello World" for emulation development of a sort. While you might be tempted to begin with something more exciting like the NES or Game Boy, these are a level of complexity higher than the Chip-8. The Chip-8 has a 1-bit monochrome display, a simple 1-channel single tone audio, and only 35 instructions (compared to ~500 for the Game Boy), but more on that later. This guide will cover the technical specifics of the Chip-8, what hardware systems need to be emulated and how, and how to interact with the user. This guide will focus on the original Chip-8 specification, and will not implement any of the many proposed extensions that have been created, such as the Super Chip-8, Chip-16, or XO-Chip; these were created independently of each other, and thus add contradictory features. 10 | 11 | ## Chip-8 Technical Specifications 12 | 13 | - A 64x32 monochrome display, drawn to via sprites that are always 8 pixels wide and between 1 and 16 pixels tall 14 | 15 | - Sixteen 8-bit general purpose registers, referred to as V0 thru VF. VF also doubles as the flag register for overflow operations 16 | 17 | - 16-bit program counter 18 | 19 | - Single 16-bit register used as a pointer for memory access, called the *I Register* 20 | 21 | - An unstandardised amount of RAM, however most emulators allocate 4 KB 22 | 23 | - 16-bit stack used for calling and returning from subroutines 24 | 25 | - 16-key keyboard input 26 | 27 | - Two special registers which decrease each frame and trigger upon reaching zero: 28 | - Delay timer: Used for time-based game events 29 | - Sound timer: Used to trigger the audio beep 30 | 31 | ## Intro to Rust 32 | 33 | Emulators can be written in nearly any programming language. This guide uses the [Rust programming language](https://www.rust-lang.org/), although the steps outlined here could be applied to any language. Rust offers a number of great advantages; it is a compiled language with targets for major platforms and it has an active community of external libraries to utilize for our project. Rust also supports building for [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly), allowing us to recompile our code to work in a browser with a minimal amount of tweaking. This guide assumes you understand the basics of the Rust language and programming as a whole. I will explain the code as we go along, but as Rust has a notoriously high learning curve, I would recommend reading and referencing the excellent [official Rust book](https://doc.rust-lang.org/stable/book/title-page.html) on any concepts that are unfamiliar to you as the guide progresses. This guide also assumes that you have Rust installed and working correctly. Please consult the [installation instructions](https://www.rust-lang.org/tools/install) for your platform if needed. 34 | 35 | ## What you will need 36 | 37 | Before you begin, please ensure you have access to or have installed the following items. 38 | 39 | ### Text Editor 40 | 41 | Any text editor can be used for the project, but there are two I recommend which offer features for Rust like syntax highlighting, code suggestions, and debugger support. 42 | 43 | - [Visual Studio Code](https://code.visualstudio.com/) is the editor I prefer for Rust, in combination with the [rust-analyzer](https://rust-analyzer.github.io/) extension. 44 | 45 | - While JetBrains does not offer a dedicated Rust IDE, there is a Rust extension for many of its other products. The [extension](https://intellij-rust.github.io/) for [CLion](https://www.jetbrains.com/clion/) has additional functionality that the others do not, such as integrated debugger support. Keep in mind that CLion is a paid product, although it offers a 30 day trial as well as extended free periods for students. 46 | 47 | If you do not care for any of these, Rust syntax and autocompletion plugins exist for many other editors, and it can be debugged fairly easily with many other debuggers, such as gdb. 48 | 49 | ### Test ROMs 50 | 51 | An emulator isn't much use if you have nothing to run! Included with the source code for this book are a number of commonly distributed Chip-8 programs, which can also be found [here](https://www.zophar.net/pdroms/chip8/chip-8-games-pack.html). Some of these games will be shown as an example throughout this guide. 52 | 53 | ### Misc. 54 | 55 | Other items that may be helpful as we progress: 56 | 57 | - Please refresh yourself with [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) notation if you do not feel comfortable with the concept. It will be used extensively throughout this project. 58 | 59 | - Chip-8 games are in a binary format, it is often helpful to be able to view the raw hex values as you debug. Standard text editors typically don't have support for viewing files in hexadecimal, instead a specialized [hex editor](https://en.wikipedia.org/wiki/Comparison_of_hex_editors) is required. Many offer similar features, but I personally prefer [Reverse Engineer's Hex Editor](https://github.com/solemnwarning/rehex). 60 | 61 | Let's begin! 62 | 63 | \newpage 64 | -------------------------------------------------------------------------------- /src/8-opcodes.md: -------------------------------------------------------------------------------- 1 | # Opcodes Table {#ot} 2 | 3 | Understanding the Opcode column: 4 | 5 | - Any hexadecimal digits (0-9, A-F) that appear in an opcode are interpreted literally, and are used to determine the operation in question. 6 | - The X or Y wild card uses the value stored in VX/VY. 7 | - N refers to a literal hexadecimal value. NN or NNN refer to two or three digit hex numbers respectively. 8 | 9 | Example: The instruction 0xD123 would match with the `DXYN` opcode, where VX is V1, VY is V2, and N is 3 (draw a 8x3 sprite at (V1, V2)) 10 | 11 | | Opcode | Description | Notes | 12 | | ------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------- | 13 | | 0000 | Nop | Do nothing, progress to next opcode | 14 | | 00E0 | Clear screen | | 15 | | 00EE | Return from subroutine | | 16 | | 1NNN | Jump to address 0xNNN | | 17 | | 2NNN | Call 0xNNN | Enter subroutine at 0xNNN, adding current PC onto stack so we can return here | 18 | | 3XNN | Skip if VX == 0xNN | | 19 | | 4XNN | Skip if VX != 0xNN | | 20 | | 5XY0 | Skip if VX == VY | | 21 | | 6XNN | VX = 0xNN | | 22 | | 7XNN | VX += 0xNN | Doesn't affect carry flag | 23 | | 8XY0 | VX = VY | | 24 | | 8XY1 | VX \|= VY | | 25 | | 8XY2 | VX &= VY | | 26 | | 8XY3 | VX ^= VY | | 27 | | 8XY4 | VX += VY | Sets VF if carry | 28 | | 8XY5 | VX -= VY | Clears VF if borrow | 29 | | 8XY6 | VX >>= 1 | Store dropped bit in VF | 30 | | 8XY7 | VX = VY - VX | Clears VF if borrow | 31 | | 8XYE | VX <<= 1 | Store dropped bit in VF | 32 | | 9XY0 | Skip if VX != VY | | 33 | | ANNN | I = 0xNNN | | 34 | | BNNN | Jump to V0 + 0xNNN | | 35 | | CXNN | VX = rand() & 0xNN | | 36 | | DXYN | Draw sprite at (VX, VY) | Sprite is 0xN pixels tall, on/off based on value in I, VF set if any pixels flipped | 37 | | EX9E | Skip if key index in VX is pressed | | 38 | | EXA1 | Skip if key index in VX isn't pressed | | 39 | | FX07 | VX = Delay Timer | | 40 | | FX0A | Waits for key press, stores index in VX | Blocking operation | 41 | | FX15 | Delay Timer = VX | | 42 | | FX18 | Sound Timer = VX | | 43 | | FX1E | I += VX | | 44 | | FX29 | Set I to address of font character in VX | | 45 | | FX33 | Stores BCD encoding of VX into I | | 46 | | FX55 | Stores V0 thru VX into RAM address starting at I | Inclusive range | 47 | | FX65 | Fills V0 thru VX with RAM values starting at address in I | Inclusive | 48 | 49 | \newpage 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /code/desktop/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "1.2.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 8 | 9 | [[package]] 10 | name = "bumpalo" 11 | version = "3.5.0" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59" 14 | 15 | [[package]] 16 | name = "cfg-if" 17 | version = "0.1.10" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 20 | 21 | [[package]] 22 | name = "cfg-if" 23 | version = "1.0.0" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 26 | 27 | [[package]] 28 | name = "chip8_core" 29 | version = "0.1.0" 30 | dependencies = [ 31 | "rand", 32 | ] 33 | 34 | [[package]] 35 | name = "desktop" 36 | version = "0.1.0" 37 | dependencies = [ 38 | "chip8_core", 39 | "sdl2", 40 | ] 41 | 42 | [[package]] 43 | name = "getrandom" 44 | version = "0.1.16" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 47 | dependencies = [ 48 | "cfg-if 1.0.0", 49 | "js-sys", 50 | "libc", 51 | "wasi", 52 | "wasm-bindgen", 53 | ] 54 | 55 | [[package]] 56 | name = "js-sys" 57 | version = "0.3.46" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" 60 | dependencies = [ 61 | "wasm-bindgen", 62 | ] 63 | 64 | [[package]] 65 | name = "lazy_static" 66 | version = "1.4.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 69 | 70 | [[package]] 71 | name = "libc" 72 | version = "0.2.81" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" 75 | 76 | [[package]] 77 | name = "log" 78 | version = "0.4.13" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" 81 | dependencies = [ 82 | "cfg-if 0.1.10", 83 | ] 84 | 85 | [[package]] 86 | name = "ppv-lite86" 87 | version = "0.2.10" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 90 | 91 | [[package]] 92 | name = "proc-macro2" 93 | version = "1.0.24" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 96 | dependencies = [ 97 | "unicode-xid", 98 | ] 99 | 100 | [[package]] 101 | name = "quote" 102 | version = "1.0.8" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" 105 | dependencies = [ 106 | "proc-macro2", 107 | ] 108 | 109 | [[package]] 110 | name = "rand" 111 | version = "0.7.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 114 | dependencies = [ 115 | "getrandom", 116 | "libc", 117 | "rand_chacha", 118 | "rand_core", 119 | "rand_hc", 120 | ] 121 | 122 | [[package]] 123 | name = "rand_chacha" 124 | version = "0.2.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 127 | dependencies = [ 128 | "ppv-lite86", 129 | "rand_core", 130 | ] 131 | 132 | [[package]] 133 | name = "rand_core" 134 | version = "0.5.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 137 | dependencies = [ 138 | "getrandom", 139 | ] 140 | 141 | [[package]] 142 | name = "rand_hc" 143 | version = "0.2.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 146 | dependencies = [ 147 | "rand_core", 148 | ] 149 | 150 | [[package]] 151 | name = "sdl2" 152 | version = "0.34.3" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "fcbb85f4211627a7291c83434d6bbfa723e28dcaa53c7606087e3c61929e4b9c" 155 | dependencies = [ 156 | "bitflags", 157 | "lazy_static", 158 | "libc", 159 | "sdl2-sys", 160 | ] 161 | 162 | [[package]] 163 | name = "sdl2-sys" 164 | version = "0.34.3" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33" 167 | dependencies = [ 168 | "cfg-if 0.1.10", 169 | "libc", 170 | "version-compare", 171 | ] 172 | 173 | [[package]] 174 | name = "syn" 175 | version = "1.0.60" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" 178 | dependencies = [ 179 | "proc-macro2", 180 | "quote", 181 | "unicode-xid", 182 | ] 183 | 184 | [[package]] 185 | name = "unicode-xid" 186 | version = "0.2.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 189 | 190 | [[package]] 191 | name = "version-compare" 192 | version = "0.0.10" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" 195 | 196 | [[package]] 197 | name = "wasi" 198 | version = "0.9.0+wasi-snapshot-preview1" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 201 | 202 | [[package]] 203 | name = "wasm-bindgen" 204 | version = "0.2.69" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" 207 | dependencies = [ 208 | "cfg-if 1.0.0", 209 | "wasm-bindgen-macro", 210 | ] 211 | 212 | [[package]] 213 | name = "wasm-bindgen-backend" 214 | version = "0.2.69" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" 217 | dependencies = [ 218 | "bumpalo", 219 | "lazy_static", 220 | "log", 221 | "proc-macro2", 222 | "quote", 223 | "syn", 224 | "wasm-bindgen-shared", 225 | ] 226 | 227 | [[package]] 228 | name = "wasm-bindgen-macro" 229 | version = "0.2.69" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" 232 | dependencies = [ 233 | "quote", 234 | "wasm-bindgen-macro-support", 235 | ] 236 | 237 | [[package]] 238 | name = "wasm-bindgen-macro-support" 239 | version = "0.2.69" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" 242 | dependencies = [ 243 | "proc-macro2", 244 | "quote", 245 | "syn", 246 | "wasm-bindgen-backend", 247 | "wasm-bindgen-shared", 248 | ] 249 | 250 | [[package]] 251 | name = "wasm-bindgen-shared" 252 | version = "0.2.69" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" 255 | -------------------------------------------------------------------------------- /src/2-basics.md: -------------------------------------------------------------------------------- 1 | # Emulation Basics {#eb} 2 | 3 | This first chapter is an overview of the concepts of emulation development and the Chip-8. Subsequent chapters will have code implementations in Rust, if you have no interest in Rust or prefer to work without examples, this chapter will give an introduction to what steps a real system performs, what we need to emulate ourselves, and how all the moving pieces fit together. If you're completely new to this subject, this should provide the information you'll need to wrap your head around what you're about to implement. 4 | 5 | ## What's in a Chip-8 Game? 6 | 7 | Let's begin with a simple question. If I give you a Chip-8 ROM[^1] file, what exactly is in it? It needs to contain all of the game logic and graphical assets needed to make a game run on a screen, but how is it all laid out? 8 | 9 | This is a basic, fundamental question; our goal is to read this file and make its program run. Well, there's only one way to find out, so let's try and open a Chip-8 game in a text editor (such as Notepad, TextEdit, or any others you prefer). For this example, I'm using the `roms/PONG2` game included with this guide. Unless you're using a very fancy editor, you should probably see something similar to Figure 1. 10 | 11 | ![Raw Chip-8 ROM file](img/PONG2_raw.png) 12 | 13 | This doesn't seem very helpful. In fact, it seems corrupted. Don't worry, your game is (probably) just fine. All computer programs are, at their core, just numbers saved into a file. These numbers can mean just about anything; it's only with context that a computer is able to put meaning to the values. Your text editor was designed to show and edit text in a language such as English, and thus will automatically try and use a language-focused context such as [ASCII](https://www.asciitable.com/). 14 | 15 | So we've learned that our Chip-8 file is not written in plain English, which we probably could have assumed already. So what does it look like? Fortunately, programs exist to display the raw contents of a file, so we will need to use one of those "hex editors" to display the actual contents of our file. 16 | 17 | ![Chip-8 ROM file](img/PONG2.png) 18 | 19 | In Figure 2, the numbers on the left of the vertical line are the *offset* values, how many bytes we are away from the start of the file. On the right hand side are the actual values stored in the file. Both the offsets and data values are displayed in hexadecimal (we'll be dealing with hexadecimal quite a bit). 20 | 21 | Okay, we have numbers now, so that's an improvement. If those numbers don't correspond to English letters, what do they mean? These are the *instructions* for the Chip-8's CPU. Actually, each instruction is two bytes, which is why I've grouped them in pairs in the screenshot. 22 | 23 | ## What is a CPU? 24 | 25 | Let me take a moment to describe exactly what functionality a CPU provides, for those who aren't familiar. A CPU, for our purposes, does math. That's it. Now, when I say it "does math", this includes your usual items such as addition, subtraction, and checking if two numbers are equal or not. There are also additional operations that are needed for the game to run, such as jumping to different sections of code, or fetching and saving numbers. The game file is entirely made up of mathematical operations for the CPU to preform. 26 | 27 | All of the mathematical operations that the Chip-8 can perform have a corresponding number, called its *opcode*. A full list of the Chip-8's opcodes can be seen on [this page](#ot). Whenever it is time for the emulator to perform another instruction (also referred to as a *tick* or a *cycle*), it will grab the next opcode from the game ROM and do whatever operation is specified by the opcode table; add, subtract, update what's drawn on the screen, whatever. 28 | 29 | What about parameters? It's not enough to say "it's time to add", you need two numbers to actually add together, plus a place to put the sum when you're done. Other systems do this differently, but two bytes per operation is a lot of possible numbers, way more than the 35 operations that Chip-8 can actually do. The extra digits are used to pack in extra information into the opcode. The exact layout of this information varies between opcodes. The opcode table uses `N`'s to indicate literal hexadecmial numbers. `N` for single digit, `NN` for two digits, and `NNN` for three digit literal values, or for a *register* to be specified via `X` or `Y`. 30 | 31 | ## What are Registers? 32 | 33 | Which brings us to our next topic. What on earth is a register? A *register* is a specially designated place to store a single byte for use in instructions. This may sound rather similar to RAM, and in some ways it is. While RAM is a large addressable place to store data, registers are usually few in number, they are all named, and they are directly used when executing an opcode. For many computers, if you want to use a value in RAM, it has to be copied into a register first. 34 | 35 | The Chip-8 has sixteen registers that can be used freely by the programmer, named V0 through VF (0-15 in hexadecimal). As an example of how registers can work, let's look at one of the addition operations in our opcode table. There is an operation for adding the values of two registers together, VX += VY, encoded as `8XY4`. The `8XY4` is used for pattern matching the opcodes. If the current opcode begins with an 8 and ends with a 4, then this is the matching operation. The middle two digits then specify which registers we are to use. Let's say our opcode is `8124`. It begins with an 8 and ends with a 4, so we're in the right place. For this instruction we will be using the values stored in V1 and V2, as those match the other two digits. Let's say V1 stores 5 and V2 stores 10, this operation would add those two values and replace what was in V1, thus V1 now holds 15. 36 | 37 | Chip-8 contains a few more registers, but they serve very specific purposes. One of the most significant ones is the *program counter* (PC), which for Chip-8 can store a 16-bit value. I've made vague references to our "current opcode", but how do we keep track of where we are? Our emulator is simulating a computer running a program. It needs to start at the beginning of our game and move from opcode to opcode, executing the instructions as it's told. The PC holds the index of what game instruction we're currently working on. So it'll start at the first byte of the game, then move on to the third (remember all opcodes are two bytes), and so on and so forth. Some instructions can also tell the PC to go somewhere else in the game, but by default it will simply move forward opcode by opcode. 38 | 39 | ## What is RAM? 40 | 41 | We have our sixteen V registers, but even for a simple system like a Chip-8, we really would like to be able to store more than 16 numbers at a time. This is where *random access memory* (RAM) comes in. You're probably familiar with it in the context of your own computer, but RAM is a large array of numbers for the CPU to do with as it pleases. Chip-8 is not a physical system, so there is no standard amount of RAM it is supposed to have. However, emulation developers have more or less settled on 4096 bytes (4 KB), so our system will have the ability to store up to 4096 8-bit numbers in its RAM, way more than most games will use. 42 | 43 | Now, time for an important detail: The Chip-8 CPU has free access to read and write to RAM as it pleases. It does not, however, have direct access to our game's ROM. With a name like "Read-Only Memory", it's safe to assume that we weren't going to be able to overwrite it, but the CPU can't read from ROM either? While the CPU needs to be able to read the game ROM, it accomplishes this indirectly, by copying the entire game into RAM[^2] when the game starts up. It is rather slow and inefficient to open our game file just to read a little bit of data over and over again. Instead, we want to be able to copy as much data as we can into RAM for us to more quickly use. The other catch is that somewhat confusingly, the ROM data is not loaded into very start of RAM. Instead, it's offset by 512 bytes (0x200). This means the first byte of the game is loaded at start into RAM address 0x200. The second ROM byte into 0x201, etc. 44 | 45 | Why doesn't the Chip-8 simply store the game at the start of RAM and call it a day? Back when the Chip-8 was designed, computers had much more limited amount of RAM, and only one program could run at a given time. The first 0x200 bytes were allocated for the Chip-8 program itself to run in. Thus, modern Chip-8 emulators need to keep that in mind, as games are still developed with that concept. Our system will actually use a little bit of that empty space, but we will cover that later. 46 | 47 | [^1]: 'ROM' stands for "Read-only memory". It is a type of memory that is hard written at manufacturing time and cannot be modified by the computer system. For the purposes of this guide "ROM file" will be used interchangeably with "game file". 48 | 49 | [^2]: Therefore, Chip-8 games have a maximum size of 4 KB, any larger and they can't be completely loaded in. 50 | 51 | \newpage 52 | -------------------------------------------------------------------------------- /src/4-methods.md: -------------------------------------------------------------------------------- 1 | # Implementing Emulation Methods 2 | 3 | We have now created our `Emu` struct and defined a number of variables for it to manage, as well as defined an initialization function. Before we move on, there are a few useful methods we should add to our object now which will come in use once we begin implementation of the instructions. 4 | 5 | ## Push and Pop 6 | 7 | We have added both a `stack` array, as well as a pointer `sp` to manage the CPU's stack, however it will be useful to implement both a `push` and `pop` method so we can access it easily. 8 | 9 | ```rust 10 | impl Emu { 11 | // -- Unchanged code omitted -- 12 | 13 | fn push(&mut self, val: u16) { 14 | self.stack[self.sp as usize] = val; 15 | self.sp += 1; 16 | } 17 | 18 | fn pop(&mut self) -> u16 { 19 | self.sp -= 1; 20 | self.stack[self.sp as usize] 21 | } 22 | 23 | // -- Unchanged code omitted -- 24 | } 25 | ``` 26 | 27 | These are pretty straightforward. `push` adds the given 16-bit value to the spot pointed to by the Stack Pointer, then moves the pointer to the next position. `pop` performs this operation in reverse, moving the SP back to the previous value then returning what is there. Note that attempting to pop an empty stack results in an underflow panic[^1]. You are welcome to add extra handling here if you like, but in the event this were to occur, that would indicate a bug with either our emulator or the game code, so I feel that a complete panic is acceptable. 28 | 29 | ## Font Sprites 30 | 31 | We haven't yet delved into how the Chip-8 screen display works, but the gist for now is that it renders *sprites* which are stored in memory to the screen, one line at a time. It is up to the game developer to correctly load their sprites before copying them over. However wouldn't it be nice if the system automatically had sprites for commonly used things, such as numbers? I mentioned earlier that our PC will begin at address 0x200, leaving the first 512 intentionally empty. Most modern emulators will use that space to store the sprite data for font characters of all the hexadecimal digits, that is characters of 0-9 and A-F. We could store this data at any fixed position in RAM, but this space is already defined as empty anyway. Each character is made up of five rows of eight pixels, with each row using a byte of data, meaning that each letter altogether takes up five bytes of data. The following diagram illustrates how a character is stored as bytes. 32 | 33 | [^1] *Underflow* is when the value of an unsigned variable goes from above zero to below zero. In some languages the value would then "roll over" to the highest possible size, but in Rust this leads to a runtime error and needs to be handled differently if desired. The same goes for values exceeding the maximum possible value, known as *overflow*. 34 | 35 | \newpage 36 | 37 | ![Chip-8 Font Sprite](img/font_diagram.png) 38 | 39 | On the right, each row is encoded into binary. Each pixel is assigned a bit, which corresponds to whether that pixel will be white or black. *Every* sprite in Chip-8 is eight pixels wide, which means a pixel row requires 8-bits (1 byte). The above diagram shows the layout of the "1" character sprite. The sprites don't need all 8 bits of width, so they all have black right halves. Sprites have been created for all of the hexadecimal digits, and are required to be present somewhere in RAM for some games to function. Later in this guide we will cover the instruction that handles these sprites, which will show how these are loaded and how the emulator knows where to find them. For now, we simply need to define them. We will do so with a constant array of bytes; at the top of `lib.rs`, add: 40 | 41 | ```rust 42 | const FONTSET_SIZE: usize = 80; 43 | 44 | const FONTSET: [u8; FONTSET_SIZE] = [ 45 | 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 46 | 0x20, 0x60, 0x20, 0x20, 0x70, // 1 47 | 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 48 | 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 49 | 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 50 | 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 51 | 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 52 | 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 53 | 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 54 | 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 55 | 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 56 | 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 57 | 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 58 | 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 59 | 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 60 | 0xF0, 0x80, 0xF0, 0x80, 0x80 // F 61 | ]; 62 | ``` 63 | 64 | You can see the bytes outlined in the "1" diagram above, all of the other letters work in a similar way. Now that these are outlined, we need to load them into RAM. Modify `Emu::new()` to copy those values in: 65 | 66 | ```rust 67 | pub fn new() -> Self { 68 | let mut new_emu = Self { 69 | pc: START_ADDR, 70 | ram: [0; RAM_SIZE], 71 | screen: [false; SCREEN_WIDTH * SCREEN_HEIGHT], 72 | v_reg: [0; NUM_REGS], 73 | i_reg: 0, 74 | sp: 0, 75 | stack: [0; STACK_SIZE], 76 | keys: [false; NUM_KEYS], 77 | dt: 0, 78 | st: 0, 79 | }; 80 | 81 | new_emu.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET); 82 | 83 | new_emu 84 | } 85 | ``` 86 | 87 | This initializes our `Emu` object in the same way as before, but copies in our character sprite data into RAM before returning it. 88 | 89 | It will also be useful to be able to reset our emulator without having to create a new object. There are fancier ways of doing this, but we'll just keep it simple and create a function that resets our member variables back to their original values when called. 90 | 91 | ```rust 92 | pub fn reset(&mut self) { 93 | self.pc = START_ADDR; 94 | self.ram = [0; RAM_SIZE]; 95 | self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT]; 96 | self.v_reg = [0; NUM_REGS]; 97 | self.i_reg = 0; 98 | self.sp = 0; 99 | self.stack = [0; STACK_SIZE]; 100 | self.keys = [false; NUM_KEYS]; 101 | self.dt = 0; 102 | self.st = 0; 103 | self.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET); 104 | } 105 | ``` 106 | 107 | ## Tick 108 | 109 | With the creation of our `Emu` object completed (for now), we can begin to define how the CPU will process each instruction and move through the game. To summarize what was described in the previous parts, the basic loop will be: 110 | 111 | 1. Fetch the value from our game (loaded into RAM) at the memory address stored in our Program Counter. 112 | 2. Decode this instruction. 113 | 3. Execute, which will possibly involve modifying our CPU registers or RAM. 114 | 4. Move the PC to the next instruction and repeat. 115 | 116 | Let's begin by adding the opcode processing to our `tick` function, beginning with the fetching step: 117 | 118 | ```rust 119 | // -- Unchanged code omitted -- 120 | 121 | pub fn tick(&mut self) { 122 | // Fetch 123 | let op = self.fetch(); 124 | // Decode 125 | // Execute 126 | } 127 | 128 | fn fetch(&mut self) -> u16 { 129 | // TODO 130 | } 131 | 132 | ``` 133 | 134 | The `fetch` function will only be called internally as part of our `tick` loop, so it doesn't need to be public. The purpose of this function is to grab the instruction we are about to execute (known as an *opcode*) for use in the next steps of this cycle. If you're unfamiliar with Chip-8's instruction format, I recommend you refresh up with the [overview](#eb) from the earlier chapters. 135 | 136 | Fortunately, Chip-8 is easier than many systems. For one, there's only 35 opcodes to deal with as opposed to the hundreds that many processors support. In addition, many systems store additional parameters for each opcode in subsequent bytes (such as operands for addition), Chip-8 encodes these into the opcode itself. Due to this, all Chip-8 opcodes are exactly 2 bytes, which is larger than some other systems, but the entire instruction is stored in those two bytes, while other contemporary systems might consume between 1 and 3 bytes per cycle. 137 | 138 | Each opcode is encoded differently, but fortunately since all instructions consume two bytes, the fetch operation is the same for all of them, and implemented as such: 139 | 140 | ```rust 141 | fn fetch(&mut self) -> u16 { 142 | let higher_byte = self.ram[self.pc as usize] as u16; 143 | let lower_byte = self.ram[(self.pc + 1) as usize] as u16; 144 | let op = (higher_byte << 8) | lower_byte; 145 | self.pc += 2; 146 | op 147 | } 148 | ``` 149 | 150 | This function fetches the 16-bit opcode stored at our current Program Counter. We store values in RAM as 8-bit values, so we fetch two and combine them as Big Endian. The PC is then incremented by the two bytes we just read, and our fetched opcode is returned for further processing. 151 | 152 | ## Timer Tick 153 | 154 | The Chip-8 specification also mentions two special purpose *timers*, the Delay Timer and the Sound Timer. While the `tick` function operates once every CPU cycle, these timers are modified instead once every frame, and thus need to be handled in a separate function. Their behavior is rather simple, every frame both decrease by one. If the Sound Timer is set to one, the system will emit a 'beep' noise. If the timers ever hit zero, they do not automatically reset; they will remain at zero until the game manually resets them to some value. 155 | 156 | ```rust 157 | pub fn tick_timers(&mut self) { 158 | if self.dt > 0 { 159 | self.dt -= 1; 160 | } 161 | 162 | if self.st > 0 { 163 | if self.st == 1 { 164 | // BEEP 165 | } 166 | self.st -= 1; 167 | } 168 | } 169 | ``` 170 | 171 | Audio is the one thing that this guide won't cover, mostly due to increased complexity in getting audio to work in both our desktop and web browser frontends. For now we'll simply leave a comment where the beep would occur, but any curious readers are encouraged to implement it themselves (and then tell me how they did it). 172 | 173 | \newpage 174 | -------------------------------------------------------------------------------- /src/3-setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Since the eventual goal of this project is to have an emulator which can be built for both the desktop and a web browser, we're going to structure this project slightly unusually. Between the two builds, the actual emulation of the Chip-8 system should be exactly the same, only things like reading in a file and displaying the screen will be different between desktop and browser. To that end, we will create the backend, which I will call the *core*, as its own crate to be used by both our future frontends. 4 | 5 | Move into the folder where you will store your project and run the following command. Do not include the `$`, that is simply to indicate that this is a terminal instruction. 6 | 7 | ``` 8 | $ cargo init chip8_core --lib 9 | ``` 10 | 11 | The `--lib` flag tells `cargo` to create a library rather than an executable module. This is where our emulation backend will go. I called it `chip8_core` for the purposes of this project, but you are free to call it whatever you like. 12 | 13 | As for the frontend, we'll create an additional crate to hold that code: 14 | 15 | ``` 16 | $ cargo init desktop 17 | ``` 18 | 19 | Unlike our core, this creates an actual executable project. If you've done it correctly, your folder structure should now look like this: 20 | 21 | ![Initial file structure](img/init_tree.png) 22 | 23 | All that remains is to tell our `desktop` frontend where to find `chip8_core`. Open up `desktop/Cargo.toml` and under `[dependencies]` add the line: 24 | 25 | ```toml 26 | chip8_core = { path = "../chip8_core" } 27 | ``` 28 | 29 | Since `chip8_core` is currently empty, this doesn't actually add anything, but it's something that will need to be done eventually. Go ahead and try and compile and run the project, just to be sure everything is working. From inside the `desktop` directory, run: 30 | 31 | ``` 32 | $ cargo run 33 | ``` 34 | 35 | If everything has been setup correctly, "Hello, world!" should print out. Great! Next, we'll begin emulating the CPU and start creating something a little more interesting. 36 | 37 | ## Defining our Emulator 38 | 39 | The most fundamental part of the system is the CPU, so we will begin there when creating our emulator. For the time being, we will mostly be developing the `chip8_core` backend, then coming back to supply our frontend when it's well along. We will begin by working in the `chip8_core/src/lib.rs` file (you can delete the auto-generated `test` code). Before we add any functionality, let's refresh some of the concepts of what we're about to do. 40 | 41 | Emulation is simply executing a program originally written for a different system, so it functions very similarly to the execution of a modern computer program. When running any old program, a line of code is read, understood by the computer to perform some task such as modifying a variable, making a comparison, or jumping to a different line of code; that action is then taken, and the execution moves to the next line to repeat this process. If you've studied compilers, you'll know that when a system is running code, it's not doing it line by line, but instead converts the code into instructions understood by the processor, and then performs this loop upon those. This is exactly how our emulator will function. We will traverse value by value in our game program, __fetching__ the instruction stored there, __decoding__ the operation that must be done, and then __executing__ it, before moving on to the next. This *fetch-decode-execute* loop will form the core of our CPU emulation. 42 | 43 | With this in mind, let's begin by defining a class which will manage our emulator. In `lib.rs`, we'll add a new empty struct: 44 | 45 | ```rust 46 | pub struct Emu { 47 | } 48 | ``` 49 | 50 | This struct will be the main object for our emulation backend, and thus must handle running the actual game, and be able to pass need information back and forth from the frontend (such as what's on the screen and button presses). 51 | 52 | ## Program Counter 53 | 54 | But what to put in our `Emu` object? As discussed previously, the program needs to know where in the game it's currently executing. All CPUs accomplish this by simply keeping an index of the current instruction, stored into a special *register* known as the *Program Counter*, or PC for short. This will be key for the *fetch* portion of our loop, and will increment through the game as it runs, and can even be modified manually by some instructions (for things such as jumping into a different section of code or calling a subroutine). Let's add this to our struct: 55 | 56 | ```rust 57 | pub struct Emu { 58 | pc: u16, 59 | } 60 | ``` 61 | 62 | ## RAM 63 | 64 | While we could read from our game file every time we need a new instruction, this is very slow, inefficient, and simply not how real systems do it. Instead, the Chip-8 is designed to copy the entire game program into its own RAM space, where it can then read and written to as needed. It should be noted that many systems, such as the Game Boy, do not allow the CPU to overwrite area of memory with the game stored in it (you wouldn't want a bug to completely corrupt game code). However, the Chip-8 has no such restriction. Since the Chip-8 was never a physical system, there isn't an official standard for how much memory it should have. However, it was originally designed to be implemented on computers with 4096 bytes (4 KB) of RAM, so that's how much we shall give it as well. Most programs won't come close to using it all, but it's there if they need it. Let's define that in our program. 65 | 66 | ```rust 67 | const RAM_SIZE: usize = 4096; 68 | 69 | pub struct Emu { 70 | pc: u16, 71 | ram: [u8; RAM_SIZE], 72 | } 73 | 74 | ``` 75 | 76 | ## The display 77 | 78 | Chip-8 uses a 64x32 monochrome display (1 bit per pixel). There's nothing too special about this, however it is one of the few things in our backend that will need to be accessible to our various frontends, and to the user. Unlike many systems, Chip-8 does not automatically clear its screen to redraw every frame, instead the screen state is maintained, and new sprites are drawn onto it (there is a clear screen command however). We can keep this screen data in an array in our Emu object. Chip-8 is also more basic than most systems as we only have to deal with two colors - black and white. Since this is a 1-bit display, we can simply store an array of booleans like so: 79 | 80 | ```rust 81 | pub const SCREEN_WIDTH: usize = 64; 82 | pub const SCREEN_HEIGHT: usize = 32; 83 | 84 | const RAM_SIZE: usize = 4096; 85 | 86 | pub struct Emu { 87 | pc: u16, 88 | ram: [u8; RAM_SIZE], 89 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 90 | } 91 | 92 | ``` 93 | 94 | You'll also notice that unlike our previous constant, we've defined the screen dimensions as public constants. This is one of the few pieces of information that the frontend will need to actually draw the screen. 95 | 96 | ## V Registers 97 | 98 | While the system has quite a bit of RAM to work with, RAM access is usually considered fairly slow (but still orders of magnitude faster than reading from disc). To speed things up, the Chip-8 defines sixteen 8-bit *registers* which the game can use as it pleases for much faster operation. These are referred to as the *V registers*, and are usually numbered in hex from V0 to VF (I'm honestly not sure what the *V* stands for), and we'll group them together in one array in our Emu struct. 99 | 100 | ```rust 101 | pub const SCREEN_WIDTH: usize = 64; 102 | pub const SCREEN_HEIGHT: usize = 32; 103 | 104 | const RAM_SIZE: usize = 4096; 105 | const NUM_REGS: usize = 16; 106 | 107 | pub struct Emu { 108 | pc: u16, 109 | ram: [u8; RAM_SIZE], 110 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 111 | v_reg: [u8; NUM_REGS], 112 | } 113 | 114 | ``` 115 | 116 | ## I Register 117 | 118 | There is also another 16-bit register known as the *I register*, which is used for indexing into RAM for reads and writes. We'll get into the finer details of how this works later on, for now we simply need to have it. 119 | 120 | ```rust 121 | pub const SCREEN_WIDTH: usize = 64; 122 | pub const SCREEN_HEIGHT: usize = 32; 123 | 124 | const RAM_SIZE: usize = 4096; 125 | const NUM_REGS: usize = 16; 126 | 127 | pub struct Emu { 128 | pc: u16, 129 | ram: [u8; RAM_SIZE], 130 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 131 | v_reg: [u8; NUM_REGS], 132 | i_reg: u16, 133 | } 134 | 135 | ``` 136 | 137 | ## The stack 138 | 139 | The CPU also has a small *stack*, which is an array of 16-bit values that the CPU can read and write to. The stack differs from regular RAM as the stack can only be read/written to via a "Last In, First Out (LIFO)" principle (like a stack of pancakes!), when you go to grab (pop) a value, you remove the last one you added (pushed). Unlike many systems, the stack is not general purpose. The only times the stack is allowed to be used is when you are entering or exiting a subroutine, where the stack is used to know where you started so you can return after the routine ends. Again, Chip-8 doesn't officially state how many numbers the stack can hold, but 16 is a typical number for emulation developers. There are a number of different ways we could implement our stack, perhaps the easiest way would be to use the `std::collections::VecDeque` object from Rust's standard library. This works fine for a Desktop-only build, however at the time of writing, many items in the `std` library don't work for WebAssembly builds. Instead we will do it the old fashioned way, with a statically sized array and an index so we know where the top of the stack is, known as the *Stack Pointer* (SP). 140 | 141 | ```rust 142 | pub const SCREEN_WIDTH: usize = 64; 143 | pub const SCREEN_HEIGHT: usize = 32; 144 | 145 | const RAM_SIZE: usize = 4096; 146 | const NUM_REGS: usize = 16; 147 | const STACK_SIZE: usize = 16; 148 | 149 | pub struct Emu { 150 | pc: u16, 151 | ram: [u8; RAM_SIZE], 152 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 153 | v_reg: [u8; NUM_REGS], 154 | i_reg: u16, 155 | sp: u16, 156 | stack: [u16; STACK_SIZE], 157 | } 158 | 159 | ``` 160 | 161 | ## The keys 162 | 163 | Chip-8 supports a surprisingly large 16 different keys, typically numbered in hexadecimal from 0 through 9, A through F. The keys are arranged similarly to a telephone layout, with A and B placed to either side of 0, and C thru F placed on the right column, making a 4x4 grid. 164 | 165 | ![Keyboard to Chip-8 key layout](img/input_layout.png) 166 | 167 | We need to keep track of which of the keys are pressed, thus we can use an array of booleans to track the states. 168 | 169 | ```rust 170 | pub const SCREEN_WIDTH: usize = 64; 171 | pub const SCREEN_HEIGHT: usize = 32; 172 | 173 | const RAM_SIZE: usize = 4096; 174 | const NUM_REGS: usize = 16; 175 | const STACK_SIZE: usize = 16; 176 | const NUM_KEYS: usize = 16; 177 | 178 | pub struct Emu { 179 | pc: u16, 180 | ram: [u8; RAM_SIZE], 181 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 182 | v_reg: [u8; NUM_REGS], 183 | i_reg: u16, 184 | sp: u16, 185 | stack: [u16; STACK_SIZE], 186 | keys: [bool; NUM_KEYS], 187 | } 188 | 189 | ``` 190 | 191 | 192 | ## The timers 193 | 194 | This has been a lot to process at once, but we're now at the final items. Chip-8 also has two other special registers that it uses as *timers*. The first, the *Delay Timer* is used by the system as a typical timer, counting down every cycle and performing some action if it hits 0. The *Sound Timer* on the other hand, also counts down every clock cycle, but upon hitting 0 emits a noise. Setting the *Sound Timer* to 0 is the only way to emit audio on the Chip-8, as we will see later. These are both 8-bit registers, and must be supported for us to continue. 195 | 196 | ```rust 197 | pub const SCREEN_WIDTH: usize = 64; 198 | pub const SCREEN_HEIGHT: usize = 32; 199 | 200 | const RAM_SIZE: usize = 4096; 201 | const NUM_REGS: usize = 16; 202 | const STACK_SIZE: usize = 16; 203 | const NUM_KEYS: usize = 16; 204 | 205 | pub struct Emu { 206 | pc: u16, 207 | ram: [u8; RAM_SIZE], 208 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 209 | v_reg: [u8; NUM_REGS], 210 | i_reg: u16, 211 | sp: u16, 212 | stack: [u16; STACK_SIZE], 213 | keys: [bool; NUM_KEYS], 214 | dt: u8, 215 | st: u8, 216 | } 217 | ``` 218 | 219 | ## Initialization 220 | 221 | That should do it for now, let's go ahead and implement a `new` constructor for this class before we move onto the next part. Following the `struct` definition, we'll implement and set the default values: 222 | 223 | ```rust 224 | // -- Unchanged code omitted -- 225 | 226 | const START_ADDR: u16 = 0x200; 227 | 228 | // -- Unchanged code omitted -- 229 | 230 | impl Emu { 231 | pub fn new() -> Self { 232 | Self { 233 | pc: START_ADDR, 234 | ram: [0; RAM_SIZE], 235 | screen: [false; SCREEN_WIDTH * SCREEN_HEIGHT], 236 | v_reg: [0; NUM_REGS], 237 | i_reg: 0, 238 | sp: 0, 239 | stack: [0; STACK_SIZE], 240 | keys: [false; NUM_KEYS], 241 | dt: 0, 242 | st: 0, 243 | } 244 | } 245 | } 246 | ``` 247 | 248 | Everything seems pretty straightforward, we simply initialize all values and arrays to zero... except for our Program Counter, which gets set to 0x200 (512 in decimal). I mentioned the reasoning behind this in the previous chapter, but the emulator has to know where the beginning of the program is, and it is Chip-8 standard that the beginning of all Chip-8 programs will be loaded in starting at RAM address 0x200. This number will come up again, so we've defined it as a constant. 249 | 250 | That wraps up this part! With the basis of our emulator underway, we can begin to implement the execution! 251 | 252 | \newpage 253 | -------------------------------------------------------------------------------- /code/chip8_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | pub const SCREEN_WIDTH: usize = 64; 4 | pub const SCREEN_HEIGHT: usize = 32; 5 | 6 | const START_ADDR: u16 = 0x200; 7 | const RAM_SIZE: usize = 4096; 8 | const NUM_REGS: usize = 16; 9 | const STACK_SIZE: usize = 16; 10 | const NUM_KEYS: usize = 16; 11 | const FONTSET_SIZE: usize = 80; 12 | 13 | const FONTSET: [u8; FONTSET_SIZE] = [ 14 | 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 15 | 0x20, 0x60, 0x20, 0x20, 0x70, // 1 16 | 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 17 | 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 18 | 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 19 | 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 20 | 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 21 | 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 22 | 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 23 | 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 24 | 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 25 | 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 26 | 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 27 | 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 28 | 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 29 | 0xF0, 0x80, 0xF0, 0x80, 0x80 // F 30 | ]; 31 | 32 | pub struct Emu { 33 | pc: u16, 34 | ram: [u8; RAM_SIZE], 35 | screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], 36 | v_reg: [u8; NUM_REGS], 37 | i_reg: u16, 38 | sp: u16, 39 | stack: [u16; STACK_SIZE], 40 | keys: [bool; NUM_KEYS], 41 | dt: u8, 42 | st: u8, 43 | } 44 | 45 | impl Emu { 46 | pub fn new() -> Self { 47 | let mut new_emu = Self { 48 | pc: START_ADDR, 49 | ram: [0; RAM_SIZE], 50 | screen: [false; SCREEN_WIDTH * SCREEN_HEIGHT], 51 | v_reg: [0; NUM_REGS], 52 | i_reg: 0, 53 | sp: 0, 54 | stack: [0; STACK_SIZE], 55 | keys: [false; NUM_KEYS], 56 | dt: 0, 57 | st: 0, 58 | }; 59 | 60 | new_emu.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET); 61 | 62 | new_emu 63 | } 64 | 65 | pub fn reset(&mut self) { 66 | self.pc = START_ADDR; 67 | self.ram = [0; RAM_SIZE]; 68 | self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT]; 69 | self.v_reg = [0; NUM_REGS]; 70 | self.i_reg = 0; 71 | self.sp = 0; 72 | self.stack = [0; STACK_SIZE]; 73 | self.keys = [false; NUM_KEYS]; 74 | self.dt = 0; 75 | self.st = 0; 76 | self.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET); 77 | } 78 | 79 | fn push(&mut self, val: u16) { 80 | self.stack[self.sp as usize] = val; 81 | self.sp += 1; 82 | } 83 | 84 | fn pop(&mut self) -> u16 { 85 | self.sp -= 1; 86 | self.stack[self.sp as usize] 87 | } 88 | 89 | pub fn tick(&mut self) { 90 | // Fetch 91 | let op = self.fetch(); 92 | // Decode & execute 93 | self.execute(op); 94 | } 95 | 96 | pub fn get_display(&self) -> &[bool] { 97 | &self.screen 98 | } 99 | 100 | pub fn keypress(&mut self, idx: usize, pressed: bool) { 101 | self.keys[idx] = pressed; 102 | } 103 | 104 | pub fn load(&mut self, data: &[u8]) { 105 | let start = START_ADDR as usize; 106 | let end = (START_ADDR as usize) + data.len(); 107 | self.ram[start..end].copy_from_slice(data); 108 | } 109 | 110 | pub fn tick_timers(&mut self) { 111 | if self.dt > 0 { 112 | self.dt -= 1; 113 | } 114 | 115 | if self.st > 0 { 116 | if self.st == 1 { 117 | // BEEP 118 | } 119 | self.st -= 1; 120 | } 121 | } 122 | 123 | fn fetch(&mut self) -> u16 { 124 | let higher_byte = self.ram[self.pc as usize] as u16; 125 | let lower_byte = self.ram[(self.pc + 1) as usize] as u16; 126 | let op = (higher_byte << 8) | lower_byte; 127 | self.pc += 2; 128 | op 129 | } 130 | 131 | fn execute(&mut self, op: u16) { 132 | let digit1 = (op & 0xF000) >> 12; 133 | let digit2 = (op & 0x0F00) >> 8; 134 | let digit3 = (op & 0x00F0) >> 4; 135 | let digit4 = op & 0x000F; 136 | 137 | match (digit1, digit2, digit3, digit4) { 138 | // NOP 139 | (0, 0, 0, 0) => return, 140 | // CLS 141 | (0, 0, 0xE, 0) => { 142 | self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT]; 143 | }, 144 | // RET 145 | (0, 0, 0xE, 0xE) => { 146 | let ret_addr = self.pop(); 147 | self.pc = ret_addr; 148 | }, 149 | // JMP NNN 150 | (1, _, _, _) => { 151 | let nnn = op & 0xFFF; 152 | self.pc = nnn; 153 | }, 154 | // CALL NNN 155 | (2, _, _, _) => { 156 | let nnn = op & 0xFFF; 157 | self.push(self.pc); 158 | self.pc = nnn; 159 | }, 160 | // SKIP VX == NN 161 | (3, _, _, _) => { 162 | let x = digit2 as usize; 163 | let nn = (op & 0xFF) as u8; 164 | if self.v_reg[x] == nn { 165 | self.pc += 2; 166 | } 167 | }, 168 | // SKIP VX != NN 169 | (4, _, _, _) => { 170 | let x = digit2 as usize; 171 | let nn = (op & 0xFF) as u8; 172 | if self.v_reg[x] != nn { 173 | self.pc += 2; 174 | } 175 | }, 176 | // SKIP VX == VY 177 | (5, _, _, _) => { 178 | let x = digit2 as usize; 179 | let y = digit3 as usize; 180 | if self.v_reg[x] == self.v_reg[y] { 181 | self.pc += 2; 182 | } 183 | }, 184 | // VX = NN 185 | (6, _, _, _) => { 186 | let x = digit2 as usize; 187 | let nn = (op & 0xFF) as u8; 188 | self.v_reg[x] = nn; 189 | }, 190 | // VX += NN 191 | (7, _, _, _) => { 192 | let x = digit2 as usize; 193 | let nn = (op & 0xFF) as u8; 194 | self.v_reg[x] = self.v_reg[x].wrapping_add(nn); 195 | }, 196 | // VX = VY 197 | (8, _, _, 0) => { 198 | let x = digit2 as usize; 199 | let y = digit3 as usize; 200 | self.v_reg[x] = self.v_reg[y]; 201 | }, 202 | // VX |= VY 203 | (8, _, _, 1) => { 204 | let x = digit2 as usize; 205 | let y = digit3 as usize; 206 | self.v_reg[x] |= self.v_reg[y]; 207 | }, 208 | // VX &= VY 209 | (8, _, _, 2) => { 210 | let x = digit2 as usize; 211 | let y = digit3 as usize; 212 | self.v_reg[x] &= self.v_reg[y]; 213 | }, 214 | // VX ^= VY 215 | (8, _, _, 3) => { 216 | let x = digit2 as usize; 217 | let y = digit3 as usize; 218 | self.v_reg[x] ^= self.v_reg[y]; 219 | }, 220 | // VX += VY 221 | (8, _, _, 4) => { 222 | let x = digit2 as usize; 223 | let y = digit3 as usize; 224 | 225 | let (new_vx, carry) = self.v_reg[x].overflowing_add(self.v_reg[y]); 226 | let new_vf = if carry { 1 } else { 0 }; 227 | 228 | self.v_reg[x] = new_vx; 229 | self.v_reg[0xF] = new_vf; 230 | }, 231 | // VX -= VY 232 | (8, _, _, 5) => { 233 | let x = digit2 as usize; 234 | let y = digit3 as usize; 235 | 236 | let (new_vx, borrow) = self.v_reg[x].overflowing_sub(self.v_reg[y]); 237 | let new_vf = if borrow { 0 } else { 1 }; 238 | 239 | self.v_reg[x] = new_vx; 240 | self.v_reg[0xF] = new_vf; 241 | }, 242 | // VX >>= 1 243 | (8, _, _, 6) => { 244 | let x = digit2 as usize; 245 | let lsb = self.v_reg[x] & 1; 246 | self.v_reg[x] >>= 1; 247 | self.v_reg[0xF] = lsb; 248 | }, 249 | // VX = VY - VX 250 | (8, _, _, 7) => { 251 | let x = digit2 as usize; 252 | let y = digit3 as usize; 253 | 254 | let (new_vx, borrow) = self.v_reg[y].overflowing_sub(self.v_reg[x]); 255 | let new_vf = if borrow { 0 } else { 1 }; 256 | 257 | self.v_reg[x] = new_vx; 258 | self.v_reg[0xF] = new_vf; 259 | }, 260 | // VX <<= 1 261 | (8, _, _, 0xE) => { 262 | let x = digit2 as usize; 263 | let msb = (self.v_reg[x] >> 7) & 1; 264 | self.v_reg[x] <<= 1; 265 | self.v_reg[0xF] = msb; 266 | }, 267 | // SKIP VX != VY 268 | (9, _, _, 0) => { 269 | let x = digit2 as usize; 270 | let y = digit3 as usize; 271 | if self.v_reg[x] != self.v_reg[y] { 272 | self.pc += 2; 273 | } 274 | }, 275 | // I = NNN 276 | (0xA, _, _, _) => { 277 | let nnn = op & 0xFFF; 278 | self.i_reg = nnn; 279 | }, 280 | // JMP V0 + NNN 281 | (0xB, _, _, _) => { 282 | let nnn = op & 0xFFF; 283 | self.pc = (self.v_reg[0] as u16) + nnn; 284 | }, 285 | // VX = rand() & NN 286 | (0xC, _, _, _) => { 287 | let x = digit2 as usize; 288 | let nn = (op & 0xFF) as u8; 289 | let rng: u8 = rand::thread_rng().gen(); 290 | self.v_reg[x] = rng & nn; 291 | }, 292 | // DRAW 293 | (0xD, _, _, _) => { 294 | // Get the (x, y) coords for our sprite 295 | let x_coord = self.v_reg[digit2 as usize] as u16; 296 | let y_coord = self.v_reg[digit3 as usize] as u16; 297 | // The last digit determines how many rows high our sprite is 298 | let num_rows = digit4; 299 | 300 | // Keep track if any pixels were flipped 301 | let mut flipped = false; 302 | // Iterate over each row of our sprite 303 | for y_line in 0..num_rows { 304 | // Determine which memory address our row's data is stored 305 | let addr = self.i_reg + y_line as u16; 306 | let pixels = self.ram[addr as usize]; 307 | // Iterate over each column in our row 308 | for x_line in 0..8 { 309 | // Use a mask to fetch current pixel's bit. Only flip if a 1 310 | if (pixels & (0b1000_0000 >> x_line)) != 0 { 311 | // Sprites should wrap around screen, so apply modulo 312 | let x = (x_coord + x_line) as usize % SCREEN_WIDTH; 313 | let y = (y_coord + y_line) as usize % SCREEN_HEIGHT; 314 | 315 | // Get our pixel's index in the 1D screen array 316 | let idx = x + SCREEN_WIDTH * y; 317 | // Check if we're about to flip the pixel and set 318 | flipped |= self.screen[idx]; 319 | self.screen[idx] ^= true; 320 | } 321 | } 322 | } 323 | // Populate VF register 324 | if flipped { 325 | self.v_reg[0xF] = 1; 326 | } else { 327 | self.v_reg[0xF] = 0; 328 | } 329 | }, 330 | // SKIP KEY PRESS 331 | (0xE, _, 9, 0xE) => { 332 | let x = digit2 as usize; 333 | let vx = self.v_reg[x]; 334 | let key = self.keys[vx as usize]; 335 | if key { 336 | self.pc += 2; 337 | } 338 | }, 339 | // SKIP KEY RELEASE 340 | (0xE, _, 0xA, 1) => { 341 | let x = digit2 as usize; 342 | let vx = self.v_reg[x]; 343 | let key = self.keys[vx as usize]; 344 | if !key { 345 | self.pc += 2; 346 | } 347 | }, 348 | // VX = DT 349 | (0xF, _, 0, 7) => { 350 | let x = digit2 as usize; 351 | self.v_reg[x] = self.dt; 352 | }, 353 | // WAIT KEY 354 | (0xF, _, 0, 0xA) => { 355 | let x = digit2 as usize; 356 | let mut pressed = false; 357 | for i in 0..self.keys.len() { 358 | if self.keys[i] { 359 | self.v_reg[x] = i as u8; 360 | pressed = true; 361 | break; 362 | } 363 | } 364 | 365 | if !pressed { 366 | // Redo opcode 367 | self.pc -= 2; 368 | } 369 | }, 370 | // DT = VX 371 | (0xF, _, 1, 5) => { 372 | let x = digit2 as usize; 373 | self.dt = self.v_reg[x]; 374 | }, 375 | // ST = VX 376 | (0xF, _, 1, 8) => { 377 | let x = digit2 as usize; 378 | self.st = self.v_reg[x]; 379 | }, 380 | // I += VX 381 | (0xF, _, 1, 0xE) => { 382 | let x = digit2 as usize; 383 | let vx = self.v_reg[x] as u16; 384 | self.i_reg = self.i_reg.wrapping_add(vx); 385 | }, 386 | // I = FONT 387 | (0xF, _, 2, 9) => { 388 | let x = digit2 as usize; 389 | let c = self.v_reg[x] as u16; 390 | self.i_reg = c * 5; 391 | }, 392 | // BCD 393 | (0xF, _, 3, 3) => { 394 | let x = digit2 as usize; 395 | let vx = self.v_reg[x] as f32; 396 | 397 | // Fetch the hundreds digit by dividing by 100 and tossing the decimal 398 | let hundreds = (vx / 100.0).floor() as u8; 399 | // Fetch the tens digit by dividing by 10, tossing the ones digit and the decimal 400 | let tens = ((vx / 10.0) % 10.0).floor() as u8; 401 | // Fetch the ones digit by tossing the hundreds and the tens 402 | let ones = (vx % 10.0) as u8; 403 | 404 | self.ram[self.i_reg as usize] = hundreds; 405 | self.ram[(self.i_reg + 1) as usize] = tens; 406 | self.ram[(self.i_reg + 2) as usize] = ones; 407 | }, 408 | // STORE V0 - VX 409 | (0xF, _, 5, 5) => { 410 | let x = digit2 as usize; 411 | let i = self.i_reg as usize; 412 | for idx in 0..=x { 413 | self.ram[i + idx] = self.v_reg[idx]; 414 | } 415 | }, 416 | // LOAD V0 - VX 417 | (0xF, _, 6, 5) => { 418 | let x = digit2 as usize; 419 | let i = self.i_reg as usize; 420 | for idx in 0..=x { 421 | self.v_reg[idx] = self.ram[i + idx]; 422 | } 423 | }, 424 | (_, _, _, _) => unimplemented!("Unimplemented opcode: {:#04x}", op), 425 | } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/7-wasm.md: -------------------------------------------------------------------------------- 1 | # Introduction to WebAssembly 2 | 3 | This section will discuss how to take our finished emulator and configure it to run in a web browser via a relatively new technology called *WebAssembly*. I encourage you to [read more](https://en.wikipedia.org/wiki/WebAssembly) about WebAssembly. It is a format for compiling programs into a binary executable, similar in scope to an .exe, but are meant to be run within a web browser. It is supported by all of the major web browsers, and is a cross-company standard being developed between them. This means that instead of having to write web code in JavaScript or other web-centric languages, you can write it in any language that supports compilation of .wasm files and still be able to run in a browser. At the time of writing, C, C++, and Rust are the major languages which support it, fortunately for us. 4 | 5 | ## Setting Up 6 | 7 | While we could cross-compile to the WebAssembly Rust targets ourselves, a useful set of tools called [wasm-pack](https://github.com/rustwasm/wasm-pack) has been developed to allow us to easily compile to WebAssembly without manually adding the appropriate targets and dependencies. You will need to install it via: 8 | 9 | ``` 10 | $ cargo install wasm-pack 11 | ``` 12 | 13 | If you are on Windows the install might fail at the openssl-sys crate https://github.com/rustwasm/wasm-pack/issues/1108 and you will have to download it manually from https://rustwasm.github.io/wasm-pack/ 14 | 15 | I also mentioned that we will need to make a slight adjustment to our `chip8_core` module to allow it to compile correctly to the `wasm` target. Rust uses a system called `wasm-bindgen` to create hooks that will work with WebAssembly. All of the `std` code we use is already fine, however we also use the `rand` crate in our backend, and it is not currently set to work correctly. Fortunately, it does support the functionality, we just need to enable it. In `chip8_core/Cargo.toml` we need to change 16 | 17 | ```toml 18 | [dependencies] 19 | rand = "^0.7.3" 20 | ``` 21 | 22 | to 23 | 24 | ```toml 25 | [dependencies] 26 | rand = { version = "^0.7.3", features = ["wasm-bindgen"] } 27 | ``` 28 | 29 | All this does is specifies that we will require `rand` to include the `wasm-bindgen` feature upon compilation, which will allow it to work correctly in our WebAssembly binary. 30 | 31 | Note: In the time between writing this tutorial's code and finishing the write-up, the `rand` crate updated to version 0.8. Among other changes is that the `wasm-bindgen` feature has been removed. If you are wanting to use the most up-to-date `rand` crate, it appears that WebAssembly support has been moved out into its own separate crate. Since we are only using the most basic random function, I didn't feel the need to upgrade to 0.8, but if you wish to, it appears that additional integration would be required. 32 | 33 | That's the last time you will need to edit your `chip8_core` module, everything else will be done in our new frontend. Let's set that up now. First, lets crate another Rust module via: 34 | 35 | ``` 36 | $ cargo init wasm --lib 37 | ``` 38 | 39 | This command may look familiar, it will create another new Rust library called `wasm`. Just like `desktop`, we will need to edit `wasm/Cargo.toml` to point to where `chip8_core` is located. 40 | 41 | ```toml 42 | [dependencies] 43 | chip8_core = { path = "../chip8_core" } 44 | ``` 45 | 46 | Now, a big difference between our `desktop` and our new `wasm` is that `desktop` was an executable project, it had a `main.rs` that we would compile and run. `wasm` will not have that, it is meant to be compiled into a .wasm file that we will load into a webpage. It is the webpage that will serve as the frontend, so let's add some basic HTML boilerplate, just to get us started. Create a new folder called `web` to hold the webpage specific code, and then create `web/index.html` and add basic HTML boilerplate. 47 | 48 | ```html 49 | 50 | 51 | 52 | Chip-8 Emulator 53 | 54 | 55 | 56 |

My Chip-8 Emulator

57 | 58 | 59 | ``` 60 | 61 | We'll add more to it later, but for now this will suffice. Our web program will not run if you simply open the file in a web browser, you will need to start a web server first. If you have Python 3 installed, which all modern Macs and many Linux distributions do, you can simply start a web server via: 62 | 63 | ``` 64 | $ python3 -m http.server 65 | ``` 66 | 67 | Navigate to `localhost` in your web browser. If you ran this in the `web` directory, you should see our `index.html` page displayed. I've tried to find a simple, built-in way to start a local web server on Windows, and I haven't really found one. I personally use Python 3, but you are welcome to use any other similar service, such as `npm` or even some Visual Studio Code extensions. It doesn't matter which, just so they can host a local web page. 68 | 69 | ## Defining our WebAssembly API 70 | 71 | We have our `chip8_core` created already, but we are now missing all of the functionality we added to `desktop`. Loading a file, handling key presses, telling it when to tick, etc. On the other hand, we have a web page that (will) run JavaScript, which needs to handle inputs from the user and display items. Our `wasm` crate is what goes in the middle. It will take inputs from JavaScript and convert them into the data types required by our `chip8_core`. 72 | 73 | Most importantly, we also need to somehow create a `chip8_core::Emu` object and keep it in scope for the entirety of our web page. 74 | 75 | To begin, let's include a few external crates that we will need to allow Rust to interface with JavaScript. Open up `wasm/Cargo.toml` and add the following dependencies: 76 | 77 | ```toml 78 | [dependencies] 79 | chip8_core = { path = "../chip8_core" } 80 | js-sys = "^0.3.46" 81 | wasm-bindgen = "^0.2.69" 82 | 83 | [dependencies.web-sys] 84 | version = "^0.3.46" 85 | features = [] 86 | ``` 87 | 88 | You'll notice that we're handling `web-sys` differently than other dependencies. That crate is structured in such a way that instead of getting everything it contains simply by including it in our `Cargo.toml`, we also need to specify additional "features" which come with the crate, but aren't available by default. Keep this file open, as we'll be adding to the `web_sys` features soon enough. 89 | 90 | Since this crate is going to be interfacing with another language, we need to specify how they are to communicate. Without getting too deep into the details, Rust can use the C language's ABI to easily communicate with other languages that support it, and it will greatly simplify our wasm binary to do so. So, we will need to tell `cargo` to use it. Add this in `wasm/Cargo.toml` as well: 91 | 92 | ```toml 93 | [lib] 94 | crate-type = ["cdylib"] 95 | ``` 96 | 97 | Excellent. Now to `wasm/src/lib.rs`. Let's create a struct that will house our `Emu` object as well as all the frontend functions we need to interface with JavaScript and operate. We'll also need to include all of our public items from `chip8_core` as well. 98 | 99 | ```rust 100 | use chip8_core::*; 101 | use wasm_bindgen::prelude::*; 102 | 103 | #[wasm_bindgen] 104 | pub struct EmuWasm { 105 | chip8: Emu, 106 | } 107 | ``` 108 | 109 | Note the `#[wasm_bindgen]` tag, which tells the compiler that this struct needs to be configured for WebAssembly. Any function or struct that is going to be called from within JavaScript will need to have it. Let's also define the constructor. 110 | 111 | ```rust 112 | use chip8_core::*; 113 | use wasm_bindgen::prelude::*; 114 | 115 | #[wasm_bindgen] 116 | pub struct EmuWasm { 117 | chip8: Emu, 118 | } 119 | 120 | #[wasm_bindgen] 121 | impl EmuWasm { 122 | #[wasm_bindgen(constructor)] 123 | pub fn new() -> EmuWasm { 124 | EmuWasm { 125 | chip8: Emu::new(), 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | Pretty straight-forward. The biggest thing to note is that the `new` method requires the special `constructor` inclusion so the compiler knows what we're trying to do. 132 | 133 | Now we have a struct containing our `chip8` emulation core object. Here, we will implement the same methods that we needed by our `desktop` frontend, such as passing key presses/releases to the core, loading in a file, and ticking. Let's begin with ticking the CPU and the timers, as it's the easiest. 134 | 135 | ```rust 136 | #[wasm_bindgen] 137 | impl EmuWasm { 138 | // -- Unchanged code omitted -- 139 | 140 | #[wasm_bindgen] 141 | pub fn tick(&mut self) { 142 | self.chip8.tick(); 143 | } 144 | 145 | #[wasm_bindgen] 146 | pub fn tick_timers(&mut self) { 147 | self.chip8.tick_timers(); 148 | } 149 | } 150 | ``` 151 | 152 | That's it, these are just thin wrappers to call the corresponding functions in the `chip8_core`. These functions don't take any input, so there's nothing fancy for them to do except, well, tick. 153 | 154 | Remember the `reset` function we created in the `chip8_core`, but then never used? Well, we'll get to use it now. This will be a wrapper just like the previous two functions. 155 | 156 | ```rust 157 | #[wasm_bindgen] 158 | pub fn reset(&mut self) { 159 | self.chip8.reset(); 160 | } 161 | ``` 162 | 163 | Key pressing is the first of these functions that will deviate from what was done in `desktop`. This works in a similar fashion to what we did for `desktop`, but rather than taking in an SDL key press, we'll need to accept one from JavaScript. I promised we'd add some `web-sys` features, so lets do so now. Back in `wasm/Cargo.toml` add the `KeyboardEvent` feature 164 | 165 | ```toml 166 | [dependencies.web-sys] 167 | version = "^0.3.46" 168 | features = [ 169 | "KeyboardEvent" 170 | ] 171 | ``` 172 | 173 | ```rust 174 | use web_sys::KeyboardEvent; 175 | 176 | // -- Unchanged code omitted -- 177 | 178 | impl EmuWasm { 179 | // -- Unchanged code omitted -- 180 | 181 | #[wasm_bindgen] 182 | pub fn keypress(&mut self, evt: KeyboardEvent, pressed: bool) { 183 | let key = evt.key(); 184 | if let Some(k) = key2btn(&key) { 185 | self.chip8.keypress(k, pressed); 186 | } 187 | } 188 | } 189 | 190 | fn key2btn(key: &str) -> Option { 191 | match key { 192 | "1" => Some(0x1), 193 | "2" => Some(0x2), 194 | "3" => Some(0x3), 195 | "4" => Some(0xC), 196 | "q" => Some(0x4), 197 | "w" => Some(0x5), 198 | "e" => Some(0x6), 199 | "r" => Some(0xD), 200 | "a" => Some(0x7), 201 | "s" => Some(0x8), 202 | "d" => Some(0x9), 203 | "f" => Some(0xE), 204 | "z" => Some(0xA), 205 | "x" => Some(0x0), 206 | "c" => Some(0xB), 207 | "v" => Some(0xF), 208 | _ => None, 209 | } 210 | } 211 | ``` 212 | 213 | This is very similar to our implementation for our `desktop`, except we are going to take in a JavaScript `KeyboardEvent`, which will result in a string for us to parse. Note that the key strings are case sensitive, so keep everything lowercase unless you want your players to hold down shift a lot. 214 | 215 | A similar story awaits us when we load a game, it will follow a similar style, except we will need to receive and handle a JavaScript object. 216 | 217 | ```rust 218 | use js_sys::Uint8Array; 219 | // -- Unchanged code omitted -- 220 | 221 | impl EmuWasm { 222 | // -- Unchanged code omitted -- 223 | 224 | #[wasm_bindgen] 225 | pub fn load_game(&mut self, data: Uint8Array) { 226 | self.chip8.load(&data.to_vec()); 227 | } 228 | } 229 | ``` 230 | 231 | The only thing remaining is our function to actually render to a screen. I'm going to create an empty function here, but we'll hold off on implementing it for now, instead we'll turn our attention back to our web page, and begin working from the other direction. 232 | 233 | ```rust 234 | impl EmuWasm { 235 | // -- Unchanged code omitted -- 236 | 237 | #[wasm_bindgen] 238 | pub fn draw_screen(&mut self, scale: usize) { 239 | // TODO 240 | } 241 | } 242 | ``` 243 | 244 | We'll come back here once we get our JavaScript setup and we know exactly how we're going to draw. 245 | 246 | ## Creating our Frontend Functionality 247 | 248 | Time to get our hands dirty in JavaScript. First, let's add some additional elements to our very bland web page. When we created the emulator to run on a PC, we used SDL to create a window to draw upon. For a web page, we will use an element HTML5 gives us called a *canvas*. We'll also go ahead and point our web page to our (currently non-existent) JS script. 249 | 250 | ```html 251 | 252 | 253 | 254 | Chip-8 Emulator 255 | 256 | 257 | 258 |

My Chip-8 Emulator

259 | 260 | 261 |
262 | If you see this message, then your browser doesn't support HTML5 263 | 264 | 265 | 266 | ``` 267 | 268 | We added three things here, first a button which when clicked will allow the users to select a Chip-8 game to run. Secondly, the `canvas` element, which includes a brief message for any unfortunate users with an out of date browser. Finally we told our web page to also load the `index.js` script we are about to create. Note that at the time of writing, in order to load a .wasm file via JavaScript, you need to specify that it is of `module` type. 269 | 270 | Now, let's create `index.js` and we'll define some items we'll need. First, we need to tell JavaScript to load in our WebAssembly functions. Now, we aren't going to load it in directly here. When we compile with `wasm-pack`, it will generate not only our .wasm file, but also an auto-generated JavaScript "glue" that will wrap each function we defined around a JavaScript function we then can use here. 271 | 272 | ```js 273 | import init, * as wasm from "./wasm.js" 274 | ``` 275 | 276 | This imports all of our functions, as well as a special `init` function that will need to be called first before we can use anything from `wasm`. 277 | 278 | Let's define some constants and do some basic setup now. 279 | 280 | ```js 281 | import init, * as wasm from "./wasm.js" 282 | 283 | const WIDTH = 64 284 | const HEIGHT = 32 285 | const SCALE = 15 286 | const TICKS_PER_FRAME = 10 287 | let anim_frame = 0 288 | 289 | const canvas = document.getElementById("canvas") 290 | canvas.width = WIDTH * SCALE 291 | canvas.height = HEIGHT * SCALE 292 | 293 | const ctx = canvas.getContext("2d") 294 | ctx.fillStyle = "black" 295 | ctx.fillRect(0, 0, WIDTH * SCALE, HEIGHT * SCALE) 296 | 297 | const input = document.getElementById("fileinput") 298 | ``` 299 | 300 | All of this will look familiar from our `desktop` build. We fetch the HTML canvas and adjust its size to the dimension of our Chip-8 screen, plus scaled up a bit (feel free to adjust this for your preferences). 301 | 302 | Let's create a main `run` function that will load our `EmuWasm` object and handle the main emulation. 303 | 304 | ```js 305 | async function run() { 306 | await init() 307 | let chip8 = new wasm.EmuWasm() 308 | 309 | document.addEventListener("keydown", function(evt) { 310 | chip8.keypress(evt, true) 311 | }) 312 | 313 | document.addEventListener("keyup", function(evt) { 314 | chip8.keypress(evt, false) 315 | }) 316 | 317 | input.addEventListener("change", function(evt) { 318 | // Handle file loading 319 | }, false) 320 | } 321 | 322 | run().catch(console.error) 323 | ``` 324 | 325 | Here, we called the mandatory `init` function which tells our browser to initialize our WebAssembly binary before we use it. We then create our emulator backend by making a new `EmuWasm` object. 326 | 327 | We will now handle loading in a file when our button is pressed. 328 | 329 | ```js 330 | input.addEventListener("change", function(evt) { 331 | // Stop previous game from rendering, if one exists 332 | if (anim_frame != 0) { 333 | window.cancelAnimationFrame(anim_frame) 334 | } 335 | 336 | let file = evt.target.files[0] 337 | if (!file) { 338 | alert("Failed to read file") 339 | return 340 | } 341 | 342 | // Load in game as Uint8Array, send to .wasm, start main loop 343 | let fr = new FileReader() 344 | fr.onload = function(e) { 345 | let buffer = fr.result 346 | const rom = new Uint8Array(buffer) 347 | chip8.reset() 348 | chip8.load_game(rom) 349 | mainloop(chip8) 350 | } 351 | fr.readAsArrayBuffer(file) 352 | }, false) 353 | 354 | function mainloop(chip8) { 355 | } 356 | ``` 357 | 358 | This function adds an event listener to our `input` button which is triggered whenever it is clicked. Our `desktop` frontend used SDL to manage not only drawing to a window, but only to ensure that we were running at 60 FPS. The analogous feature for canvases is the "Animation Frames". Anytime we want to render something to the canvas, we request the window to animate a frame, and it will wait until the correct time has elapsed to ensure 60 FPS performance. We'll see how this works in a moment, but for now, we need to tell our program that if we're loading a new game, we need to stop the previous animation. We'll also reset our emulator before we load in the ROM, to ensure everything is just as it started, without having to reload the webpage. 359 | 360 | Following that, we look at the file that the user has pointed us to. We don't need to check if it's actually a Chip-8 program, but we do need to make sure that it is a file of some sort. We then read it in and pass it to our backend via our `EmuWasm` object. Once the game is loaded, we can jump into our main emulation loop! 361 | 362 | ```js 363 | function mainloop(chip8) { 364 | // Only draw every few ticks 365 | for (let i = 0; i < TICKS_PER_FRAME; i++) { 366 | chip8.tick() 367 | } 368 | chip8.tick_timers() 369 | 370 | // Clear the canvas before drawing 371 | ctx.fillStyle = "black" 372 | ctx.fillRect(0, 0, WIDTH * SCALE, HEIGHT * SCALE) 373 | // Set the draw color back to white before we render our frame 374 | ctx.fillStyle = "white" 375 | chip8.draw_screen(SCALE) 376 | 377 | anim_frame = window.requestAnimationFrame(() => { 378 | mainloop(chip8) 379 | }) 380 | } 381 | ``` 382 | 383 | This should look very similar to what we did for our `desktop` frontend. We tick several times before clearing the canvas and telling our `EmuWasm` object to draw the current frame to our canvas. Here is where we tell our window that we would like to render a frame, and we also save its ID for if we need to cancel it above. The `requestAnimationFrame` will wait to ensure 60 FPS performance, and then restart our `mainloop` when it is time, beginning the process all over again. 384 | 385 | ## Compiling our WebAssembly binary 386 | 387 | Before we go any further, let's now try and build our Rust code and ensure that it can be loaded by our web page without issue. `wasm-pack` will handle the compilation of our .wasm binary, but we also need to specify that we don't wish to use any web packing systems like `npm`. To build, change directories into the `wasm` folder and run: 388 | 389 | ``` 390 | $ wasm-pack build --target web 391 | ``` 392 | 393 | Once it is completed, the targets will be built into a new `pkg` directory. There are several items in here, but the only ones we need are `wasm_bg.wasm` and `wasm.js`. `wasm_bg.wasm` is the combination of our `wasm` and `chip8_core` Rust crates compiled into one, and `wasm.js` is the JavaScript "glue" that we included earlier. It is mainly wrappers around the API we defined in `wasm` as well as some initialization code. It is actually quite readable, so it's worth taking a look at what it is doing. 394 | 395 | Running the page in a local web server should allow you to pick and load a game without any warnings coming up in the browser's console. However, we haven't written the screen rendering function yet, so let's finish that so we can see our game actually run. 396 | 397 | ## Drawing to the canvas 398 | 399 | Here is the final step, rendering to the screen. We created an empty `draw_screen` function in our `EmuWasm` object, and we call it at the right time, but it currently doesn't do anything. Now, there are two ways we could handle this. We could either pass the frame buffer into JavaScript and render it, or we could obtain our canvas in our `EmuWasm` binary and render to it in Rust. Either method would work fine, but personally I found that handling the rendering in Rust is easier. 400 | 401 | We've used the `web_sys` crate to handle JavaScript `KeyboardEvents` in Rust, but it has the functionality to manage many more JavaScript elements. Again, the ones we wish to use will need to be defined as features in `wasm/Cargo.toml`. 402 | 403 | ```toml 404 | [dependencies.web-sys] 405 | version = "^0.3.46" 406 | features = [ 407 | "CanvasRenderingContext2d", 408 | "Document", 409 | "Element", 410 | "HtmlCanvasElement", 411 | "ImageData", 412 | "KeyboardEvent", 413 | "Window" 414 | ] 415 | ``` 416 | 417 | Here is an overview of our next steps. In order to render to an HTML5 canvas, you need to obtain the canvas object and its *context* which is the object which gets the draw functions called upon it. Since our WebAssembly binary has been loaded by our webpage, it has access to all of its elements just as a JS script would. We will change our `new` constructor to grab the current window, canvas, and context much like you would in JavaScript. 418 | 419 | ```rust 420 | use wasm_bindgen::JsCast; 421 | use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, KeyboardEvent}; 422 | 423 | #[wasm_bindgen] 424 | pub struct EmuWasm { 425 | chip8: Emu, 426 | ctx: CanvasRenderingContext2d, 427 | } 428 | 429 | #[wasm_bindgen] 430 | impl EmuWasm { 431 | #[wasm_bindgen(constructor)] 432 | pub fn new() -> Result { 433 | let chip8 = Emu::new(); 434 | 435 | let document = web_sys::window().unwrap().document().unwrap(); 436 | let canvas = document.get_element_by_id("canvas").unwrap(); 437 | let canvas: HtmlCanvasElement = canvas 438 | .dyn_into::() 439 | .map_err(|_| ()) 440 | .unwrap(); 441 | 442 | let ctx = canvas.get_context("2d") 443 | .unwrap().unwrap() 444 | .dyn_into::() 445 | .unwrap(); 446 | 447 | Ok(EmuWasm{chip8, ctx}) 448 | } 449 | 450 | // -- Unchanged code omitted -- 451 | } 452 | ``` 453 | 454 | This should look pretty familiar to those who have done JavaScript programming before. We grab our current window's canvas and get its 2D context, which is saved as a member variable of our `EmuWasm` struct. Now that we have an actual context to render to, we can update our `draw_screen` function to draw to it. 455 | 456 | ```rust 457 | #[wasm_bindgen] 458 | pub fn draw_screen(&mut self, scale: usize) { 459 | let disp = self.chip8.get_display(); 460 | for i in 0..(SCREEN_WIDTH * SCREEN_HEIGHT) { 461 | if disp[i] { 462 | let x = i % SCREEN_WIDTH; 463 | let y = i / SCREEN_WIDTH; 464 | self.ctx.fill_rect( 465 | (x * scale) as f64, 466 | (y * scale) as f64, 467 | scale as f64, 468 | scale as f64 469 | ); 470 | } 471 | } 472 | } 473 | ``` 474 | 475 | We get the display buffer from our `chip8_core` and iterate through every pixel. If set, we draw it scaled up to the value passed in by our frontend. Don't forget that we already cleared the canvas to black and set the draw color to white before calling `draw_screen`, so it doesn't need to be done here. 476 | 477 | That does it! The implementation is done. All that remains is to build it and try it for ourself. 478 | 479 | Rebuild by moving into the `wasm` directory and running: 480 | 481 | ``` 482 | $ wasm-pack build --target web 483 | $ mv pkg/wasm_bg.wasm ../web 484 | $ mv pkg/wasm.js ../web 485 | ``` 486 | 487 | Now start your web server and pick a game. If everything has gone well, you should be able to play Chip-8 games just as well in the browser as on the desktop! 488 | 489 | \newpage 490 | -------------------------------------------------------------------------------- /src/6-frontend.md: -------------------------------------------------------------------------------- 1 | # Writing our Desktop Frontend {#dfe} 2 | 3 | In this section, we will finally connect all the pieces and get our emulator to load and run a game. At this stage, our emulator core is capable of parsing and processing the game's opcodes, and being able to update the screen, RAM, and the registers as needed. Next, we will need to add some public functions to expose some functionality to the frontend, such as loading in a game, accepting user input, and sharing the screen buffer to be displayed. 4 | 5 | ## Exposing the Core to the Frontend 6 | 7 | We are not done with our `chip8_core` just yet. We need to add a few public functions to our `Emu` struct to give access to some of its items. 8 | 9 | In `chip8_core/src/lib.rs`, add the following method to our `Emu` struct: 10 | 11 | ```rust 12 | impl Emu { 13 | // -- Unchanged code omitted -- 14 | 15 | pub fn get_display(&self) -> &[bool] { 16 | &self.screen 17 | } 18 | 19 | // -- Unchanged code omitted -- 20 | } 21 | ``` 22 | 23 | This simply passes a pointer to our screen buffer array up to the frontend, where it can be used to render to the display. 24 | 25 | Next, we will need to handle key presses. We already have a `keys` array, but it never actually gets written to. Our frontend will handle actually reading keyboard presses, but we'll need to expose a function that allows it to interface and set elements in our `keys` array. 26 | 27 | ```rust 28 | impl Emu { 29 | // -- Unchanged code omitted -- 30 | 31 | pub fn keypress(&mut self, idx: usize, pressed: bool) { 32 | self.keys[idx] = pressed; 33 | } 34 | 35 | // -- Unchanged code omitted -- 36 | } 37 | ``` 38 | 39 | This function is rather straightforward. It takes the index of the key that has been pressed and sets the value. We could have split this function into a `press_key` and `release_key` function, but this is simple enough that I think the intention still comes across. Keep in mind that `idx` needs to be under 16 or else the program will panic. You can add that restriction here, but instead we'll handle it in the frontend and assume that it's been done correctly in the backend, rather than checking it twice. 40 | 41 | Lastly, we need some way to load the game code from a file into our RAM so it can be executed. We'll dive into this more deeply when we begin reading from a file in our frontend, but for now we need to take in a list of bytes and copy them into our RAM. 42 | 43 | ```rust 44 | impl Emu { 45 | // -- Unchanged code omitted -- 46 | 47 | pub fn load(&mut self, data: &[u8]) { 48 | let start = START_ADDR as usize; 49 | let end = (START_ADDR as usize) + data.len(); 50 | self.ram[start..end].copy_from_slice(data); 51 | } 52 | 53 | // -- Unchanged code omitted -- 54 | } 55 | ``` 56 | 57 | This function copies all the values from our input `data` slice into RAM beginning at 0x200. Remember that the first 512 bytes of RAM aren't to contain game data, and are empty except for the character sprite data we store there. 58 | 59 | ## Frontend Setup 60 | 61 | Finally, let's setup the frontend of the emulator so we can test things out and (hopefully) play some games! Making a fancy GUI interface is beyond the scope of this guide, we will simply start the emulator and choose which game to play via a command line argument. Let's set that up now. 62 | 63 | In `desktop/src/main.rs` we will need to read the command line arguments to receive the path to our game ROM file. We could create several flags for additional configuration, but we'll keep it simple and say that we'll require exactly one argument - the path to the game. Any other number and we'll exit out with an error. 64 | 65 | ```rust 66 | use std::env; 67 | 68 | fn main() { 69 | let args: Vec<_> = env::args().collect(); 70 | if args.len() != 2 { 71 | println!("Usage: cargo run path/to/game"); 72 | return; 73 | } 74 | } 75 | ``` 76 | 77 | This grabs all of the passed command line parameters into a vector, and if there isn't two (the name of the program is always stored in `args[0]`), then we print out the correct input and exit. The path passed in by the user is now stored in `args[1]`. We'll have to make sure that's a valid file once we attempt to open it, but first, we have some other stuff to setup. 78 | 79 | ## Creating a Window 80 | 81 | For this emulation project, we are going to use the SDL library to create our game window and render to it. SDL is an excellent drawing library to use with good support for key presses and drawing. There's a small amount of boilerplate we'll need in order to set it up, but once that's done we can begin our emulation. 82 | 83 | First, we'll need to include it into our project. Open `desktop/Cargo.toml` and add `sdl2` to our dependencies: 84 | 85 | ```toml 86 | [dependencies] 87 | chip8_core = { path = "../chip8_core" } 88 | sdl2 = "^0.34.3 89 | ``` 90 | 91 | Now back in `desktop/src/main.rs`, we can begin bringing it all together. We'll need the public functions we defined in our core, so let's tell Rust we'll need those with `use chip8_core::*`. 92 | 93 | ```rust 94 | use chip8_core::*; 95 | use std::env; 96 | 97 | fn main() { 98 | let args: Vec<_> = env::args().collect(); 99 | if args.len() != 2 { 100 | println!("Usage: cargo run path/to/game"); 101 | return; 102 | } 103 | } 104 | ``` 105 | 106 | Inside `chip8_core`, we created public constants to hold the screen size, which we are now importing. However, a 64x32 game window is really small on today's monitors, so let's go ahead and scale it up a bit. After experimenting with some numbers, a 15x scale works well on my monitor, but you can tweak this if you prefer something else. 107 | 108 | ```rust 109 | const SCALE: u32 = 15; 110 | const WINDOW_WIDTH: u32 = (SCREEN_WIDTH as u32) * SCALE; 111 | const WINDOW_HEIGHT: u32 = (SCREEN_HEIGHT as u32) * SCALE; 112 | ``` 113 | 114 | Recall that `SCREEN_WIDTH` and `SCREEN_HEIGHT` were public constants we defined in our backend and are now included into this crate via the `use chip8_core::*` statement. SDL will require screen sizes to be `u32` rather than `usize` so we'll cast them here. 115 | 116 | It's time to create our SDL window! The following code simply creates a new SDL context, then makes the window itself and the canvas that we'll draw upon. 117 | 118 | ```rust 119 | fn main() { 120 | // -- Unchanged code omitted -- 121 | 122 | // Setup SDL 123 | let sdl_context = sdl2::init().unwrap(); 124 | let video_subsystem = sdl_context.video().unwrap(); 125 | let window = video_subsystem 126 | .window("Chip-8 Emulator", WINDOW_WIDTH, WINDOW_HEIGHT) 127 | .position_centered() 128 | .opengl() 129 | .build() 130 | .unwrap(); 131 | 132 | let mut canvas = window.into_canvas().present_vsync().build().unwrap(); 133 | canvas.clear(); 134 | canvas.present(); 135 | } 136 | ``` 137 | 138 | We'll initialize SDL and tell it to create a new window of our scaled up size. We'll also have it be created in the middle of the user's screen. We'll then get the canvas object we'll actually draw to, with VSYNC on. Then go ahead and clear it and show it to the user. 139 | 140 | If you attempt to run it now (give it a dummy file name to test, like `cargo run test`), you'll see a window pop up for a brief moment before closing. This is because the SDL window is created briefly, but then the program ends and the window closes. We'll need to create our main game loop so that our program doesn't end immediately, and while we're at it, let's add some handling to quit the program if we try to exit out of the window (otherwise you'll have to force quit the program from your task manager). 141 | 142 | SDL uses something called an *event pump* to poll for events every loop. By checking this, we can cause different things to happen for given events, such as attempting to close the window or pressing a key. For now, we'll just have the program break out of the main game loop if it needs the window to close. 143 | 144 | We'll need to tell Rust that we wish to use SDL's `Event`: 145 | 146 | ```rust 147 | use sdl2::event::Event; 148 | ``` 149 | 150 | And modify our `main` function to add our basic game loop: 151 | 152 | ```rust 153 | fn main() { 154 | // -- Unchanged code omitted -- 155 | 156 | // Setup SDL 157 | let sdl_context = sdl2::init().unwrap(); 158 | let video_subsystem = sdl_context.video().unwrap(); 159 | let window = video_subsystem 160 | .window("Chip-8 Emulator", WINDOW_WIDTH, WINDOW_HEIGHT) 161 | .position_centered() 162 | .opengl() 163 | .build() 164 | .unwrap(); 165 | 166 | let mut canvas = window.into_canvas().present_vsync().build().unwrap(); 167 | canvas.clear(); 168 | canvas.present(); 169 | 170 | let mut event_pump = sdl_context.event_pump().unwrap(); 171 | 172 | 'gameloop: loop { 173 | for evt in event_pump.poll_iter() { 174 | match evt { 175 | Event::Quit{..} => { 176 | break 'gameloop; 177 | }, 178 | _ => () 179 | } 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | This addition sets up our main game loop, which checks if any events have been triggered. If the `Quit` event is detected (by attempting to close the window), then the program breaks out of the loop, causing it to end. If you try to run it again via `cargo run test`, you'll see a new black window pop-up, with the title of 'Chip-8 Emulator'. The window should successfully close without issue. 186 | 187 | It lives! Next up, we'll initialize our emulator's `chip8_core` backend, and open and load the game file. 188 | 189 | ## Loading a File 190 | 191 | Our frontend can now create a new emulation window, so it's time to start getting the backend up and running as well. Our next step will be to actually read in a game file, and pass its data to the backend to be stored in RAM and executed upon. Firstly, we need to actually have a backend object to pass things to. Still in `frontend/src/main.rs`, let's create our emulation object. Our `Emu` object has already been included with all the other public items from `chip8_core`, so we're free to initialize. 192 | 193 | ```rust 194 | fn main() { 195 | // -- Unchanged code omitted -- 196 | 197 | let mut chip8 = Emu::new(); 198 | 199 | 'gameloop: loop { 200 | // -- Unchanged code omitted -- 201 | } 202 | } 203 | ``` 204 | 205 | Creating the `Emu` object needs to go somewhere prior to our main game loop, as that is where the emulation drawing and key press handing will go. Remember that the path to the game is being passed in by the user, so we'll also need to make sure this file actually exists before we attempt read it in. We'll need to first `use` a few items from the standard library in `main.rs` to open a file. 206 | 207 | ```rust 208 | use std::fs::File; 209 | use std::io::Read; 210 | ``` 211 | 212 | Pretty self-explanatory there. Next, we'll open the file given to us as a command line parameter, read it into a buffer, then pass that data into our emulator backend. 213 | 214 | ```rust 215 | fn main() { 216 | // -- Unchanged code omitted -- 217 | let mut chip8 = Emu::new(); 218 | 219 | let mut rom = File::open(&args[1]).expect("Unable to open file"); 220 | let mut buffer = Vec::new(); 221 | 222 | rom.read_to_end(&mut buffer).unwrap(); 223 | chip8.load(&buffer); 224 | // -- Unchanged code omitted -- 225 | } 226 | ``` 227 | 228 | A few things to note here. In the event that Rust is unable to open the file from the path the user gave us (likely because it doesn't exist), then the `expect` condition will fail and the program will exit with that message. Secondly, we could give the file path to the backend and load the data there, but reading a file is a more frontend-type behavior and it better fits here. More importantly, our eventual plan is to make this emulator work in a web browser with little to no changes to our backend. How a browser reads a file is very different to how your file system will do it, so we will allow the frontends to handle the reading, and pass in the data once we have it. 229 | 230 | ## Running the Emulator and Drawing to the Screen 231 | 232 | The game has been loaded into RAM and our main loop is running. Now we need to tell our backend to begin processing its instructions, and to actually draw to the screen. If you recall, the emulator runs through a clock cycle each time its `tick` function is called, so let's add that to our loop. 233 | 234 | ```rust 235 | fn main() { 236 | // -- Unchanged code omitted -- 237 | 238 | 'gameloop: loop { 239 | for event in event_pump.poll_iter() { 240 | // -- Unchanged code omitted -- 241 | } 242 | 243 | chip8.tick(); 244 | } 245 | } 246 | ``` 247 | 248 | Now every time the loop cycles, the emulator will progress through another instruction. This may seem too easy, but we've set it up so `tick` moves all the pieces of the backend, including modifying our screen buffer. Let's add a function that will grab the screen data from the backend and update our SDL window. First, we need to `use` a few additional elements from SDL: 249 | 250 | ```rust 251 | use sdl2::pixels::Color; 252 | use sdl2::rect::Rect; 253 | use sdl2::render::Canvas; 254 | use sdl2::video::Window; 255 | ``` 256 | 257 | Next, the function, which will take in a reference to our `Emu` object, as well as a mutable reference to our SDL canvas. Drawing the screen requires a few steps. First, we clear the canvas to erase the previous frame. Then, we iterate through the screen buffer, drawing a white rectangle anytime the given value is true. Since Chip-8 only supports black and white; if we clear the screen as black, we only have to worry about drawing the white squares. 258 | 259 | ```rust 260 | fn draw_screen(emu: &Emu, canvas: &mut Canvas) { 261 | // Clear canvas as black 262 | canvas.set_draw_color(Color::RGB(0, 0, 0)); 263 | canvas.clear(); 264 | 265 | let screen_buf = emu.get_display(); 266 | // Now set draw color to white, iterate through each point and see if it should be drawn 267 | canvas.set_draw_color(Color::RGB(255, 255, 255)); 268 | for (i, pixel) in screen_buf.iter().enumerate() { 269 | if *pixel { 270 | // Convert our 1D array's index into a 2D (x,y) position 271 | let x = (i % SCREEN_WIDTH) as u32; 272 | let y = (i / SCREEN_WIDTH) as u32; 273 | 274 | // Draw a rectangle at (x,y), scaled up by our SCALE value 275 | let rect = Rect::new((x * SCALE) as i32, (y * SCALE) as i32, SCALE, SCALE); 276 | canvas.fill_rect(rect).unwrap(); 277 | } 278 | } 279 | canvas.present(); 280 | } 281 | ``` 282 | 283 | To summarize this function, we get our 1D screen buffer array and iterate across it. If we find a white pixel (a true value), then we calculate the 2D (x, y) of the screen and draw a rectangle there, scaled up by our `SCALE` factor. 284 | 285 | We'll call this function in our main loop, just after we `tick`. 286 | 287 | ```rust 288 | fn main() { 289 | // -- Unchanged code omitted -- 290 | 291 | 'gameloop: loop { 292 | for event in event_pump.poll_iter() { 293 | // -- Unchanged code omitted -- 294 | } 295 | 296 | chip8.tick(); 297 | draw_screen(&chip8, &mut canvas); 298 | } 299 | } 300 | ``` 301 | 302 | Some of you might be raising your eyebrows at this. The screen should be updated at 60 frames per second, or 60 Hz. Surely the emulation needs to happen faster than that? It does, but hold that thought for now. We'll start by making sure it works at all before we fix the timings. 303 | 304 | If you have a Chip-8 game downloaded, go ahead and try running your emulator with an actual game via: 305 | 306 | ``` 307 | $ cargo run path/to/game 308 | ``` 309 | 310 | If everything has gone well, you should see the window appear and the game begin to render and play! You should feel accomplished for getting this far with your very own emulator. 311 | 312 | As I mentioned previously, the emulation `tick` speed should probably run faster than the canvas refresh rate. If you watch your game run, it might feel a bit sluggish. Right now, we execute one instruction, then draw to the screen, then repeat. As you're aware, it takes several instructions to be able to do any meaningful changes to the screen. To get around this, we will allow the emulator to tick several times before redrawing. 313 | 314 | Now, this is where things get a bit experimental. The Chip-8 specification says nothing about how quickly the system should actually run. Even leaving it is now so it runs at 60 Hz is a valid solution (and you're welcome to do so). We'll simply allow our `tick` function to loop several times before moving on to drawing the screen. Personally, I (and other emulators I've looked at) find that 10 ticks per frame is a nice sweet spot. 315 | 316 | ```rust 317 | const TICKS_PER_FRAME: usize = 10; 318 | // -- Unchanged code omitted -- 319 | 320 | fn main() { 321 | // -- Unchanged code omitted -- 322 | 323 | 'gameloop: loop { 324 | for event in event_pump.poll_iter() { 325 | // -- Unchanged code omitted -- 326 | } 327 | 328 | for _ in 0..TICKS_PER_FRAME { 329 | chip8.tick(); 330 | } 331 | draw_screen(&chip8, &mut canvas); 332 | } 333 | } 334 | ``` 335 | 336 | Some of you might feel this is a bit hackish (I somewhat agree with you). However, this is also how more 'sophisticated' systems work, with the exception that those CPUs usually have some way of notifying the screen that it's ready to redraw. Since the Chip-8 has no such mechanism nor any defined clock speed, this is a easier way to accomplish this task. 337 | 338 | If you run again, you might notice that it doesn't get very far before pausing. This is likely due to the fact that we never update our two timers, so the emulator has no concept of how long time has passed for its games. I mentioned earlier that the timers run once per frame, rather than at the clock speed, so we can modify the timers at the same point as when we modify the screen. 339 | 340 | ```rust 341 | fn main() { 342 | // -- Unchanged code omitted -- 343 | 344 | 'gameloop: loop { 345 | for event in event_pump.poll_iter() { 346 | // -- Unchanged code omitted -- 347 | } 348 | 349 | for _ in 0..TICKS_PER_FRAME { 350 | chip8.tick(); 351 | } 352 | chip8.tick_timers(); 353 | draw_screen(&chip8, &mut canvas); 354 | } 355 | } 356 | ``` 357 | 358 | There's still a few things left to implement (you can't actually control your game for one) but it's a great start, and we're on the final stretches now! 359 | 360 | ## Adding User Input 361 | 362 | We can finally render our Chip-8 game to the screen, but we can't get very far into playing it as we have no way to control it. Fortunately, SDL supports reading in inputs to the keyboard which we can translate and send to our backend emulation. 363 | 364 | As a refresher, the Chip-8 system supports 16 different keys. These are typically organized in a 4x4 grid, with keys 0-9 organized like a telephone with keys A-F surrounding. While you are welcome to organize the keys in any configuration you like, some game devs assumed they're in the grid pattern when choosing their games inputs, which means it can be awkward to play some games otherwise. For our emulator, we'll use the left-hand keys of the QWERTY keyboard as our inputs, as shown below. 365 | 366 | Let's create a function to convert SDL's key type into the values that we will send to the emulator. We'll need to bring SDL keyboard support into `main.rs` via: 367 | 368 | ```rust 369 | use sdl2::keyboard::Keycode; 370 | ``` 371 | 372 | Now, we'll create a new function that will take in a `Keycode` and output an optional `u8` value. There are only 16 valid keys, so we'll wrap a valid output valid in `Some`, and return `None` if the user presses a non-Chip-8 key. This function is then just pattern matching all of the valid keys as outlined in the image above. 373 | 374 | ```rust 375 | fn key2btn(key: Keycode) -> Option { 376 | match key { 377 | Keycode::Num1 => Some(0x1), 378 | Keycode::Num2 => Some(0x2), 379 | Keycode::Num3 => Some(0x3), 380 | Keycode::Num4 => Some(0xC), 381 | Keycode::Q => Some(0x4), 382 | Keycode::W => Some(0x5), 383 | Keycode::E => Some(0x6), 384 | Keycode::R => Some(0xD), 385 | Keycode::A => Some(0x7), 386 | Keycode::S => Some(0x8), 387 | Keycode::D => Some(0x9), 388 | Keycode::F => Some(0xE), 389 | Keycode::Z => Some(0xA), 390 | Keycode::X => Some(0x0), 391 | Keycode::C => Some(0xB), 392 | Keycode::V => Some(0xF), 393 | _ => None, 394 | } 395 | } 396 | ``` 397 | 398 | ![Keyboard to Chip-8 key layout](img/input_layout.png) 399 | 400 | Next, we'll add two additional events to our main event loop, one for `KeyDown` and the other for `KeyUp`. Each event will check if the pressed key gives a `Some` value from our `key2btn` function, and if so pass it to the emulator via the public `keypress` function we defined earlier. The only difference between the two will be if it sets or clears. 401 | 402 | ```rust 403 | fn main() { 404 | // -- Unchanged code omitted -- 405 | 'gameloop: loop { 406 | for evt in event_pump.poll_iter() { 407 | match evt { 408 | Event::Quit{..} => { 409 | break 'gameloop; 410 | }, 411 | Event::KeyDown{keycode: Some(key), ..} => { 412 | if let Some(k) = key2btn(key) { 413 | chip8.keypress(k, true); 414 | } 415 | }, 416 | Event::KeyUp{keycode: Some(key), ..} => { 417 | if let Some(k) = key2btn(key) { 418 | chip8.keypress(k, false); 419 | } 420 | }, 421 | _ => () 422 | } 423 | 424 | for _ in 0..TICKS_PER_FRAME { 425 | chip8.tick(); 426 | } 427 | chip8.tick_timers(); 428 | draw_screen(&chip8, &mut canvas); 429 | } 430 | // -- Unchanged code omitted -- 431 | } 432 | ``` 433 | 434 | The `if let` statement is only satisfied if the value on the right matches that on the left, namely that `key2btn(key)` returns a `Some` value. The unwrapped value is then stored in `k`. 435 | 436 | Let's also add a common emulator ability - quitting the program by pressing Escape. We'll add that alongside our `Quit` event. 437 | 438 | ```rust 439 | fn main() { 440 | // -- Unchanged code omitted -- 441 | 'gameloop: loop { 442 | for evt in event_pump.poll_iter() { 443 | match evt { 444 | Event::Quit{..} | Event::KeyDown{keycode: Some(Keycode::Escape), ..}=> { 445 | break 'gameloop; 446 | }, 447 | Event::KeyDown{keycode: Some(key), ..} => { 448 | if let Some(k) = key2btn(key) { 449 | chip8.keypress(k, true); 450 | } 451 | }, 452 | Event::KeyUp{keycode: Some(key), ..} => { 453 | if let Some(k) = key2btn(key) { 454 | chip8.keypress(k, false); 455 | } 456 | }, 457 | _ => () 458 | } 459 | } 460 | 461 | for _ in 0..TICKS_PER_FRAME { 462 | chip8.tick(); 463 | } 464 | chip8.tick_timers(); 465 | draw_screen(&chip8, &mut canvas); 466 | } 467 | // -- Unchanged code omitted -- 468 | } 469 | ``` 470 | 471 | Unlike the other key events, where we would check the found `key` variable, we want to use the Escape key to quit. If you don't want this ability in your emulator, or would like some other key press functionality, you're welcome to do so. 472 | 473 | That's it! The desktop frontend of our Chip-8 emulator is now complete. We can specify a game via a command line parameter, load and execute it, display the output to the screen, and handle user input. 474 | 475 | I hope you were able to get an understanding of how emulation works. Chip-8 is a rather basic system, but the techniques discussed here form the basis for how all emulation works. 476 | 477 | However, this guide isn't done! In the next section I will discuss how to build our emulator with WebAssembly and getting it to run in a web browser. 478 | 479 | \newpage 480 | -------------------------------------------------------------------------------- /src/5-instr.md: -------------------------------------------------------------------------------- 1 | # Opcode Execution 2 | 3 | In the previous section, we fetched our opcode and were preparing to decode which instruction it corresponds to to execute that instruction. Currently, our `tick` function looks like this: 4 | 5 | ```rust 6 | pub fn tick(&mut self) { 7 | // Fetch 8 | let op = self.fetch(); 9 | // Decode 10 | // Execute 11 | } 12 | ``` 13 | 14 | This implies that decode and execute will be their own separate functions. While they could be, for Chip-8 it's easier to simply perform the operation as we determine it, rather than involving another function call. Our `tick` function thus becomes this: 15 | 16 | ```rust 17 | pub fn tick(&mut self) { 18 | // Fetch 19 | let op = self.fetch(); 20 | // Decode & execute 21 | self.execute(op); 22 | } 23 | 24 | fn execute(&mut self, op: u16) { 25 | // TODO 26 | } 27 | ``` 28 | 29 | Our next step is to *decode*, or determine exactly which operation we're dealing with. The [Chip-8 opcode cheatsheet](#ot) has all of the available opcodes, how to interpret their parameters, and some notes on what they mean. You will need to reference this often. For a complete emulator, each and every one of them must be implemented. 30 | 31 | ## Pattern Matching 32 | 33 | Fortunately, Rust has a very robust and useful pattern matching feature we can use to our advantage. However, we will need to separate out each hex "digit" before we do so. 34 | 35 | ```rust 36 | fn execute(&mut self, op: u16) { 37 | let digit1 = (op & 0xF000) >> 12; 38 | let digit2 = (op & 0x0F00) >> 8; 39 | let digit3 = (op & 0x00F0) >> 4; 40 | let digit4 = op & 0x000F; 41 | } 42 | ``` 43 | 44 | Perhaps not the cleanest code, but we need each hex digit separately. From here, we can create a `match` statement where we can specify the patterns for all of our opcodes: 45 | 46 | ```rust 47 | fn execute(&mut self, op: u16) { 48 | let digit1 = (op & 0xF000) >> 12; 49 | let digit2 = (op & 0x0F00) >> 8; 50 | let digit3 = (op & 0x00F0) >> 4; 51 | let digit4 = op & 0x000F; 52 | 53 | match (digit1, digit2, digit3, digit4) { 54 | (_, _, _, _) => unimplemented!("Unimplemented opcode: {}", op), 55 | } 56 | } 57 | ``` 58 | 59 | Rust's `match` statement demands that all possible options be taken into account which is done with the `_` variable, which captures "everything else". Inside, we'll use the `unimplemented!` macro to cause the program to panic if it reaches that point. By the time we finish adding all opcodes, the Rust compiler demands that we still have an "everything else" statement, but we should never hit it. 60 | 61 | While a long `match` statement would certainly work for other architectures, it is usually more common to implement instructions in their own functions, and either use a lookup table or programmatically determine which function is correct. Chip-8 is somewhat unusual because it stores instruction parameters into the opcode itself, meaning we need a lot of wild cards to match the instructions. Since there are a relatively small number of them, a `match` statement works well here. 62 | 63 | With the framework setup, let's dive in! 64 | 65 | ## Intro to Implementing Opcodes 66 | 67 | The following pages individually discuss how all of Chip-8's instructions work, and include code of how to implement them. You are welcome to simply follow along and implement instruction by instruction, but before you do that, you may want to look forward to the [next section](#dfe) and begin working on some of the frontend code. Currently we have no way of actually running our emulator, and it may be useful to some to be able to attempt to load and run a game for debugging. However, do remember that the emulator will likely crash rather quickly unless all of the instructions are implemented. Personally, I prefer to work on the instructions first before working on the other moving parts (hence why this guide is laid out the way it is). 68 | 69 | With that disclaimer out of the way, let's proceed to working on each of the Chip-8 instructions in turn. 70 | 71 | ### 0000 - Nop 72 | 73 | Our first instruction is the simplest one - do nothing. This may seem like a silly one to have, but sometimes it's needed for timing or alignment purposes. In any case, we simply need to move on to the next opcode (which was already done in `fetch`), and return. 74 | 75 | ```rust 76 | match (digit1, digit2, digit3, digit4) { 77 | // NOP 78 | (0, 0, 0, 0) => return, 79 | (_, _, _, _) => unimplemented!("Unimplemented opcode: {}", op), 80 | } 81 | ``` 82 | 83 | ### 00E0 - Clear screen 84 | 85 | Opcode 0x00E0 is the instruction to clear the screen, which means we need to reset our screen buffer to be empty again. 86 | 87 | ```rust 88 | match (digit1, digit2, digit3, digit4) { 89 | // -- Unchanged code omitted -- 90 | 91 | // CLS 92 | (0, 0, 0xE, 0) => { 93 | self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT]; 94 | }, 95 | 96 | // -- Unchanged code omitted -- 97 | } 98 | ``` 99 | 100 | ### 00EE - Return from Subroutine 101 | 102 | We haven't yet spoken about subroutines (aka functions) and how they work. Entering into a subroutine works in the same way as a plain jump; we move the PC to the specified address and resume execution from there. Unlike a jump, a subroutine is expected to complete at some point, and we will need to return back to the point where we entered. This is where our stack comes in. When we enter a subroutine, we simply push our address onto the stack, run the routine's code, and when we're ready to return we pop that value off our stack and execute from that point again. A stack also allows us to maintain return addresses for nested subroutines while ensuring they are returned in the correct order. 103 | 104 | ```rust 105 | match (digit1, digit2, digit3, digit4) { 106 | // -- Unchanged code omitted -- 107 | 108 | // RET 109 | (0, 0, 0xE, 0xE) => { 110 | let ret_addr = self.pop(); 111 | self.pc = ret_addr; 112 | }, 113 | 114 | // -- Unchanged code omitted -- 115 | } 116 | ``` 117 | 118 | ### 1NNN - Jump 119 | 120 | The jump instruction is pretty easy to add, simply move the PC to the given address. 121 | 122 | ```rust 123 | match (digit1, digit2, digit3, digit4) { 124 | // -- Unchanged code omitted -- 125 | 126 | // JMP NNN 127 | (1, _, _, _) => { 128 | let nnn = op & 0xFFF; 129 | self.pc = nnn; 130 | }, 131 | 132 | // -- Unchanged code omitted -- 133 | } 134 | ``` 135 | 136 | The main thing to notice here is that this opcode is defined by '0x1' being the most significant digit. The other digits are used as parameters for this operation, hence the `_` placeholder in our match statement, here we want anything starting with a 1, but ending in any three digits to enter this statement. 137 | 138 | ### 2NNN - Call Subroutine 139 | 140 | The opposite of our 'Return from Subroutine' operation, we are going to add our current PC to the stack, and then jump to the given address. If you skipped straight here, I recommend reading the *Return* section for additional context. 141 | 142 | ```rust 143 | match (digit1, digit2, digit3, digit4) { 144 | // -- Unchanged code omitted -- 145 | 146 | // CALL NNN 147 | (2, _, _, _) => { 148 | let nnn = op & 0xFFF; 149 | self.push(self.pc); 150 | self.pc = nnn; 151 | }, 152 | 153 | // -- Unchanged code omitted -- 154 | } 155 | ``` 156 | 157 | ### 3XNN - Skip next if VX == NN 158 | 159 | This opcode is first of a few that follow a similar pattern. For those who are unfamiliar with assembly, being able to skip a line gives similar functionality to an if-else block. We can make a comparison, and if true go to one instruction, and if false go somewhere else. This is also the first opcode which will use one of our *V registers*. In this case, the second digit tells us which register to use, while the last two digits provide the raw value. 160 | 161 | ```rust 162 | match (digit1, digit2, digit3, digit4) { 163 | // -- Unchanged code omitted -- 164 | 165 | // SKIP VX == NN 166 | (3, _, _, _) => { 167 | let x = digit2 as usize; 168 | let nn = (op & 0xFF) as u8; 169 | if self.v_reg[x] == nn { 170 | self.pc += 2; 171 | } 172 | }, 173 | 174 | // -- Unchanged code omitted -- 175 | } 176 | ``` 177 | 178 | The implementation works like this: since we already have the second digit saved to a variable, we will reuse it for our 'X' index, although cast to a `usize`, as Rust requires all array indexing to be done with a `usize` variable. If that value stored in that register equals `nn`, then we skip the next opcode, which is the same as skipping our PC ahead by two bytes. 179 | 180 | ### 4XNN - Skip next if VX != NN 181 | 182 | This opcode is exactly the same as the previous, except we skip if the compared values are not equal. 183 | 184 | ```rust 185 | match (digit1, digit2, digit3, digit4) { 186 | // -- Unchanged code omitted -- 187 | 188 | // SKIP VX != NN 189 | (4, _, _, _) => { 190 | let x = digit2 as usize; 191 | let nn = (op & 0xFF) as u8; 192 | if self.v_reg[x] != nn { 193 | self.pc += 2; 194 | } 195 | }, 196 | 197 | // -- Unchanged code omitted -- 198 | } 199 | ``` 200 | 201 | ### 5XY0 - Skip next if VX == VY 202 | 203 | A similar operation again, however we now use the third digit to index into another *V Register*. You will also notice that the least significant digit is not used in the operation. This opcode requires it to be 0. 204 | 205 | ```rust 206 | match (digit1, digit2, digit3, digit4) { 207 | // -- Unchanged code omitted -- 208 | 209 | // SKIP VX == VY 210 | (5, _, _, 0) => { 211 | let x = digit2 as usize; 212 | let y = digit3 as usize; 213 | if self.v_reg[x] == self.v_reg[y] { 214 | self.pc += 2; 215 | } 216 | }, 217 | 218 | // -- Unchanged code omitted -- 219 | } 220 | ``` 221 | 222 | ### 6XNN - VX = NN 223 | 224 | Set the *V Register* specified by the second digit to the value given. 225 | 226 | ```rust 227 | match (digit1, digit2, digit3, digit4) { 228 | // -- Unchanged code omitted -- 229 | 230 | // VX = NN 231 | (6, _, _, _) => { 232 | let x = digit2 as usize; 233 | let nn = (op & 0xFF) as u8; 234 | self.v_reg[x] = nn; 235 | }, 236 | 237 | // -- Unchanged code omitted -- 238 | } 239 | ``` 240 | 241 | ### 7XNN - VX += NN 242 | 243 | This operation adds the given value to the VX register. In the event of an overflow, Rust will panic, so we need to use a different method than the typical addition operator. Note also that while Chip-8 has a carry flag (more on that later), it is not modified by this operation. 244 | 245 | ```rust 246 | match (digit1, digit2, digit3, digit4) { 247 | // -- Unchanged code omitted -- 248 | 249 | // VX += NN 250 | (7, _, _, _) => { 251 | let x = digit2 as usize; 252 | let nn = (op & 0xFF) as u8; 253 | self.v_reg[x] = self.v_reg[x].wrapping_add(nn); 254 | }, 255 | 256 | // -- Unchanged code omitted -- 257 | } 258 | ``` 259 | 260 | ### 8XY0 - VX = VY 261 | 262 | Like the `VX = NN` operation, but the source value is from the VY register. 263 | 264 | ```rust 265 | match (digit1, digit2, digit3, digit4) { 266 | // -- Unchanged code omitted -- 267 | 268 | // VX = VY 269 | (8, _, _, 0) => { 270 | let x = digit2 as usize; 271 | let y = digit3 as usize; 272 | self.v_reg[x] = self.v_reg[y]; 273 | }, 274 | 275 | // -- Unchanged code omitted -- 276 | } 277 | ``` 278 | 279 | ### 8XY1, 8XY2, 8XY3 - Bitwise operations 280 | 281 | The `8XY1`, `8XY2`, and `8XY3` opcodes are all similar functions, so rather than repeat myself three times over, I'll implement the *OR* operation, and allow the reader to implement the other two. 282 | 283 | ```rust 284 | match (digit1, digit2, digit3, digit4) { 285 | // -- Unchanged code omitted -- 286 | 287 | // VX |= VY 288 | (8, _, _, 1) => { 289 | let x = digit2 as usize; 290 | let y = digit3 as usize; 291 | self.v_reg[x] |= self.v_reg[y]; 292 | }, 293 | 294 | // -- Unchanged code omitted -- 295 | } 296 | ``` 297 | 298 | ### 8XY4 - VX += VY 299 | 300 | This operation has two aspects to make note of. Firstly, this operation has the potential to overflow, which will cause a panic in Rust if not handled correctly. Secondly, this operation is the first to utilize the `VF` flag register. I've touched upon it previously, but while the first 15 *V* registers are general usage, the final 16th (0xF) register doubles as the *flag register*. Flag registers are common in many CPU processors; in the case of Chip-8 it also stores the *carry flag*, basically a special variable that notes if the last application operation resulted in an overflow/underflow. Here, if an overflow were to happen, we need to set the `VF` to be 1, or 0 if not. With these two aspects in mind, we will use Rust's `overflowing_add` attribute, which will return a tuple of both the wrapped sum, as well as a boolean of whether an overflow occurred. 301 | 302 | ```rust 303 | match (digit1, digit2, digit3, digit4) { 304 | // -- Unchanged code omitted -- 305 | 306 | // VX += VY 307 | (8, _, _, 4) => { 308 | let x = digit2 as usize; 309 | let y = digit3 as usize; 310 | 311 | let (new_vx, carry) = self.v_reg[x].overflowing_add(self.v_reg[y]); 312 | let new_vf = if carry { 1 } else { 0 }; 313 | 314 | self.v_reg[x] = new_vx; 315 | self.v_reg[0xF] = new_vf; 316 | }, 317 | 318 | // -- Unchanged code omitted -- 319 | } 320 | ``` 321 | ### 8XY5 - VX -= VY 322 | 323 | This is the same operation as the previous opcode, but with subtraction rather than addition. The key distinction is that the `VF` carry flag works in the opposite fashion. The addition operation would set the flag to 1 if an overflow occurred, here if an underflow occurs, it is set to 0, and vice versa. The `overflowing_sub` method will be of use to us here. 324 | 325 | ```rust 326 | match (digit1, digit2, digit3, digit4) { 327 | // -- Unchanged code omitted -- 328 | 329 | // VX -= VY 330 | (8, _, _, 5) => { 331 | let x = digit2 as usize; 332 | let y = digit3 as usize; 333 | 334 | let (new_vx, borrow) = self.v_reg[x].overflowing_sub(self.v_reg[y]); 335 | let new_vf = if borrow { 0 } else { 1 }; 336 | 337 | self.v_reg[x] = new_vx; 338 | self.v_reg[0xF] = new_vf; 339 | }, 340 | 341 | // -- Unchanged code omitted -- 342 | } 343 | ``` 344 | 345 | ### 8XY6 - VX >>= 1 346 | 347 | This operation performs a single right shift on the value in VX, with the bit that was dropped off being stored into the `VF` register. Unfortunately, there isn't a built-in Rust `u8` operator to catch the dropped bit, so we will have to do it ourself. 348 | 349 | ```rust 350 | match (digit1, digit2, digit3, digit4) { 351 | // -- Unchanged code omitted -- 352 | 353 | // VX >>= 1 354 | (8, _, _, 6) => { 355 | let x = digit2 as usize; 356 | let lsb = self.v_reg[x] & 1; 357 | self.v_reg[x] >>= 1; 358 | self.v_reg[0xF] = lsb; 359 | }, 360 | 361 | // -- Unchanged code omitted -- 362 | } 363 | ``` 364 | 365 | ### 8XY7 - VX = VY - VX 366 | 367 | This operation works the same as the previous VX -= VY operation, but with the operands in the opposite direction. 368 | 369 | ```rust 370 | match (digit1, digit2, digit3, digit4) { 371 | // -- Unchanged code omitted -- 372 | 373 | // VX = VY - VX 374 | (8, _, _, 7) => { 375 | let x = digit2 as usize; 376 | let y = digit3 as usize; 377 | 378 | let (new_vx, borrow) = self.v_reg[y].overflowing_sub(self.v_reg[x]); 379 | let new_vf = if borrow { 0 } else { 1 }; 380 | 381 | self.v_reg[x] = new_vx; 382 | self.v_reg[0xF] = new_vf; 383 | }, 384 | 385 | // -- Unchanged code omitted -- 386 | } 387 | ``` 388 | 389 | ### 8XYE - VX <<= 1 390 | 391 | Similar to the right shift operation, but we store the value that is overflowed in the flag register. 392 | 393 | ```rust 394 | match (digit1, digit2, digit3, digit4) { 395 | // -- Unchanged code omitted -- 396 | 397 | // VX <<= 1 398 | (8, _, _, 0xE) => { 399 | let x = digit2 as usize; 400 | let msb = (self.v_reg[x] >> 7) & 1; 401 | self.v_reg[x] <<= 1; 402 | self.v_reg[0xF] = msb; 403 | }, 404 | 405 | // -- Unchanged code omitted -- 406 | } 407 | ``` 408 | 409 | ### 9XY0 - Skip if VX != VY 410 | 411 | Done with the 0x8000 operations, it's time to go back and add an opcode that was notably missing, skipping the next line if VX != VY. This is the same code as the 5XY0 operation, but with an inequality. 412 | 413 | ```rust 414 | match (digit1, digit2, digit3, digit4) { 415 | // -- Unchanged code omitted -- 416 | 417 | // SKIP VX != VY 418 | (9, _, _, 0) => { 419 | let x = digit2 as usize; 420 | let y = digit3 as usize; 421 | if self.v_reg[x] != self.v_reg[y] { 422 | self.pc += 2; 423 | } 424 | }, 425 | 426 | // -- Unchanged code omitted -- 427 | } 428 | ``` 429 | 430 | ### ANNN - I = NNN 431 | 432 | This is the first instruction to utilize the *I Register*, which will be used in several additional instructions, primarily as an address pointer to RAM. In this case, we are simply setting it to the 0xNNN value encoded in this opcode. 433 | 434 | ```rust 435 | match (digit1, digit2, digit3, digit4) { 436 | // -- Unchanged code omitted -- 437 | 438 | // I = NNN 439 | (0xA, _, _, _) => { 440 | let nnn = op & 0xFFF; 441 | self.i_reg = nnn; 442 | }, 443 | 444 | // -- Unchanged code omitted -- 445 | } 446 | ``` 447 | 448 | ### BNNN - Jump to V0 + NNN 449 | 450 | While previous instructions have used the *V Register* specified within the opcode, this instruction always uses the first *V0* register. This operation moves the PC to the sum of the value stored in *V0* and the raw value 0xNNN supplied in the opcode. 451 | 452 | ```rust 453 | match (digit1, digit2, digit3, digit4) { 454 | // -- Unchanged code omitted -- 455 | 456 | // JMP V0 + NNN 457 | (0xB, _, _, _) => { 458 | let nnn = op & 0xFFF; 459 | self.pc = (self.v_reg[0] as u16) + nnn; 460 | }, 461 | 462 | // -- Unchanged code omitted -- 463 | } 464 | ``` 465 | 466 | ### CXNN - VX = rand() & NN 467 | 468 | Finally, something to shake up the monotony! This opcode is Chip-8's random number generation, with a slight twist, in that the random number is then AND'd with the lower 8-bits of the opcode. While the Rust development team has released a random generation crate, it is not part of its standard library, so we shall have to add it to our project. 469 | 470 | In `chip8_core/Cargo.toml`, add the following line somewhere under `[dependencies]`: 471 | 472 | ```toml 473 | rand = "^0.7.3" 474 | ``` 475 | 476 | Note: If you are planning on following this guide completely to its end, there will be a future change to how we include this library for web browser support. However, at this stage in the project, it is enough to specify it as is. 477 | 478 | Time now to add RNG support and implement this opcode. At the top of `lib.rs`, we will need to import a function from the `rand` crate: 479 | 480 | ```rust 481 | use rand::random; 482 | ``` 483 | 484 | We will then use the `random` function when implementing our opcode: 485 | 486 | ```rust 487 | match (digit1, digit2, digit3, digit4) { 488 | // -- Unchanged code omitted -- 489 | 490 | // VX = rand() & NN 491 | (0xC, _, _, _) => { 492 | let x = digit2 as usize; 493 | let nn = (op & 0xFF) as u8; 494 | let rng: u8 = random(); 495 | self.v_reg[x] = rng & nn; 496 | }, 497 | 498 | // -- Unchanged code omitted -- 499 | } 500 | ``` 501 | 502 | Note that specifying `rng` as a `u8` variable is necessary for the `random()` function to know which type it is supposed to generate. 503 | 504 | ### DXYN - Draw Sprite 505 | 506 | This is probably the single most complicated opcode, so allow me to take a moment to describe how it works in detail. Rather than drawing individual pixels or rectangles to the screen at a time, the Chip-8 display works by drawing *sprites*, images stored in memory that are copied to the screen at a specified (x, y). For this opcode, the second and third digits give us which *V Registers* we are to fetch our (x, y) coordinates from. So far so good. Chip-8's sprites are always 8 pixels wide, but can be a variable number of pixels tall, from 1 to 16. This is specified in the final digit of our opcode. I mentioned earlier that the *I Register* is used frequently to store an address in memory, and this is the case here; our sprites are stored row by row *beginning* at the address stored in *I*. So if we are told to draw a 3px tall sprite, the first row's data is stored at \*I, followed by \*I + 1, then \*I + 2. This explains why all sprites are 8 pixels wide, each row is assigned a byte, which is 8-bits, one for each pixel, black or white. The last detail to note is that if *any* pixel is flipped from white to black or vice versa, the *VF* is set (and cleared otherwise). With these things in mind, let's begin. 507 | 508 | ```rust 509 | match (digit1, digit2, digit3, digit4) { 510 | // -- Unchanged code omitted -- 511 | 512 | // DRAW 513 | (0xD, _, _, _) => { 514 | // Get the (x, y) coords for our sprite 515 | let x_coord = self.v_reg[digit2 as usize] as u16; 516 | let y_coord = self.v_reg[digit3 as usize] as u16; 517 | // The last digit determines how many rows high our sprite is 518 | let num_rows = digit4; 519 | 520 | // Keep track if any pixels were flipped 521 | let mut flipped = false; 522 | // Iterate over each row of our sprite 523 | for y_line in 0..num_rows { 524 | // Determine which memory address our row's data is stored 525 | let addr = self.i_reg + y_line as u16; 526 | let pixels = self.ram[addr as usize]; 527 | // Iterate over each column in our row 528 | for x_line in 0..8 { 529 | // Use a mask to fetch current pixel's bit. Only flip if a 1 530 | if (pixels & (0b1000_0000 >> x_line)) != 0 { 531 | // Sprites should wrap around screen, so apply modulo 532 | let x = (x_coord + x_line) as usize % SCREEN_WIDTH; 533 | let y = (y_coord + y_line) as usize % SCREEN_HEIGHT; 534 | 535 | // Get our pixel's index for our 1D screen array 536 | let idx = x + SCREEN_WIDTH * y; 537 | // Check if we're about to flip the pixel and set 538 | flipped |= self.screen[idx]; 539 | self.screen[idx] ^= true; 540 | } 541 | } 542 | } 543 | 544 | // Populate VF register 545 | if flipped { 546 | self.v_reg[0xF] = 1; 547 | } else { 548 | self.v_reg[0xF] = 0; 549 | } 550 | }, 551 | 552 | // -- Unchanged code omitted -- 553 | } 554 | ``` 555 | 556 | ### EX9E - Skip if Key Pressed 557 | 558 | Time at last to introduce user input. When setting up our emulator object, I mentioned that there are 16 possible keys numbered 0 to 0xF. This instruction checks if the index stored in VX is pressed, and if so, skips the next instruction. 559 | 560 | ```rust 561 | match (digit1, digit2, digit3, digit4) { 562 | // -- Unchanged code omitted -- 563 | 564 | // SKIP KEY PRESS 565 | (0xE, _, 9, 0xE) => { 566 | let x = digit2 as usize; 567 | let vx = self.v_reg[x]; 568 | let key = self.keys[vx as usize]; 569 | if key { 570 | self.pc += 2; 571 | } 572 | }, 573 | 574 | // -- Unchanged code omitted -- 575 | } 576 | ``` 577 | 578 | ### EXA1 - Skip if Key Not Pressed 579 | 580 | Same as the previous instruction, however this time the next instruction is skipped if the key in question is not being pressed. 581 | 582 | ```rust 583 | match (digit1, digit2, digit3, digit4) { 584 | // -- Unchanged code omitted -- 585 | 586 | // SKIP KEY RELEASE 587 | (0xE, _, 0xA, 1) => { 588 | let x = digit2 as usize; 589 | let vx = self.v_reg[x]; 590 | let key = self.keys[vx as usize]; 591 | if !key { 592 | self.pc += 2; 593 | } 594 | }, 595 | 596 | // -- Unchanged code omitted -- 597 | } 598 | ``` 599 | 600 | ### FX07 - VX = DT 601 | 602 | I mentioned the use of the *Delay Timer* when we were setting up the emulation structure. This timer ticks down every frame until reaching zero. However, that operation happens automatically, it would be really useful to be able to actually see what's in the *Delay Timer* for our game's timing purposes. This instruction does just that, and stores the current value into one of the *V Registers* for us to use. 603 | 604 | ```rust 605 | match (digit1, digit2, digit3, digit4) { 606 | // -- Unchanged code omitted -- 607 | 608 | // VX = DT 609 | (0xF, _, 0, 7) => { 610 | let x = digit2 as usize; 611 | self.v_reg[x] = self.dt; 612 | }, 613 | 614 | // -- Unchanged code omitted -- 615 | } 616 | ``` 617 | 618 | ### FX0A - Wait for Key Press 619 | 620 | While we already had instructions to check if keys are either pressed or released, this instruction does something very different. Unlike those, which checked the key state and then moved on, this instruction is *blocking*, meaning the whole game will pause and wait for as long as it needs to until the player presses a key. That means it needs to loop endlessly until something in our `keys` array turns true. Once a key is found, it is stored into VX. If more than one key is currently being pressed, it takes the lowest indexed one. 621 | 622 | ```rust 623 | match (digit1, digit2, digit3, digit4) { 624 | // -- Unchanged code omitted -- 625 | 626 | // WAIT KEY 627 | (0xF, _, 0, 0xA) => { 628 | let x = digit2 as usize; 629 | let mut pressed = false; 630 | for i in 0..self.keys.len() { 631 | if self.keys[i] { 632 | self.v_reg[x] = i as u8; 633 | pressed = true; 634 | break; 635 | } 636 | } 637 | 638 | if !pressed { 639 | // Redo opcode 640 | self.pc -= 2; 641 | } 642 | }, 643 | 644 | // -- Unchanged code omitted -- 645 | } 646 | ``` 647 | 648 | You may be looking at this implementation and asking "why are we resetting the opcode and going through the entire fetch sequence again, rather than simply doing this in a loop?". Simply put, while we want this instruction to block future instructions from running, we do not want to block any new key presses from being registered. By remaining in a loop, we would prevent our key press code from ever running, causing this loop to never end. Perhaps inefficient, but much simpler than some sort of asynchronous checking. 649 | 650 | ### FX15 - DT = VX 651 | 652 | This operation works the other direction from our previous *Delay Timer* instruction. We need someway to reset the *Delay Timer* to a value, and this instruction allows us to copy over a value from a *V Register* of our choosing. 653 | 654 | ```rust 655 | match (digit1, digit2, digit3, digit4) { 656 | // -- Unchanged code omitted -- 657 | 658 | // DT = VX 659 | (0xF, _, 1, 5) => { 660 | let x = digit2 as usize; 661 | self.dt = self.v_reg[x]; 662 | }, 663 | 664 | // -- Unchanged code omitted -- 665 | } 666 | ``` 667 | 668 | ### FX18 - ST = VX 669 | 670 | Almost the exact same instruction as the previous, however this time we are going to store the value from VX into our *Sound Timer*. 671 | 672 | ```rust 673 | match (digit1, digit2, digit3, digit4) { 674 | // -- Unchanged code omitted -- 675 | 676 | // ST = VX 677 | (0xF, _, 1, 8) => { 678 | let x = digit2 as usize; 679 | self.st = self.v_reg[x]; 680 | }, 681 | 682 | // -- Unchanged code omitted -- 683 | } 684 | ``` 685 | 686 | ### FX1E - I += VX 687 | 688 | Instruction ANNN sets I to the encoded 0xNNN value, but sometimes it is useful to be able to simply increment the value. This instruction takes the value stored in VX and adds it to the *I Register*. In the case of an overflow, the register should simply roll over back to 0, which we can accomplish with Rust's `wrapping_add` method. 689 | 690 | ```rust 691 | match (digit1, digit2, digit3, digit4) { 692 | // -- Unchanged code omitted -- 693 | 694 | // I += VX 695 | (0xF, _, 1, 0xE) => { 696 | let x = digit2 as usize; 697 | let vx = self.v_reg[x] as u16; 698 | self.i_reg = self.i_reg.wrapping_add(vx); 699 | }, 700 | 701 | // -- Unchanged code omitted -- 702 | } 703 | ``` 704 | 705 | ### FX29 - Set I to Font Address 706 | 707 | This is another tricky instruction where it may not be clear how to progress at first. If you recall, we stored an array of font data at the very beginning of RAM when initializing the emulator. This instruction wants us to take from VX a number to print on screen (from 0 to 0xF), and store the RAM address of that sprite into the *I Register*. We are actually free to store those sprites anywhere we wanted, so long as we are consistent and point to them correctly here. However, we stored them in a very convenient location, at the beginning of RAM. Let me show you what I mean by printing out some of the sprites and their RAM locations. 708 | 709 | | Character | RAM Address | 710 | | --------- | ----------- | 711 | | 0 | 0 | 712 | | 1 | 5 | 713 | | 2 | 10 | 714 | | 3 | 15 | 715 | | ... | ... | 716 | | E (14) | 70 | 717 | | F (15) | 75 | 718 | 719 | You'll notice that since all of our font sprites take up five bytes each, their RAM address is simply their value times 5. If we happened to store the fonts in a different RAM address, we could still follow this rule, however we'd have to apply an offset to where the block begins. 720 | 721 | ```rust 722 | match (digit1, digit2, digit3, digit4) { 723 | // -- Unchanged code omitted -- 724 | 725 | // I = FONT 726 | (0xF, _, 2, 9) => { 727 | let x = digit2 as usize; 728 | let c = self.v_reg[x] as u16; 729 | self.i_reg = c * 5; 730 | }, 731 | 732 | // -- Unchanged code omitted -- 733 | } 734 | ``` 735 | 736 | ### FX33 - I = BCD of VX 737 | 738 | Most of the instructions for Chip-8 are rather self-explanitory, and can be implemented quite easily just by hearing a vague description. However, there are a few that are quite tricky, such as drawing to a screen and this one, storing the [Binary-Coded Decimal](https://en.wikipedia.org/wiki/Binary-coded_decimal) of a number stored in VX into the *I Register*. I encourage you to read up on BCD if you are unfamiliar with it, but a brief refresher goes like this. In this tutorial, we've been using hexadecimal quite a bit, which works by converting our normal decimal numbers into base-16, which is more easily understood by computers. For example, the decimal number 100 would become 0x64. This is good for computers, but not very accessible to humans, and certainly not to the general audience who are going to play your games. The main purpose of BCD is to convert a hexadecimal number back into a pseudo-decimal number to print out for the user, such as for your points or high scores. So while Chip-8 might store 0x64 internally, fetching its BCD would give us `0x1, 0x0, 0x0`, which we could print to the screen as "100". You'll notice that we've gone from one byte to three in order to store all three digits of our number, which is why we are going to store the BCD into RAM, beginning at the address currently in the *I Register* and moving along. Given that VX stores 8-bit numbers, which range from 0 to 255, we are always going to end up with three bytes, even if some are zero. 739 | 740 | ```rust 741 | match (digit1, digit2, digit3, digit4) { 742 | // -- Unchanged code omitted -- 743 | 744 | // BCD 745 | (0xF, _, 3, 3) => { 746 | let x = digit2 as usize; 747 | let vx = self.v_reg[x] as f32; 748 | 749 | // Fetch the hundreds digit by dividing by 100 and tossing the decimal 750 | let hundreds = (vx / 100.0).floor() as u8; 751 | // Fetch the tens digit by dividing by 10, tossing the ones digit and the decimal 752 | let tens = ((vx / 10.0) % 10.0).floor() as u8; 753 | // Fetch the ones digit by tossing the hundreds and the tens 754 | let ones = (vx % 10.0) as u8; 755 | 756 | self.ram[self.i_reg as usize] = hundreds; 757 | self.ram[(self.i_reg + 1) as usize] = tens; 758 | self.ram[(self.i_reg + 2) as usize] = ones; 759 | }, 760 | 761 | // -- Unchanged code omitted -- 762 | } 763 | ``` 764 | 765 | For this implementation, I converted our VX value first into a `float`, so that I could use division and modulo arithmetic to get each decimal digit. This is not the fastest implementation nor is it probably the shortest. However, it is one of the easiest to understand. I'm sure there are some highly binary-savvy readers who are disgusted that I did it this way, but this solution is not for them. This is for readers who have never seen BCD before, where losing some speed for greater understanding is a better trade-off. However, once you have this implemented, I would encourage everyone to go out and look up more efficient BCD algorithms to add a bit of easy optimization into your code. 766 | 767 | ### FX55 - Store V0 - VX into I 768 | 769 | We're on the home stretch! These final two instructions populate our *V Registers* V0 thru the specified VX (inclusive) with the same range of values from RAM, beginning with the address in the *I Register*. This first one stores the values into RAM, while the next one will load them the opposite way. 770 | 771 | ```rust 772 | match (digit1, digit2, digit3, digit4) { 773 | // -- Unchanged code omitted -- 774 | 775 | // STORE V0 - VX 776 | (0xF, _, 5, 5) => { 777 | let x = digit2 as usize; 778 | let i = self.i_reg as usize; 779 | for idx in 0..=x { 780 | self.ram[i + idx] = self.v_reg[idx]; 781 | } 782 | }, 783 | 784 | // -- Unchanged code omitted -- 785 | } 786 | ``` 787 | 788 | ### FX65 - Load I into V0 - VX 789 | 790 | ```rust 791 | match (digit1, digit2, digit3, digit4) { 792 | // -- Unchanged code omitted -- 793 | 794 | // LOAD V0 - VX 795 | (0xF, _, 6, 5) => { 796 | let x = digit2 as usize; 797 | let i = self.i_reg as usize; 798 | for idx in 0..=x { 799 | self.v_reg[idx] = self.ram[i + idx]; 800 | } 801 | }, 802 | 803 | // -- Unchanged code omitted -- 804 | } 805 | ``` 806 | 807 | ### Final Thoughts 808 | 809 | That's it! With this, we now have a fully implemented Chip-8 CPU. You may have noticed a lot of possible opcode values are never covered, particularly in the 0x0000, 0xE000, and 0xF000 ranges. This is okay. These opcodes are left as undefined by the original design, and thus if any game attempts to use them it will lead to a runtime panic. If you are still curious following the completion of this emulator, there are a number of Chip-8 extensions which do fill in some of these gaps to add additional functionality, but they will not be covered by this guide. 810 | 811 | \newpage 812 | -------------------------------------------------------------------------------- /src/img/font_diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 104 | 110 | 116 | 122 | 128 | 134 | 140 | 146 | 152 | 158 | 164 | 170 | 176 | 182 | 188 | 194 | 200 | 206 | 212 | 218 | 224 | 230 | 236 | 242 | 248 | 254 | 260 | 266 | 272 | 278 | 284 | 290 | 296 | 303 | 307 | 311 | 315 | 319 | 323 | 327 | 331 | 335 | 336 | 343 | 347 | 351 | 355 | 359 | 363 | 367 | 371 | 375 | 376 | 383 | 387 | 391 | 395 | 399 | 403 | 407 | 411 | 415 | 416 | 423 | 427 | 431 | 435 | 439 | 443 | 447 | 451 | 455 | 456 | 463 | 467 | 471 | 475 | 479 | 483 | 487 | 491 | 495 | 496 | 502 | 503 | 504 | --------------------------------------------------------------------------------