Skip to main content

oxid8_core/
lib.rs

1//! # Oxid-8 Core
2//!
3//! `oxid8_core` is an interpreter core for the Chip-8 programming language,
4//! developed by Joseph Weisbecker in the mid-1970s for making games on the
5//! COSMAC VIP and Telmac 1800.
6//!
7//! This is the core interpreter library for `Oxid8`. So that developers can
8//! create their own renderers on top of this library crate.
9//!
10//! # Getting Started
11//!
12//! ```no_run
13//! use oxid8_core::Oxid8;
14//! use std::time::{Duration, Instant};
15//!
16//! #[derive(Default)]
17//! struct State {
18//!     should_exit: bool,
19//!     last_frame: Option<Instant>,
20//! }
21//!
22//! #[derive(Default)]
23//! struct Emu {
24//!     state: State,
25//!     core: Oxid8,
26//! }
27//!
28//! fn main() -> std::io::Result<()> {
29//!     let mut emu = Emu::default();
30//!     emu.core.load_font();
31//!     emu.core.load_rom("rom_path")?;
32//!
33//!     while !emu.state.should_exit {
34//!         let time = Instant::now();
35//!
36//!         // TODO: Poll and Handle Events.
37//!
38//!         if let Some(last_frame) = emu.state.last_frame {
39//!             if time.duration_since(last_frame) >= Duration::from_millis(16) {
40//!                 if let Err(err) = emu.core.next_frame() {
41//!                     panic!("{err}");
42//!                 }
43//!
44//!                 // TODO: Draw current frame.
45//!
46//!                 emu.state.last_frame = Some(time);
47//!             }
48//!
49//!             if emu.core.sound() {
50//!                 // TODO: Beep!
51//!             }
52//!
53//!         } else {
54//!             emu.state.last_frame = Some(Instant::now());
55//!         }
56//!     }
57//!
58//!     Ok(())
59//! }
60//! ```
61//!
62//! # WASM Compatibility
63//!
64//! ```toml
65//! # Cargo.toml
66//!
67//! [dependencies]
68//! web-time = "1.1.0"
69//!
70//! [target.'cfg(target_arch = "wasm32")'.dependencies]
71//! getrandom = { version = "0.3", features = ["wasm_js"] }
72//! ```
73//!
74//! ```toml
75//! # config.toml
76//!
77//! [target.'cfg(target_arch = "wasm32")']
78//! rustflags = ["--cfg", 'getrandom_backend="wasm_js"']
79//! ```
80//!
81//! # Frame Time
82//!
83//! You should generate frames at 60Hz or roughly 16ms if not relying on
84//! vsync. `std::time::{Instant, Duration}` panic in the web so use the
85//! [web-time](https://crates.io/crates/web-time) crate when compiling to
86//! web assembly.
87
88use rand::{Rng, rng, rngs::ThreadRng};
89use std::{fmt, io, time::Duration};
90
91/// Standard CPU tick rate set to 700Hz. This value is not used internally.
92/// Run a CPU cycle this often.
93pub const CPU_TICK: Duration = Duration::from_micros(1430);
94
95/// Standard TIMER tick rate set to 60Hz. This value is not used internally.
96/// Decrement the timers and refresh the display this often.
97pub const TIMER_TICK: Duration = Duration::from_micros(16667);
98
99/// Virtual screen width (64 pixels).
100pub const SCREEN_WIDTH: usize = 64;
101
102/// Virtual screen height (32 pixels).
103pub const SCREEN_HEIGHT: usize = 32;
104
105/// Virtual screen area (2048 pixels).
106pub const SCREEN_AREA: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
107
108// Source for font and constants:
109// https://aquova.net/emudev/chip8/
110const FONTSET_SIZE: usize = 80;
111const FONT_ADDR: u16 = 0x050;
112
113// Some games may behave differently based on the font.
114// This font set is common.
115const FONTSET: [u8; FONTSET_SIZE] = [
116    0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
117    0x20, 0x60, 0x20, 0x20, 0x70, // 1
118    0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
119    0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
120    0x90, 0x90, 0xF0, 0x10, 0x10, // 4
121    0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
122    0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
123    0xF0, 0x10, 0x20, 0x40, 0x40, // 7
124    0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
125    0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
126    0xF0, 0x90, 0xF0, 0x90, 0x90, // A
127    0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
128    0xF0, 0x80, 0x80, 0x80, 0xF0, // C
129    0xE0, 0x90, 0x90, 0x90, 0xE0, // D
130    0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
131    0xF0, 0x80, 0xF0, 0x80, 0x80, // F
132];
133
134const RAM_SIZE: usize = 4096;
135const NUM_REGS: usize = 16;
136const STACK_SIZE: usize = 16;
137const NUM_KEYS: usize = 16;
138const VF: usize = 15;
139const START_ADDR: u16 = 0x200;
140
141#[derive(Debug)]
142struct Opcode(u8, u8, u8, u8);
143
144// struct Oxid8 fields based on:
145// https://aquova.net/emudev/chip8/
146
147/// Oxid8 Core
148#[derive(Debug)]
149pub struct Oxid8 {
150    pc: u16,                     // Program Counter
151    ram: [u8; RAM_SIZE],         // RAM
152    screen: [bool; SCREEN_AREA], // Monochrome Display
153    v_reg: [u8; NUM_REGS],       // 8-bit V Registers
154    i_reg: u16,                  // 16[12]-bit I Register
155    sp: u16,                     // Stack Pointer
156    stack: [u16; STACK_SIZE],    // Stack
157    keys: [bool; NUM_KEYS],      // Keys (0-F)
158    stored_key: Option<usize>,   // Stored key
159    dt: u8,                      // Delay Timer
160    st: u8,                      // Sound Timer
161    rng: ThreadRng,              // RNG
162}
163
164/// 4-byte opcode.
165impl Opcode {
166    /// New Opcode.
167    fn new(byte1: u8, byte2: u8) -> Self {
168        Self(
169            (byte1 & 0xF0) >> 4,
170            byte1 & 0x0F,
171            (byte2 & 0xF0) >> 4,
172            byte2 & 0x0F,
173        )
174    }
175
176    /// A 16-bit value, the whole instruction.
177    fn full(&self) -> u16 {
178        (self.0 as u16) << 12 | (self.1 as u16) << 8 | (self.2 as u16) << 4 | (self.3 as u16)
179    }
180
181    /// A 12-bit value, the lowest 12 bits of the instruction.
182    fn nnn(&self) -> u16 {
183        (self.1 as u16) << 8 | (self.2 as u16) << 4 | (self.3 as u16)
184    }
185
186    /// A 4-bit value, the lowest 4 bits of the instruction.
187    fn n(&self) -> u8 {
188        self.3
189    }
190
191    /// A 4-bit value, the lower 4 bits of the high byte of the instruction.
192    fn x(&self) -> u8 {
193        self.1
194    }
195
196    /// A 4-bit value, the upper 4 bits of the low byte of the instruction.
197    fn y(&self) -> u8 {
198        self.2
199    }
200
201    /// An 8-bit value, the lowest 8 bits of the instruction.
202    fn kk(&self) -> u8 {
203        self.2 << 4 | self.3
204    }
205}
206
207/// Formatted as "(byte1, byte2, byte3, byte4)"
208impl fmt::Display for Opcode {
209    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
210        write!(f, "({}, {}, {}, {})", self.0, self.1, self.2, self.3)
211    }
212}
213
214/// Oxid8 Core
215impl Oxid8 {
216    /// Create a new oxid8 instance.
217    pub fn new() -> Self {
218        Oxid8::default()
219    }
220
221    /// Reset all parameters to default.
222    /// Must call `load_font` to reload font.
223    pub fn reset(&mut self) {
224        *self = Oxid8::default();
225    }
226
227    /// Emulates a full frame.
228    ///
229    /// Each frame emulates 10 cpu cycles and decrements
230    /// the sound and delay timers. If your frame time is
231    /// 60Hz, cpu cycles run at 600Hz and timers at 60Hz.
232    /// CHIP-8 cpu cycles have historically ran anywhere
233    /// between 500Hz to 700Hz depending on hardware and
234    /// implementation. If you want finer control over
235    /// cpu speeds use call `run_cycle` yourself, and
236    /// call `dec_timers` at a rate of 16ms.
237    ///
238    /// # Errors
239    ///
240    /// Invalid opcodes will cause `frame` to return
241    /// an error string with the full opcode and program
242    /// counter at that point. The rom is bad.
243    ///
244    /// # Panics
245    ///
246    /// `push` and `pop` instructions can panic with a
247    /// Stack Overflow/Underflow error.
248    ///
249    /// Other opcodes may panic if the game attempts to
250    /// perform an invalid action. Otherwise the interpreter
251    /// can be left in an invalid state. The rom is bad.
252    pub fn next_frame(&mut self) -> Result<(), String> {
253        for _ in 0..10 {
254            self.run_cycle()?;
255        }
256        self.dec_timers();
257
258        Ok(())
259    }
260
261    /// Emulates a single cycle.
262    ///
263    /// Use `next_frame` instead if you don't want to
264    /// control cpu speed.
265    ///
266    /// # Errors
267    ///
268    /// Invalid opcodes will cause `run_cycle` to return
269    /// an error string with the full opcode and program
270    /// counter at that point. The rom is bad.
271    ///
272    /// # Panics
273    ///
274    /// `push` and `pop` instructions can panic with a
275    /// Stack Overflow/Underflow error.
276    ///
277    /// Other opcodes may panic if the game attempts to
278    /// perform an invalid action. Otherwise the interpreter
279    /// can be left in an invalid state. The rom is bad.
280    pub fn run_cycle(&mut self) -> Result<(), String> {
281        let opcode = Opcode::new(
282            self.ram[self.pc as usize],     //
283            self.ram[self.pc as usize + 1], //
284        );
285
286        let pc_at_err = self.pc;
287        self.pc += 2;
288
289        let invalid = || -> Result<(), String> {
290            Err(format!(
291                "Invalid Instruction: {:04X} at {}",
292                opcode.full(),
293                pc_at_err,
294            ))
295        };
296
297        match opcode.0 {
298            0x0 => match opcode.kk() {
299                0xE0 => self.cls(),
300                0xEE => self.ret(),
301                _ => invalid()?,
302            },
303            0x1 => self.jp_nnn(opcode.nnn()),
304            0x2 => self.call(opcode.nnn()),
305            0x3 => self.se_xkk(opcode.x() as usize, opcode.kk()),
306            0x4 => self.sne_xkk(opcode.x() as usize, opcode.kk()),
307            0x5 => self.se_xy(opcode.x() as usize, opcode.y() as usize),
308            0x6 => self.ld_xkk(opcode.x() as usize, opcode.kk()),
309            0x7 => self.add_xkk(opcode.x() as usize, opcode.kk()),
310            0x8 => match opcode.n() {
311                0x0 => self.ld_xy(opcode.x() as usize, opcode.y() as usize),
312                0x1 => self.or(opcode.x() as usize, opcode.y() as usize),
313                0x2 => self.and(opcode.x() as usize, opcode.y() as usize),
314                0x3 => self.xor(opcode.x() as usize, opcode.y() as usize),
315                0x4 => self.add_xy(opcode.x() as usize, opcode.y() as usize),
316                0x5 => self.sub_xy(opcode.x() as usize, opcode.y() as usize),
317                0x6 => self.shr(opcode.x() as usize, opcode.y() as usize),
318                0x7 => self.subn_xy(opcode.x() as usize, opcode.y() as usize),
319                0xE => self.shl(opcode.x() as usize, opcode.y() as usize),
320                _ => invalid()?,
321            },
322            0x9 => self.sne_xy(opcode.x() as usize, opcode.y() as usize),
323            0xA => self.ld_innn(opcode.nnn()),
324            0xB => self.jp_0nnn(opcode.nnn()),
325            0xC => self.rnd(opcode.x() as usize, opcode.kk()),
326            0xD => {
327                self.drw(
328                    opcode.x() as usize, //
329                    opcode.y() as usize, //
330                    opcode.n(),          //
331                );
332            }
333            0xE => match opcode.kk() {
334                0x9E => self.skp(opcode.x() as usize),
335                0xA1 => self.sknp(opcode.x() as usize),
336                _ => invalid()?,
337            },
338            0xF => match opcode.kk() {
339                0x07 => self.ld_xdt(opcode.x() as usize),
340                0x0A => self.ld_xk(opcode.x() as usize),
341                0x15 => self.ld_dtx(opcode.x() as usize),
342                0x18 => self.ld_stx(opcode.x() as usize),
343                0x1E => self.add_ix(opcode.x() as usize),
344                0x29 => self.ld_fx(opcode.x() as usize),
345                0x33 => self.ld_bx(opcode.x() as usize),
346                0x55 => self.ld_ix(opcode.x() as usize),
347                0x65 => self.ld_xi(opcode.x() as usize),
348                _ => invalid()?,
349            },
350            _ => invalid()?,
351        }
352
353        Ok(())
354    }
355
356    /// Decrements the delay and sound and timers.
357    ///
358    /// Use `next_frame` instead if you don't want to
359    /// control cpu speed.
360    pub fn dec_timers(&mut self) {
361        if self.dt > 0 {
362            self.dt -= 1;
363        }
364        if self.st > 0 {
365            self.st -= 1;
366        }
367    }
368
369    /// Returns true if sound timer is zero.
370    #[must_use]
371    pub fn sound(&self) -> bool {
372        self.st != 0
373    }
374
375    /// Sets a key on the virtual keypad.
376    ///
377    /// # Panics
378    ///
379    /// `set_key` panics if key is out of bounds.
380    /// Expects 0x0 - 0xF (0 - 15).
381    pub fn set_key(&mut self, k: usize, val: bool) {
382        self.keys[k] = val;
383    }
384
385    /// Clears the virtual keypad.
386    pub fn clear_keys(&mut self) {
387        self.keys = [false; NUM_KEYS];
388    }
389
390    /// Returns a reference to the screen.
391    #[must_use]
392    pub fn screen_ref(&self) -> &[bool; SCREEN_AREA] {
393        &self.screen
394    }
395
396    /// Instructs the interpreter to load the fontset.
397    pub fn load_font(&mut self) {
398        self.ram[FONT_ADDR as usize..(FONT_ADDR as usize + FONTSET_SIZE)] //
399            .copy_from_slice(&FONTSET);
400    }
401
402    /// Loads a rom given a filename.
403    ///
404    /// # Errors
405    ///
406    /// If there is any issue loading the ROM, then an error is returned.
407    pub fn load_rom(&mut self, path: impl AsRef<std::path::Path>) -> io::Result<()> {
408        use std::fs;
409
410        let rom_data: Vec<u8> = fs::read(path)?;
411        self.load_rom_bytes(rom_data.as_slice())
412    }
413
414    /// Loads a rom from byte array.
415    ///
416    /// # Errors
417    ///
418    /// If there is any issue loading the ROM, then an error is returned.
419    pub fn load_rom_bytes(&mut self, rom_data: &[u8]) -> io::Result<()> {
420        let len = rom_data.len();
421        if len > (RAM_SIZE - START_ADDR as usize) {
422            return Err(io::Error::new(
423                io::ErrorKind::FileTooLarge,
424                format!("ROM too large: {}", len),
425            ));
426        }
427
428        self.ram[START_ADDR as usize..(START_ADDR as usize + len)] //
429            .copy_from_slice(rom_data);
430
431        Ok(())
432    }
433
434    /// Pushes `val` onto the program stack and increments the stack pointer.
435    ///
436    /// # Panics
437    ///
438    /// `push` panics if the stack overflows.
439    fn push(&mut self, val: u16) {
440        match self.sp as usize {
441            0..STACK_SIZE => {
442                self.stack[self.sp as usize] = val;
443                self.sp += 1;
444            }
445            _ => panic!("ERROR::Emulator Stack Overflow"),
446        };
447    }
448
449    /// Pops top value off the program stack and decrements the stack pointer.
450    ///
451    /// # Panics
452    ///
453    /// `pop` panics if the stack underflows.
454    fn pop(&mut self) -> u16 {
455        match self.sp as usize {
456            1..=STACK_SIZE => {
457                self.sp -= 1;
458                self.stack[self.sp as usize]
459            }
460            _ => panic!("ERROR::Emulator Stack Underflow"),
461        }
462    }
463}
464
465impl Default for Oxid8 {
466    fn default() -> Self {
467        Self {
468            pc: START_ADDR,
469            ram: [0; RAM_SIZE],
470            screen: [false; SCREEN_WIDTH * SCREEN_HEIGHT],
471            v_reg: [0; NUM_REGS],
472            i_reg: 0,
473            sp: 0,
474            stack: [0; STACK_SIZE],
475            keys: [false; NUM_KEYS],
476            stored_key: None,
477            dt: 0,
478            st: 0,
479            rng: rng(),
480        }
481    }
482}
483
484// Cowgod's Chip-8 Technical Reference v1.0:
485// http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#0.1
486
487/// Oxid8 CPU Instructions
488///
489/// # Naming Conventions:
490/// - n:      half-byte
491/// - kk:     byte
492/// - nnn:    address
493/// - x,y,i:  register
494/// - dt:     delay timer
495/// - st:     sound timer
496/// - k:      key
497impl Oxid8 {
498    /// 00E0 - Clear the display.
499    fn cls(&mut self) {
500        self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT];
501    }
502
503    /// 00EE - Return from a subroutine.
504    fn ret(&mut self) {
505        self.pc = self.pop();
506    }
507
508    /// 1nnn - Jump to location nnn.
509    fn jp_nnn(&mut self, nnn: u16) {
510        self.pc = nnn;
511    }
512
513    /// 2nnn - Call subroutine at nnn.
514    fn call(&mut self, nnn: u16) {
515        self.push(self.pc);
516        self.pc = nnn;
517    }
518
519    /// 3xkk - Skip next instruction if Vx = kk.
520    fn se_xkk(&mut self, x: usize, kk: u8) {
521        if self.v_reg[x] == kk {
522            self.pc += 2;
523        }
524    }
525
526    /// 4xkk - Skip next instruction if Vx != kk.
527    fn sne_xkk(&mut self, x: usize, kk: u8) {
528        if self.v_reg[x] != kk {
529            self.pc += 2;
530        }
531    }
532
533    /// 5xy0 - Skip next instruction if Vx = Vy.
534    fn se_xy(&mut self, x: usize, y: usize) {
535        if self.v_reg[x] == self.v_reg[y] {
536            self.pc += 2;
537        }
538    }
539
540    /// 6xkk - Set Vx = kk.
541    fn ld_xkk(&mut self, x: usize, kk: u8) {
542        self.v_reg[x] = kk;
543    }
544
545    /// 7xkk - Set Vx = Vx + kk.
546    fn add_xkk(&mut self, x: usize, kk: u8) {
547        self.v_reg[x] = self.v_reg[x].wrapping_add(kk);
548    }
549
550    /// 8xy0 - Set Vx = Vy.
551    fn ld_xy(&mut self, x: usize, y: usize) {
552        self.v_reg[x] = self.v_reg[y];
553    }
554
555    /// 8xy1 - Set Vx = Vx OR Vy.
556    fn or(&mut self, x: usize, y: usize) {
557        self.v_reg[x] |= self.v_reg[y];
558    }
559
560    /// 8xy2 - Set Vx = Vx AND Vy.
561    fn and(&mut self, x: usize, y: usize) {
562        self.v_reg[x] &= self.v_reg[y];
563    }
564
565    /// 8xy3 - Set Vx = Vx XOR Vy.
566    fn xor(&mut self, x: usize, y: usize) {
567        self.v_reg[x] ^= self.v_reg[y];
568    }
569
570    /// 8xy4 - Set Vx = Vx + Vy, set VF = carry.
571    fn add_xy(&mut self, x: usize, y: usize) {
572        let (vx, carry) = self.v_reg[x].overflowing_add(self.v_reg[y]);
573        self.v_reg[x] = vx;
574        self.v_reg[VF] = carry as u8;
575    }
576
577    /// 8xy5 - Set Vx = Vx - Vy, set VF = NOT borrow.
578    fn sub_xy(&mut self, x: usize, y: usize) {
579        let (vx, borrow) = self.v_reg[x].overflowing_sub(self.v_reg[y]);
580        self.v_reg[x] = vx;
581        self.v_reg[VF] = !borrow as u8;
582    }
583
584    /// 8xy6 - Set Vx = Vx SHR 1.
585    fn shr(&mut self, x: usize, _y: usize) {
586        let vx = self.v_reg[x];
587        self.v_reg[x] = vx >> 1;
588        self.v_reg[VF] = vx & 1;
589    }
590
591    /// 8xy7 - Set Vx = Vy - Vx, set VF = NOT borrow.
592    fn subn_xy(&mut self, x: usize, y: usize) {
593        let (vx, borrow) = self.v_reg[y].overflowing_sub(self.v_reg[x]);
594        self.v_reg[x] = vx;
595        self.v_reg[VF] = !borrow as u8;
596    }
597
598    /// 8xyE - Set Vx = Vx SHL 1.
599    fn shl(&mut self, x: usize, _y: usize) {
600        let vx = self.v_reg[x];
601        self.v_reg[x] = vx << 1;
602        self.v_reg[VF] = (vx >> 7) & 1;
603    }
604
605    /// 9xy0 - Skip next instruction if Vx != Vy.
606    fn sne_xy(&mut self, x: usize, y: usize) {
607        if self.v_reg[x] != self.v_reg[y] {
608            self.pc += 2;
609        }
610    }
611
612    /// Annn - Set I = nnn.
613    fn ld_innn(&mut self, nnn: u16) {
614        self.i_reg = nnn;
615    }
616
617    /// Bnnn - Jump to location nnn + V0.
618    fn jp_0nnn(&mut self, nnn: u16) {
619        self.pc = nnn + self.v_reg[0] as u16;
620    }
621
622    /// Cxkk - Set Vx = random byte AND kk.
623    fn rnd(&mut self, x: usize, kk: u8) {
624        self.v_reg[x] = self.rng.random_range(0..=0xFF) as u8 & kk;
625    }
626
627    /// Dxyn - Display n-byte sprite starting at memory location I at (Vx, Vy),
628    /// set VF = collision.
629    fn drw(&mut self, x: usize, y: usize, n: u8) {
630        // a sprite is a byte wide and n in [1,15] rows where n is an integer
631        let (x, y) = (
632            self.v_reg[x] as usize % SCREEN_WIDTH,  // wrap
633            self.v_reg[y] as usize % SCREEN_HEIGHT, // wrap
634        );
635        self.v_reg[VF] = 0; // turn off collision flag
636        let start_pixel: usize = (y * SCREEN_WIDTH) + x;
637        let start_addr: usize = self.i_reg as usize;
638
639        // draw n bytes to the screen
640        for i in 0..n as usize {
641            if y + i >= SCREEN_HEIGHT {
642                break; // clip
643            }
644            let pixel_posn: usize = start_pixel + (SCREEN_WIDTH * i);
645            let sprite_row: u8 = self.ram[start_addr + i];
646
647            // for each bit
648            for j in 0..8 {
649                if x + j >= SCREEN_WIDTH {
650                    break; // clip
651                }
652                let ref mut pixel_ref = self.screen[pixel_posn + j];
653                let old_pixel = *pixel_ref;
654
655                let sprite_pixel = (sprite_row >> (0x7 - j)) & 0x1;
656                *pixel_ref ^= sprite_pixel != 0;
657
658                if !(*pixel_ref) && old_pixel {
659                    self.v_reg[VF] = 1; // turn on collision flag
660                }
661            }
662        }
663    }
664
665    /// Ex9E - Skip next instruction if key with the value of Vx is pressed.
666    fn skp(&mut self, x: usize) {
667        if self.keys[self.v_reg[x] as usize] {
668            self.pc += 2;
669        }
670    }
671
672    /// ExA1 - Skip next instruction if key with the value of Vx is not pressed.
673    fn sknp(&mut self, x: usize) {
674        if !self.keys[self.v_reg[x] as usize] {
675            self.pc += 2;
676        }
677    }
678
679    /// Fx07 - Set Vx = delay timer value.
680    fn ld_xdt(&mut self, x: usize) {
681        self.v_reg[x] = self.dt;
682    }
683
684    /// Fx0A - Wait for a key press, store the value of the key in Vx.
685    fn ld_xk(&mut self, x: usize) {
686        match self.stored_key {
687            Some(k) => {
688                // Wait for key release
689                if !self.keys[k] {
690                    self.v_reg[x] = k as u8;
691                    self.stored_key = None;
692                    return;
693                }
694            }
695            None => {
696                // Store key press
697                for (k, &pressed) in self.keys.iter().enumerate() {
698                    if pressed {
699                        self.stored_key = Some(k);
700                        break;
701                    }
702                }
703            }
704        }
705        // Halt: set pc to previous state
706        self.pc -= 2;
707    }
708
709    /// Fx15 - Set delay timer = Vx.
710    fn ld_dtx(&mut self, x: usize) {
711        self.dt = self.v_reg[x];
712    }
713
714    /// Fx18 - Set sound timer = Vx.
715    fn ld_stx(&mut self, x: usize) {
716        self.st = self.v_reg[x];
717    }
718
719    /// Fx1E - Set I = I + Vx.
720    fn add_ix(&mut self, x: usize) {
721        self.i_reg = self.i_reg.wrapping_add(self.v_reg[x] as u16);
722    }
723
724    /// Fx29 - Set I = location of sprite for digit Vx.
725    fn ld_fx(&mut self, x: usize) {
726        self.i_reg = FONT_ADDR + (self.v_reg[x] as u16 * 5);
727    }
728
729    /// Fx33 - Store BCD representation of Vx in memory locations I, I+1, and I+2.
730    fn ld_bx(&mut self, x: usize) {
731        let i = self.i_reg as usize;
732        let v = self.v_reg[x];
733        self.ram[i] = (v / 100) % 10;
734        self.ram[i + 1] = (v / 10) % 10;
735        self.ram[i + 2] = v % 10;
736    }
737
738    /// Fx55 - Store registers V0 through Vx in memory starting at location I.
739    fn ld_ix(&mut self, x: usize) {
740        let i = self.i_reg as usize;
741        self.ram[i..=(i + x)].copy_from_slice(&self.v_reg[0..=x]);
742    }
743
744    /// Fx65 - Read registers V0 through Vx from memory starting at location I.
745    fn ld_xi(&mut self, x: usize) {
746        let i = self.i_reg as usize;
747        self.v_reg[0..=x].copy_from_slice(&self.ram[i..=(i + x)]);
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    #[test]
756    fn test() {
757        // for misc testing
758        let a: [u8; 5] = [255, 155, 100, 55, 5];
759        let i: u16 = 0;
760        assert_eq!(255, a[i as usize]);
761        assert_eq!(155, a[i as usize + 1]);
762    }
763
764    #[test]
765    fn opcode_new() {
766        let opcode = Opcode::new(0x12, 0x34);
767        assert_eq!(opcode.0, 0x1);
768        assert_eq!(opcode.1, 0x2);
769        assert_eq!(opcode.2, 0x3);
770        assert_eq!(opcode.3, 0x4);
771    }
772
773    #[test]
774    fn opcode_decode() {
775        let opcode = Opcode::new(0x12, 0x34);
776        assert_eq!(opcode.full(), 0x1234);
777        assert_eq!(opcode.nnn(), 0x234);
778        assert_eq!(opcode.n(), 0x4);
779        assert_eq!(opcode.x(), 0x2);
780        assert_eq!(opcode.y(), 0x3);
781        assert_eq!(opcode.kk(), 0x34);
782    }
783
784    #[test]
785    fn invalid_opcode() {
786        let mut emu = Oxid8::new();
787        emu.ram[START_ADDR as usize] = 0xFF;
788        emu.ram[START_ADDR as usize + 1] = 0xFF;
789        assert!(emu.run_cycle().is_err_and(|msg| msg
790            == format!(
791                "Invalid Instruction: FFFF at {}", //
792                START_ADDR                         //
793            )))
794    }
795
796    #[test]
797    fn push_pop() {
798        let mut emu = Oxid8::new();
799        assert_eq!(emu.sp, 0); // base stack pointer
800        emu.push(1); // push
801        assert_eq!(emu.sp, 1); // inc stack pointer
802        assert_eq!(emu.stack[0], 1); // value on stack
803        assert_eq!(emu.pop(), 1); // pop
804        assert_eq!(emu.sp, 0); // dec stack pointer
805    }
806
807    #[test]
808    #[should_panic(expected = "Stack Overflow")]
809    fn push_panic() {
810        let mut emu = Oxid8::new();
811        for _ in 0..=STACK_SIZE {
812            emu.push(1);
813        }
814    }
815
816    #[test]
817    #[should_panic(expected = "Stack Underflow")]
818    fn pop_panic() {
819        let mut emu = Oxid8::new();
820        emu.pop();
821    }
822
823    #[test]
824    fn load_font() {
825        let mut emu = Oxid8::new();
826        emu.load_font();
827        assert_eq!(
828            emu.ram[FONT_ADDR as usize..(FONT_ADDR as usize + FONTSET_SIZE)],
829            FONTSET
830        );
831    }
832
833    #[test]
834    fn draw_basic() {
835        // Largest drawable sprite.
836        // Just two 'X' on top of each other sized 8x15.
837        let sprite = [
838            0x81, 0x42, 0x24, 0x18, //
839            0x18, 0x24, 0x42, 0x81, //
840            0x42, 0x24, 0x18, 0x18, //
841            0x24, 0x42, 0x81, //
842        ];
843
844        let screen = [
845            true, false, false, false, false, false, false, true, // 1
846            false, true, false, false, false, false, true, false, // 2
847            false, false, true, false, false, true, false, false, // 3
848            false, false, false, true, true, false, false, false, // 4
849            false, false, false, true, true, false, false, false, // 5
850            false, false, true, false, false, true, false, false, // 6
851            false, true, false, false, false, false, true, false, // 7
852            true, false, false, false, false, false, false, true, // 8
853            false, true, false, false, false, false, true, false, // 9
854            false, false, true, false, false, true, false, false, // 10
855            false, false, false, true, true, false, false, false, // 11
856            false, false, false, true, true, false, false, false, // 12
857            false, false, true, false, false, true, false, false, // 13
858            false, true, false, false, false, false, true, false, // 14
859            true, false, false, false, false, false, false, true, // 15
860        ];
861
862        let mut emu = Oxid8::new();
863
864        emu.i_reg = START_ADDR;
865        let start = START_ADDR as usize;
866
867        emu.ram[start..start + sprite.len()].copy_from_slice(&sprite);
868        emu.drw(0, 0, sprite.len() as u8);
869
870        for i in 0..15 {
871            let offset1: usize = i * SCREEN_WIDTH;
872            let offset2: usize = i * 8;
873            assert_eq!(
874                emu.screen[offset1 + 0..offset1 + 8],
875                screen[offset2 + 0..offset2 + 8]
876            );
877        }
878    }
879}