rusty_chip8/
lib.rs

1#![deny(clippy::all)]
2#![warn(clippy::pedantic)]
3#![deny(clippy::as_conversions)]
4#![warn(clippy::nursery)]
5#![warn(clippy::cargo)]
6
7use std::{thread::sleep, time::Duration, vec};
8
9use anyhow::{bail, Context, Ok, Result};
10use log::debug;
11
12/// Number of horizontal sprites.
13pub const TERMINAL_WIDTH: usize = 64;
14/// Number of vertical sprites.
15pub const TERMINAL_HEIGHT: usize = 32;
16/// Frame rate per second.
17pub const FPS: u64 = 60;
18const RAM_SIZE: usize = 4096;
19const PROGRAM_START: usize = 512;
20
21// Font settings
22const FONT_ADDR: usize = 0x50;
23const FONT_SIZE: usize = 5;
24const FONTS: [u8; 80] = [
25    0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
26    0x20, 0x60, 0x20, 0x20, 0x70, // 1
27    0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
28    0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
29    0x90, 0x90, 0xF0, 0x10, 0x10, // 4
30    0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
31    0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
32    0xF0, 0x10, 0x20, 0x40, 0x40, // 7
33    0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
34    0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
35    0xF0, 0x90, 0xF0, 0x90, 0x90, // A
36    0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
37    0xF0, 0x80, 0x80, 0x80, 0xF0, // C
38    0xE0, 0x90, 0x90, 0x90, 0xE0, // D
39    0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
40    0xF0, 0x80, 0xF0, 0x80, 0x80, // F
41];
42
43/// Chip8 emulator.
44#[derive(Debug, Default)]
45pub struct Chip8 {
46    clock: u64,
47    pixels: Vec<Vec<bool>>,
48    ram: Vec<u8>,
49    pc: usize,
50    i: usize,
51    stack: Vec<usize>,
52    registers: [u8; 16],
53    delay_timer: u8,
54    sound_timer: u8,
55    beeping: bool,
56    key_pressed: Option<u8>,
57    waiting_for_input: Option<usize>,
58}
59
60/// Represents Chip8 instructions.
61#[derive(Debug, Clone, Copy)]
62enum Instruction {
63    Cls00E0,
64    SetIndexRegisterANNN(usize),
65    SetVRegister6XNN(usize, u8),
66    Dxyn(usize, usize, usize),
67    Add7XNN(usize, u8),
68    Jump1NNN(u16),
69    SubroutineCall2NNN(u16),
70    SubroutineReturn00EE,
71    SkipEqual3XNN(usize, u8),
72    SkipNotEqual4XNN(usize, u8),
73    BinaryCodedDecimalConversionFX33(usize),
74    FontCharacterFX29(usize),
75    SetDelayTimerFX15(usize),
76    ReadDelayTimerFX07(usize),
77    GetKeyFX0A(usize),
78    SetSoundTimerFX18(usize),
79    AddToIndexFX1E(usize),
80    StoreRegistersToMemoryFX55(usize),
81    LoadRegistersFromMemoryFX65(usize),
82    RandomCXNN(usize, u8),
83    SkipIfKeyPressedEX9E(usize),
84    SkipIfKeyNotPressedEXA1(usize),
85    BinaryAnd8XY2(usize, usize),
86    RegisterAdd8XY4(usize, usize),
87    RegisterSet8XY0(usize, usize),
88    RegisterSub8XY5(usize, usize),
89    RegisterSubRev8XY7(usize, usize),
90    ShiftRight8XY6(usize, usize),
91    ShiftLeft8XYE(usize, usize),
92    SkipIfEqual5XY0(usize, usize),
93    SkipIfNotEqual9XY0(usize, usize),
94    Xor8XY3(usize, usize),
95}
96
97impl Instruction {
98    fn new(b1: u8, b2: u8) -> Result<Self> {
99        let i = b1 >> 4;
100        let x = b1 & 0xf;
101        let y = b2 >> 4;
102        let n = usize::from(b2 & 0xf);
103        let nn = b2;
104        let nnn = u16::from_ne_bytes([nn, x]);
105        let x = usize::from(x);
106        let y = usize::from(y);
107        let ins = match (i, x, y, n, nn, nnn) {
108            (0, 0, 0xE, 0, _, _) => Self::Cls00E0,
109            (0xA, _, _, _, _, nnn) => Self::SetIndexRegisterANNN(nnn.into()),
110            (1, _, _, _, _, nnn) => Self::Jump1NNN(nnn),
111            (6, x, _, _, nn, _) => Self::SetVRegister6XNN(x, nn),
112            (0xD, x, y, n, _, _) => Self::Dxyn(x, y, n),
113            (2, _, _, _, _, nnn) => Self::SubroutineCall2NNN(nnn),
114            (0, 0, 0xE, 0xE, _, _) => Self::SubroutineReturn00EE,
115            (3, x, _, _, nn, _) => Self::SkipEqual3XNN(x, nn),
116            (4, x, _, _, nn, _) => Self::SkipNotEqual4XNN(x, nn),
117            (5, x, y, 0, _, _) => Self::SkipIfEqual5XY0(x, y),
118            (9, x, y, 0, _, _) => Self::SkipIfNotEqual9XY0(x, y),
119            (7, x, _, _, nn, _) => Self::Add7XNN(x, nn),
120            (8, x, y, 3, _, _) => Self::Xor8XY3(x, y),
121            (0xF, x, 3, 3, _, _) => Self::BinaryCodedDecimalConversionFX33(x),
122            (0xF, x, 2, 9, _, _) => Self::FontCharacterFX29(x),
123            (0xF, x, 1, 5, _, _) => Self::SetDelayTimerFX15(x),
124            (0xF, x, 0, 7, _, _) => Self::ReadDelayTimerFX07(x),
125            (0xF, x, 0, 0xA, _, _) => Self::GetKeyFX0A(x),
126            (0xF, x, 1, 8, _, _) => Self::SetSoundTimerFX18(x),
127            (0xF, x, 1, 0xE, _, _) => Self::AddToIndexFX1E(x),
128            (0xF, x, 5, 5, _, _) => Self::StoreRegistersToMemoryFX55(x),
129            (0xF, x, 6, 5, _, _) => Self::LoadRegistersFromMemoryFX65(x),
130            (0xC, x, _, _, nn, _) => Self::RandomCXNN(x, nn),
131            (0xE, x, 9, 0xE, _, _) => Self::SkipIfKeyPressedEX9E(x),
132            (0xE, x, 0xA, 1, _, _) => Self::SkipIfKeyNotPressedEXA1(x),
133            (8, x, y, 2, _, _) => Self::BinaryAnd8XY2(x, y),
134            (8, x, y, 4, _, _) => Self::RegisterAdd8XY4(x, y),
135            (8, x, y, 0, _, _) => Self::RegisterSet8XY0(x, y),
136            (8, x, y, 5, _, _) => Self::RegisterSub8XY5(x, y),
137            (8, x, y, 6, _, _) => Self::ShiftRight8XY6(x, y),
138            (8, x, y, 0xE, _, _) => Self::ShiftLeft8XYE(x, y),
139            (8, x, y, 7, _, _) => Self::RegisterSubRev8XY7(x, y),
140            _ => {
141                std::thread::sleep(Duration::from_secs(5));
142                bail!("unimplemented instruction: {} {} {} {}", i, x, y, n)
143            }
144        };
145        Ok(ins)
146    }
147
148    const fn requires_pc_inc(self) -> usize {
149        match self {
150            Self::SubroutineCall2NNN(_) | Self::Jump1NNN(_) => 0,
151            _ => 2,
152        }
153    }
154}
155
156impl Chip8 {
157    /// Returns a Chip8 instance.
158    ///
159    /// # Arguments
160    ///
161    /// * `clock` - refers to the instructions per second. The common value used is `700`.
162    #[must_use]
163    pub fn new(clock: u64) -> Self {
164        let mut ram = vec![0; RAM_SIZE];
165        ram[FONT_ADDR..FONT_ADDR + FONTS.len()].copy_from_slice(&FONTS);
166        Self {
167            clock,
168            pixels: vec![vec![false; TERMINAL_WIDTH]; TERMINAL_HEIGHT],
169            ram,
170            pc: PROGRAM_START,
171            ..Default::default()
172        }
173    }
174
175    /// Fetches, decodes and executes Chip8 instructions from RAM.
176    ///
177    /// This function is supposed to be called [FPS] times per second.
178    ///
179    /// # Panics
180    ///
181    /// Panics when an invalid (or unimplemented) instruction encountered.
182    pub fn tick(&mut self, graphics: &mut impl Graphics, audio: &mut impl Audio) {
183        self.decrease_timers();
184        for _ in 0..self.clock / FPS {
185            sleep(Duration::from_millis(1000 / self.clock));
186            if self.waiting_for_input.is_some() {
187                return;
188            }
189            let inst = self
190                .fetch_and_decode_next_instruction()
191                .expect("instruction failure");
192            self.execute_instruction(inst, graphics)
193                .unwrap_or_else(|_| panic!("failed to execute instruction: {inst:?}"));
194            self.pc += inst.requires_pc_inc();
195            if self.sound_timer > 0 && !self.beeping {
196                audio.start_beep();
197                self.beeping = true;
198            } else if self.sound_timer == 0 && self.beeping {
199                audio.stop_beep();
200                self.beeping = false;
201            }
202        }
203    }
204
205    /// Decreases sound and delay timers.
206    fn decrease_timers(&mut self) {
207        self.delay_timer = self.delay_timer.saturating_sub(1);
208        self.sound_timer = self.sound_timer.saturating_sub(1);
209    }
210
211    fn fetch_and_decode_next_instruction(&mut self) -> Result<Instruction> {
212        let b1 = *self
213            .ram
214            .get(self.pc)
215            .unwrap_or_else(|| panic!("invalid memory address: {}", self.pc));
216        let b2 = *self
217            .ram
218            .get(self.pc + 1)
219            .unwrap_or_else(|| panic!("invalid memory address: {}", self.pc));
220        let inst = Instruction::new(b1, b2).context("failed to decode instruction")?;
221        Ok(inst)
222    }
223
224    /// Executes the Chip8 instruction.
225    #[allow(clippy::too_many_lines)]
226    fn execute_instruction(
227        &mut self,
228        inst: Instruction,
229        graphics: &mut impl Graphics,
230    ) -> Result<()> {
231        debug!(
232            "pc = {}, index = {}, registers = {:?}\n",
233            self.pc, self.i, self.registers
234        );
235        debug!("{:?}", inst);
236        match inst {
237            Instruction::Cls00E0 => {
238                for (y, row) in self.pixels.iter().enumerate() {
239                    for (x, pixel) in row.iter().enumerate() {
240                        if *pixel {
241                            graphics.clear_pixel(x, y);
242                        }
243                    }
244                }
245                self.pixels = vec![vec![false; TERMINAL_WIDTH]; TERMINAL_HEIGHT];
246            }
247            Instruction::SetIndexRegisterANNN(nnn) => self.i = nnn,
248            Instruction::SetVRegister6XNN(x, nn) => self.registers[x] = nn,
249            Instruction::Dxyn(x, y, n) => {
250                let x_org = usize::from(self.registers[x]) % TERMINAL_WIDTH;
251                let mut y = usize::from(self.registers[y]) % TERMINAL_HEIGHT;
252                self.registers[15] = 0;
253                let mut collision = false;
254                let sprites = &self.ram[self.i..self.i + n];
255                for row in sprites {
256                    let mut x = x_org;
257                    for i in (0..8).rev() {
258                        let pixel = (row >> i) & 1;
259                        if pixel == 1 {
260                            let is_pixel_on =
261                                self.is_pixel_on(x, y).context("failed to check pixel")?;
262                            self.pixels[y][x] = !is_pixel_on;
263                            if is_pixel_on {
264                                graphics.clear_pixel(x, y);
265                                collision = true;
266                            } else {
267                                graphics.draw_pixel(x, y);
268                            }
269                        }
270                        x += 1;
271                        if x == TERMINAL_WIDTH {
272                            break;
273                        }
274                    }
275                    y += 1;
276                    if y == TERMINAL_HEIGHT {
277                        break;
278                    }
279                }
280                if collision {
281                    self.registers[15] = 1;
282                }
283            }
284            Instruction::Add7XNN(x, nn) => {
285                let (res, _) = self.registers[x].overflowing_add(nn);
286                self.registers[x] = res;
287            }
288            Instruction::Jump1NNN(nnn) => self.pc = nnn.into(),
289            Instruction::SubroutineCall2NNN(nnn) => {
290                self.stack.push(self.pc);
291                self.pc = usize::from(nnn);
292            }
293            Instruction::SubroutineReturn00EE => {
294                self.pc = self
295                    .stack
296                    .pop()
297                    .context("failed to return from subroutine: stack underflow")?;
298            }
299            Instruction::SkipEqual3XNN(x, nn) => {
300                if self.registers[x] == nn {
301                    self.pc += 2;
302                }
303            }
304            Instruction::SkipNotEqual4XNN(x, nn) => {
305                if self.registers[x] != nn {
306                    self.pc += 2;
307                }
308            }
309            Instruction::BinaryCodedDecimalConversionFX33(x) => {
310                let val = self.registers[x];
311                self.ram[self.i] = val / 100;
312                self.ram[self.i + 1] = (val % 100) / 10;
313                self.ram[self.i + 2] = val % 10;
314            }
315            Instruction::FontCharacterFX29(x) => {
316                self.i = FONT_ADDR + (usize::from(self.registers[x]) * FONT_SIZE);
317            }
318            Instruction::SetDelayTimerFX15(x) => {
319                self.delay_timer = self.registers[x];
320            }
321            Instruction::ReadDelayTimerFX07(x) => self.registers[x] = self.delay_timer,
322            Instruction::SetSoundTimerFX18(x) => self.sound_timer = self.registers[x],
323            Instruction::AddToIndexFX1E(x) => {
324                let (res, overflow) = self.i.overflowing_add(self.registers[x].into());
325                self.i = res;
326                if overflow {
327                    self.registers[15] = 1;
328                }
329            }
330            Instruction::StoreRegistersToMemoryFX55(x) => {
331                self.ram[self.i..=self.i + x].copy_from_slice(&self.registers[0..=x]);
332            }
333            Instruction::LoadRegistersFromMemoryFX65(x) => {
334                let data = &self.ram[self.i..=self.i + x];
335                self.registers[0..=x].copy_from_slice(data);
336            }
337            Instruction::RandomCXNN(x, nn) => {
338                let r: u8 = rand::random();
339                self.registers[x] = r & nn;
340            }
341            Instruction::SkipIfKeyPressedEX9E(x) => {
342                if self.key_pressed == Some(self.registers[x]) {
343                    self.pc += 2;
344                }
345            }
346            Instruction::SkipIfKeyNotPressedEXA1(x) => {
347                if self.key_pressed != Some(self.registers[x]) {
348                    self.pc += 2;
349                }
350            }
351            Instruction::BinaryAnd8XY2(x, y) => {
352                self.registers[x] &= self.registers[y];
353            }
354            Instruction::RegisterAdd8XY4(x, y) => {
355                let (res, carry) = self.registers[x].overflowing_add(self.registers[y]);
356                self.registers[x] = res;
357                self.registers[15] = u8::from(carry);
358            }
359            Instruction::RegisterSet8XY0(x, y) => {
360                self.registers[x] = self.registers[y];
361            }
362            Instruction::RegisterSub8XY5(x, y) => {
363                let (res, carry) = self.registers[x].overflowing_sub(self.registers[y]);
364                self.registers[x] = res;
365                self.registers[15] = u8::from(!carry);
366            }
367            Instruction::RegisterSubRev8XY7(x, y) => {
368                let (res, carry) = self.registers[y].overflowing_sub(self.registers[x]);
369                self.registers[x] = res;
370                self.registers[15] = u8::from(!carry);
371            }
372            Instruction::GetKeyFX0A(x) => self.waiting_for_input = Some(x),
373            Instruction::ShiftRight8XY6(x, _) => {
374                self.registers[15] = self.registers[x] & 1u8;
375                self.registers[x] >>= 1;
376            }
377            Instruction::ShiftLeft8XYE(x, _) => {
378                self.registers[15] = self.registers[x] & (1u8 << 7);
379                self.registers[x] <<= 1;
380            }
381            Instruction::SkipIfEqual5XY0(x, y) => {
382                if self.registers[x] == self.registers[y] {
383                    self.pc += 2;
384                }
385            }
386            Instruction::SkipIfNotEqual9XY0(x, y) => {
387                if self.registers[x] != self.registers[y] {
388                    self.pc += 2;
389                }
390            }
391            Instruction::Xor8XY3(x, y) => {
392                self.registers[x] ^= self.registers[y];
393            }
394        }
395        Ok(())
396    }
397
398    /// Returns true if the pixel at the coordinates is on, otherwise false.
399    ///
400    /// If the coordinates is out of the screen area it returns an Error.
401    fn is_pixel_on(&self, x: usize, y: usize) -> Result<bool> {
402        check_coordinates(x, y)?;
403        Ok(self.pixels[y][x])
404    }
405
406    /// Stores data in RAM.
407    ///
408    /// # Errors
409    ///
410    /// If the data is bigger than the available space it returns Error.
411    pub fn store_in_ram(&mut self, rom: impl AsRef<[u8]>) -> Result<()> {
412        let rom = &rom.as_ref();
413        if rom.len() + PROGRAM_START > RAM_SIZE {
414            bail!("data is too big to fit into the ram");
415        }
416        self.ram[PROGRAM_START..PROGRAM_START + rom.len()].copy_from_slice(rom);
417        Ok(())
418    }
419
420    /// Handles released key.
421    ///
422    /// The real key press/release logic is supposed to be handled by the client.
423    pub fn handle_key_released(&mut self) {
424        self.key_pressed = None;
425    }
426
427    /// Handles pressed key.
428    ///
429    /// The real key press/release logic is supposed to be handled by the client.
430    ///
431    /// # Arguments
432    ///
433    /// * `key` - The key is supposed to be a value in the range `0..16`.
434    ///     Chip8's original keypad has 16 buttons.
435    pub fn handle_key_pressed(&mut self, key: u8) {
436        self.key_pressed = Some(key);
437        if let Some(x) = self.waiting_for_input {
438            self.registers[x] = key;
439            self.waiting_for_input = None;
440        }
441    }
442}
443
444/// Checks if the coordinates are valid.
445fn check_coordinates(x: usize, y: usize) -> Result<()> {
446    if x >= TERMINAL_WIDTH {
447        bail!("invalid X coordinate to draw: {}", x);
448    }
449    if y >= TERMINAL_HEIGHT {
450        bail!("invalid Y coordinate to draw: {}", y);
451    }
452    Ok(())
453}
454
455/// Graphics abstraction for Chip8.
456///
457/// Clients are supposed to implement this trait in accordance with
458/// the graphics library used.
459pub trait Graphics {
460    /// Clears/turns off a pixel on a specific coordinate.
461    fn clear_pixel(&mut self, x: usize, y: usize);
462
463    /// Draws/turns on a pixel on a specific coordinate.
464    fn draw_pixel(&mut self, x: usize, y: usize);
465}
466
467/// Audio abstraction for Chip8.
468///
469/// Clients are supposed to implement this trait in accordance with
470/// the sound library used.
471pub trait Audio {
472    /// Starts the beep sound.
473    fn start_beep(&mut self);
474
475    /// Stops the beep sound.
476    fn stop_beep(&mut self);
477}