Skip to content

Commit

Permalink
refactor an emulator abstraction in between to more easily allow cust…
Browse files Browse the repository at this point in the history
…om stepping through the emulation.
  • Loading branch information
ablakey committed May 17, 2020
1 parent 2b18f0a commit f21e46c
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 146 deletions.
101 changes: 52 additions & 49 deletions src/chip8.rs
Expand Up @@ -56,9 +56,6 @@ pub struct Chip8 {

/// Core feature implenentation.
impl Chip8 {
pub const CPU_FREQUENCY: usize = 2000; // microseconds -> 500Hz
pub const TIMER_FREQUENCY: usize = 16667; // microseconds -> 60Hz

// Memory addresses (start, end).
// const ADDR_INTERPRETER: (usize, usize) = (0x000, 0x1FF);
// const ADDR_FONTSET: (usize, usize) = (0x050, 0x0A0);
Expand Down Expand Up @@ -96,26 +93,6 @@ impl Chip8 {
Ok(())
}

/// Execute the next opcode.
pub fn tick(&mut self) {
// Reset flags.
self.has_graphics_update = false;

// Get opcode by combining two bits from memory.
let low = self.memory[self.program_counter + 1];
let high = self.memory[self.program_counter];
let opcode = ((high) << 8) | low;
let opcode_symbols = OpCodeSymbols::from_value(opcode);
println!("opcode: {:x?}", opcode);

self.execute_opcode(&opcode_symbols);

// Increment PC unless opcode is JUMP, JUMPI, or CALL.
if ![0xB, 0x2, 0x1].contains(&opcode_symbols.a) {
self.program_counter += Chip8::OPCODE_SIZE;
}
}

/// Decrement both sound and delay timers.
/// This should be getting called at 60hz by the emulator's controller.
pub fn decrement_timers(&mut self) {
Expand All @@ -135,11 +112,28 @@ impl Chip8 {
// rest of that opcode (write state to register) and then unpause the machine.
}

fn execute_opcode(&mut self, opcode_symbols: &OpCodeSymbols) {
#[rustfmt::skip]
// These are possible opcode symbols, not all of which are valid. Depending on the matched
// opcode, some of the symbols may be used.
let OpCodeSymbols { a, x, y, n, nnn, nn } = *opcode_symbols;
pub fn execute_opcode(&mut self) {
// These are possible opcode symbols, not all of which are valid. Depending on the matched
// opcode, some of the symbols may be used.

// Reset flags.
self.has_graphics_update = false;

// Get opcode by combining two bits from memory.
let low = self.memory[self.program_counter + 1];
let high = self.memory[self.program_counter];
let opcode = ((high) << 8) | low;
let opcode_symbols = OpCodeSymbols::from_value(opcode);
println!("opcode: {:x?}", opcode);

let OpCodeSymbols {
a,
x,
y,
n,
nnn,
nn,
} = opcode_symbols;

// The order of these match branches are important.
// Some opcodes are more specific than others.
Expand Down Expand Up @@ -181,6 +175,11 @@ impl Chip8 {
(0xF, _, 6, 5) => self.READ(x),
(_, _, _, _) => panic!("Tried to call {:?} but isn't handled.", opcode_symbols),
};

// Increment PC unless opcode is JUMP, JUMPI, or CALL.
if ![0xB, 0x2, 0x1].contains(&opcode_symbols.a) {
self.program_counter += Chip8::OPCODE_SIZE;
}
}
}
/// Opcode implementation.
Expand All @@ -191,7 +190,12 @@ impl Chip8 {
self.has_graphics_update = true;
}

fn RTS(&mut self) {}
/// Return from subroutine.
fn RTS(&mut self) {
self.program_counter = self.stack[self.stack_pointer];
self.stack_pointer -= 1;
self.print_debug();
}

// Jump to machine code routine at nnn. Not implemented in modern CHIP8 emulators.
fn SYS(&mut self, nnn: usize) {
Expand Down Expand Up @@ -242,7 +246,7 @@ impl Chip8 {

/// Add NN to VX. Carry flag isn't changed.
fn ADD(&mut self, x: usize, nn: usize) {
self.registers[x] = (self.registers[x] + nn) / 0x100
self.registers[x] = (self.registers[x] + nn) % 0x100
}

/// Write VY to VX.
Expand Down Expand Up @@ -271,7 +275,7 @@ impl Chip8 {
let vy = self.registers[y];

self.registers[0xF] = if vx + vy >= 0x100 { 1 } else { 0 };
self.registers[x] = (vx + vy) / 0x100;
self.registers[x] = (vx + vy) % 0x100;
}

fn SUB(&mut self, x: usize, y: usize) {
Expand All @@ -283,8 +287,13 @@ impl Chip8 {
fn SUBN(&mut self, x: usize, y: usize) {
self.not_implemented();
}

/// Store most-significant bit of VX in VF then shift VX left by 1.
fn SHL(&mut self, x: usize, y: usize) {
self.not_implemented();
let vx = self.registers[x];
self.registers[0xF] = x & 0x80;

self.registers[x] = (vx << 1) & 0xFF;
}

/// Skip next instruction if VX != VY.
Expand Down Expand Up @@ -372,7 +381,7 @@ impl Chip8 {
let i = self.index_register;

self.registers[0xF] = if vx + i >= 0x1000 { 1 } else { 0 };
self.index_register = (vx + i) / 0x1000
self.index_register = (vx + i) % 0x1000
}

fn LDSPR(&mut self, x: usize) {
Expand All @@ -384,18 +393,24 @@ impl Chip8 {
fn STOR(&mut self, x: usize) {
self.not_implemented();
}

/// Populate registers V0 to VX with data starting at I.
fn READ(&mut self, x: usize) {
self.not_implemented();
for n in 0..x + 1 {
self.registers[n] = self.memory[self.index_register + n];
}
}
}

/// Debug functions.
// #[cfg(debug_assertions)]
impl Chip8 {
pub fn print_debug(&self) {
println!("PC: {}", self.program_counter);
println!("I: {}", self.index_register);
println!("Registers: {:x?}", self.registers)
println!("PC: {:x}", self.program_counter);
println!("SP: {:x}", self.stack_pointer);
println!("I: {:x}", self.index_register);
println!("Registers: {:x?}", self.registers);
println!("Stack: {:x?}", self.stack);
}

pub fn print_mem(&self) {
Expand Down Expand Up @@ -444,18 +459,6 @@ mod tests {
assert_eq!(&machine.memory[start..end], TEST_ROM_BYTES);
}

/// The CLR opcode must set all graphics to 0, set the graphics update flag, and increment
// the program counter.
#[test]
fn test_opcode_clr() {
let mut machine = Chip8::init();
let opcode_symbols = OpCodeSymbols::from_value(0x00E0);
machine.execute_opcode(&opcode_symbols);

assert!(machine.graphics_buffer.iter().all(|&n| !n));
assert!(machine.has_graphics_update);
}

/// Timers should decrement by 1 each time `decrement_timers` is called, but never fall below 0.
#[test]
fn test_decrement_timers() {
Expand Down
73 changes: 73 additions & 0 deletions src/emulator.rs
@@ -0,0 +1,73 @@
use super::chip8::Chip8;

pub struct Emulator {
states: Vec<Chip8>,
tick_count: usize,
}

impl Emulator {
const MAX_STATES: usize = 1000;

pub fn init() -> Result<Self, String> {
Ok(Self {
states: Vec::with_capacity(Emulator::MAX_STATES * 2),
tick_count: 0,
})
}

pub fn load_rom(&mut self, path: String) {
let mut c8 = Chip8::init();
c8.load_rom(path).unwrap();
self.save_state(c8);
}

/// Gets a clone of the latest state, updates the keys, and applies it to the stack of states.
pub fn set_keys(&mut self, keys: [bool; 16]) {
let mut s = self.get_state_clone();
s.set_keys(keys);
self.save_state(s);
}

/// Emulate one tick of the application. A Chip8 runs at about 500Hz (the timers are 60Hz)
/// This isn't perfectly accurate but that is 1 Opcode per tick and decrementing timers once
/// every 8 ticks.
pub fn tick(&mut self) {
self.tick_count += 1;

let mut s = self.get_state_clone();

// Every tick, process 1 opcode.
s.execute_opcode();

// Every 8th tick, decrement timers.
if self.tick_count % 8 == 0 {
s.decrement_timers();
}

self.save_state(s);
}

/// Return the current state's screen buffer.
pub fn get_screen_buffer(&self) -> &[bool; 64 * 32] {
return &self.states.last().unwrap().graphics_buffer;
}

pub fn has_graphics_update(&self) -> bool {
return self.states.last().unwrap().has_graphics_update;
}

fn get_state_clone(&self) -> Chip8 {
return self.states.last().unwrap().clone();
}

fn save_state(&mut self, c8: Chip8) {
// Garbage collect older states.
// TODO: A ring buffer makes a lot more sense. Let's use one rather than all this memory
// allocation and cleanup.
if self.states.len() > Emulator::MAX_STATES {
self.states = Vec::from(&self.states[1000..]);
}

self.states.push(c8);
}
}
7 changes: 7 additions & 0 deletions src/input.rs
Expand Up @@ -7,13 +7,16 @@ pub enum InputEvent {
None,
Exit,
ToggleRun,
Tick,
}

pub struct Input {
event_pump: EventPump,
}

impl Input {
// Binding each of the Chip8's keys to a real keyboard key. Chip8 has 16 keys: 0-F. Each is an
// index in this array. See get_chip8_keys for details.
const KEY_BINDINGS: [Scancode; 16] = [
Scancode::X,
Scancode::Num1,
Expand Down Expand Up @@ -56,6 +59,10 @@ impl Input {
keycode: Some(Keycode::Space),
..
} => InputEvent::ToggleRun,
Event::KeyUp {
keycode: Some(Keycode::Right),
..
} => InputEvent::Tick,
Event::KeyDown { .. } => InputEvent::None,
_ => InputEvent::None,
};
Expand Down
61 changes: 20 additions & 41 deletions src/main.rs
@@ -1,75 +1,54 @@
mod chip8;
mod emulator;
mod input;
mod screen;
use chip8::Chip8;
use emulator::Emulator;
use input::{Input, InputEvent};
use screen::Screen;
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use std::time::Duration;

fn main() -> Result<(), String> {
// Debug flags.
let mut paused = true;

// Init I/O components
let sdl_context = sdl2::init()?;
let mut input = Input::init(&sdl_context)?;
let mut screen = Screen::create(&sdl_context, 30)?;

// Init "system clock".
let clock = SystemTime::now();
let mut last_cpu_tick: u128 = 0;
let mut last_timer_tick: u128 = 0;

// Load a program.
let mut c8 = Chip8::init();
c8.load_rom(String::from("roms/maze.c8")).unwrap();

// Debug flags.
let mut paused = false;
// Init the emulated machine.
let mut emulator = Emulator::init().unwrap();
emulator.load_rom(String::from("roms/maze.c8"));

c8.print_mem();

// Loop controls the application, including debug tools.
// It ticks at a very high frequency to more accurately count delta time between ticks.
// The CPU runs at 500hz while the timers run at 60Hz.
// The screen is drawn on any opcode that has changed the graphics buffer.
'program: loop {
// Handle clock rate.
let now = clock.elapsed().unwrap().as_micros();

// Handle emulator I/O (the inputs not destined for the Chip8).
match input.get_event() {
InputEvent::Exit => break 'program,
InputEvent::ToggleRun => paused = !paused,
InputEvent::Tick => emulator.tick(),
_ => (),
}

// Do not run the machine if the emulator has paused it.
if !paused {
// Write keyboard state from I/O to emulator memory.
c8.set_keys(input.get_chip8_keys());
// Write key states to emulator's memory.
emulator.set_keys(input.get_chip8_keys());

// CPU tick?
if now - last_cpu_tick > Chip8::CPU_FREQUENCY as u128 && !c8.wait_for_input {
c8.tick();
last_cpu_tick = now;
}

// timer tick?
if now - last_timer_tick > Chip8::TIMER_FREQUENCY as u128 {
c8.decrement_timers();
last_timer_tick = now;
}
// Advance the emulator one tick.
emulator.tick();
}

// Draw to screen?
if c8.has_graphics_update {
screen.draw(&c8.graphics_buffer);
}
// Draw screen.
if emulator.has_graphics_update() {
screen.draw(emulator.get_screen_buffer());
}

// Sleep this hot loop at the same rate as the CPU (the most frequent thing).
// In a more accurate emulator, there's better ways to handle this, given it's possible
// that we sleep too long and never tick the CPU regularly enough. We would also have to
// handle "speeding up" to catch up with the expected frequency in that case.
sleep(Duration::new(0, (Chip8::CPU_FREQUENCY * 1000) as u32))
// 2000 Microseconds -> Milliseconds.
sleep(Duration::new(0, (2000 * 1000) as u32))
}

Ok(())
Expand Down

0 comments on commit f21e46c

Please sign in to comment.