diff options
author | lamp | 2023-03-05 21:30:48 +0000 |
---|---|---|
committer | lamp | 2023-03-05 21:30:48 +0000 |
commit | e428933723cde84d59137b5abca282bb1fdbc1e7 (patch) | |
tree | f16f3e685e9d85174e45834185fdff4a1c6344df /src |
initmain
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3fa8f4c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,441 @@ +extern crate minifb; +extern crate cpal; +extern crate rand; + +use std::env; +use std::fs::File; +use std::io::Read; +use std::time::{Instant, Duration}; +use std::thread; +use std::sync::mpsc; +use std::path::Path; + +use minifb::{Window, WindowOptions, Key}; +use cpal::traits::{HostTrait, DeviceTrait, StreamTrait}; +use cpal::SampleFormat; + +const PROGRAM_START_ADDR: usize = 0x200; +const FONT_START_ADDR: usize = 0x000; +const MEMORY_LENGTH: usize = 0x1000; +const SCREEN_HEIGHT: usize = 0x20; +const SCREEN_WIDTH: usize = 0x40; +const PIXEL_OFF_COLOR: u32 = 0x00000000; +const PIXEL_ON_COLOR: u32 = 0x00FFFFFF; +const FONT_BYTES_PER_CHAR: u16 = 5; +const FONT_DATA: [u8; 80] = [ + 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 + 0x20, 0x60, 0x20, 0x20, 0x70, // 1 + 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 + 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 + 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 + 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 + 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 + 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 + 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 + 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 + 0xF0, 0x90, 0xF0, 0x90, 0x90, // A + 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B + 0xF0, 0x80, 0x80, 0x80, 0xF0, // C + 0xE0, 0x90, 0x90, 0x90, 0xE0, // D + 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E + 0xF0, 0x80, 0xF0, 0x80, 0x80, // F +]; + +struct Registers { + pub i: u16, + pub pc: u16, + pub v: [u8; 0x10], +} + +impl Registers { + fn new() -> Self { + Self { + i: 0, + pc: PROGRAM_START_ADDR as u16, + v: [0; 0x10], + } + } +} + +struct Display { + framebuffer: Vec<u32>, + window: Window, + scale_factor: usize, +} + +impl Display { + fn new(scale_factor: usize) -> Self { + Self { + framebuffer: vec![PIXEL_OFF_COLOR; SCREEN_HEIGHT * SCREEN_WIDTH * scale_factor * scale_factor], + window: Window::new("CHIP-8 Emulator", SCREEN_WIDTH * scale_factor, SCREEN_HEIGHT * scale_factor, WindowOptions::default()).unwrap(), + scale_factor, + } + } + + fn clear(&mut self) { + for pixel in &mut self.framebuffer { + *pixel = PIXEL_OFF_COLOR; + } + } + + fn redraw(&mut self) { + self.window.update_with_buffer(&self.framebuffer, SCREEN_WIDTH * self.scale_factor, SCREEN_HEIGHT * self.scale_factor).unwrap(); + } + + fn flip_pixel(&mut self, x: usize, y: usize) -> bool { + if let Some(pixel) = self.get_pixel(x, y) { + if *pixel == PIXEL_OFF_COLOR { + self.set_pixel(x, y, PIXEL_ON_COLOR); + false + } else { + self.set_pixel(x, y, PIXEL_OFF_COLOR); + true + } + } else { + false + } + } + + fn get_pixel(&self, x: usize, y: usize) -> Option<&u32> { + self.framebuffer.get(y * self.scale_factor * self.scale_factor * SCREEN_WIDTH + x * self.scale_factor) + } + + fn set_pixel(&mut self, x: usize, y: usize, color: u32) { + for x_scaled in 0..self.scale_factor { + for y_scaled in 0..self.scale_factor { + //if let Some(pixel) = self.framebuffer.get_mut((y * self.scale_factor + y_scaled) * SCREEN_WIDTH + x * self.scale_factor + x_scaled) { + if let Some(pixel) = self.framebuffer.get_mut((y * self.scale_factor + y_scaled) * self.scale_factor * SCREEN_WIDTH + x * self.scale_factor + x_scaled ) { + *pixel = color; + } + } + } + } + + fn is_key_down(&self, key: Key) -> bool { + self.window.is_key_down(key) + } + + fn is_open(&self) -> bool { + self.window.is_open() + } + + fn update_without_redraw(&mut self) { + self.window.update(); + } +} + +struct Buzzer { + stream: cpal::Stream, +} + +impl Buzzer { + fn new() -> Self { + let host = cpal::default_host(); + let device = host.devices().unwrap().next().unwrap(); + let supported_config = device.default_output_config().expect("Failed to determine default output configuration for audio device!"); + let sample_format = supported_config.sample_format(); + let config = supported_config.into(); + let stream = match sample_format { + SampleFormat::F32 => Self::build_stream::<f32>(&device, &config), + SampleFormat::I16 => Self::build_stream::<i16>(&device, &config), + SampleFormat::U16 => Self::build_stream::<u16>(&device, &config), + }; + stream.pause().unwrap(); + Self { + stream, + } + } + + fn build_stream<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> cpal::Stream + where + T: cpal::Sample + { + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + + let mut sample_clock = 0f32; + let mut next_value = move || { + sample_clock = (sample_clock + 1.0) % sample_rate; + (sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin() + }; + + let err_fn = |err| println!("An error occurend on audio stream: {}", err); + + device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + Self::write_data(data, channels, &mut next_value) + }, + err_fn, + ).expect("Failed to build audio output stream!") + } + + fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) + where + T: cpal::Sample + { + for frame in output.chunks_mut(channels) { + let value: T = cpal::Sample::from::<f32>(&next_sample()); + for sample in frame.iter_mut() { + *sample = value; + } + } + } + + fn play(&self) { + self.stream.play().unwrap(); + } + + fn pause(&self) { + self.stream.pause().unwrap(); + } +} + +// TODO: play sound +fn main() { + let mut args = env::args(); + args.next(); // Discard path to binary + let mut memory = vec![0; MEMORY_LENGTH]; + memory[FONT_START_ADDR..(FONT_START_ADDR + FONT_DATA.len())].copy_from_slice(&FONT_DATA); + let mut display = Display::new(args.next() + .expect("Invalid arguments. Specify a scale factor.") + .trim() + .parse::<usize>() + .expect("Only integer scale factors are supported.")); + let cycle_duration = Duration::from_secs(1) / args.next() + .expect("Invalid arguments. Specify a clock speed.") + .trim() + .parse::<u32>() + .expect("Only integer clock speeds are supported."); + { + let file_name = args.next().expect("Invalid arguments. Specify the path to the program."); + let mut program = File::open(Path::new(&file_name)).expect(&format!("Failed to open: {}", file_name)); + program.read(&mut memory[PROGRAM_START_ADDR..MEMORY_LENGTH]).expect("Failed to read program into memory!"); + } + let mut stack: Vec<u16> = Vec::new(); + let mut reg = Registers::new(); + let mut delay_timer: u8 = 0x00; + let mut sound_timer: u8 = 0x00; + let buzzer = Buzzer::new(); + let (timer_tx, timer_rx) = mpsc::channel::<()>(); + let (end_tx, end_rx) = mpsc::channel::<()>(); + thread::spawn(move || { + let cycle_duration = Duration::from_secs(1) / 60; + loop { + let start_time = Instant::now(); + if let Ok(_) = end_rx.try_recv() { + break; + } + timer_tx.send(()).expect("Failed to update timers!"); + thread::sleep(cycle_duration.saturating_sub(start_time.elapsed())); + } + }); + let mut waiting_for_key: Option<usize> = None; + let keymap = [ + Key::X, + Key::Key1, + Key::Key2, + Key::Key3, + Key::Q, + Key::W, + Key::E, + Key::A, + Key::S, + Key::D, + Key::Z, + Key::C, + Key::Key4, + Key::R, + Key::F, + Key::V, + ]; + while !display.is_key_down(Key::Escape) && display.is_open() { + let cycle_start_time = Instant::now(); + if let Some(register) = waiting_for_key { + for i in 0..keymap.len() { + if display.is_key_down(keymap[i]) { + reg.v[register] = i as u8; + waiting_for_key = None; + break; + } + } + display.update_without_redraw(); + thread::sleep(cycle_duration.saturating_sub(cycle_start_time.elapsed())); + continue; + } + let operation = u16::from_be_bytes([memory[reg.pc as usize], memory[(reg.pc + 1) as usize]]); + reg.pc += 2; + let high_nibble = (operation & 0xF000) >> 12; + match high_nibble { + 0x00 => { + match operation { + 0x00E0 => { + display.clear(); + display.redraw(); + }, + 0x00EE => reg.pc = stack.pop().expect("Attempted to return with an empty stack!"), + _ => panic!("Not implemented: {:#x} (execute machine language subroutine)", operation), + } + }, + 0x01 => reg.pc = operation & 0x0FFF, + 0x02 => { + stack.push(reg.pc); + reg.pc = operation & 0x0FFF; + }, + 0x03 => { + let register = (operation & 0x0F00) >> 8; + if reg.v[register as usize] == (operation & 0x00FF) as u8 { + reg.pc += 2; + } + }, + 0x04 => { + let register = (operation & 0x0F00) >> 8; + if reg.v[register as usize] != (operation & 0x00FF) as u8 { + reg.pc += 2; + } + }, + 0x05 => { + let x = (operation & 0x0F00) >> 8; + let y = (operation & 0x00F0) >> 4; + if reg.v[x as usize] == reg.v[y as usize] { + reg.pc += 2; + } + }, + 0x06 => { + let register = (operation & 0x0F00) >> 8; + let value = (operation & 0x00FF) as u8; + reg.v[register as usize] = value; + }, + 0x07 => { + let register = ((operation & 0x0F00) >> 8) as usize; + let value = (operation & 0x00FF) as u8; + reg.v[register] = reg.v[register].wrapping_add(value); + }, + 0x08 => { + let x = ((operation & 0x0F00) >> 8) as usize; + let y = ((operation & 0x00F0) >> 4) as usize; + match operation & 0x000F { + 0x00 => reg.v[x] = reg.v[y], + 0x01 => reg.v[x] = reg.v[x] | reg.v[y], + 0x02 => reg.v[x] = reg.v[x] & reg.v[y], + 0x03 => reg.v[x] = reg.v[x] ^ reg.v[y], + 0x04 => { + let (result, overflowed) = reg.v[x].overflowing_add(reg.v[y]); + reg.v[x] = result; + reg.v[0xF] = overflowed as u8; + }, + 0x05 => { + let (result, overflowed) = reg.v[x].overflowing_sub(reg.v[y]); + reg.v[x] = result; + reg.v[0xF] = (!overflowed) as u8; + }, + 0x06 => { + reg.v[x] = reg.v[y] >> 1; + reg.v[0xF] = reg.v[y] & 0x01; + }, + 0x07 => { + let (result, overflowed) = reg.v[y].overflowing_sub(reg.v[x]); + reg.v[x] = result; + reg.v[0xF] = (!overflowed) as u8; + } + 0x0E => { + reg.v[x] = reg.v[y] << 1; + reg.v[0xF] = (reg.v[y] & 0x80) >> 7; + } + _ => panic!("Invalid instruction: {:#x}", operation), + } + }, + 0x09 => { + let x = (operation & 0x0F00) >> 8; + let y = (operation & 0x00F0) >> 4; + if reg.v[x as usize] != reg.v[y as usize] { + reg.pc += 2; + } + }, + 0x0A => reg.i = operation & 0x0FFF, + 0x0B => reg.pc = (operation & 0x0FFF) + (reg.v[0] as u16), + 0x0C => { + let register = ((operation & 0x0F00) >> 8) as usize; + let mask = (operation & 0x00FF) as u8; + reg.v[register] = rand::random::<u8>() & mask; + }, + 0x0D => { + reg.v[0xF] = 0x00; + let x = reg.v[((operation & 0x0F00) >> 8) as usize] % (SCREEN_WIDTH as u8); + let y = reg.v[((operation & 0x00F0) >> 4) as usize] % (SCREEN_HEIGHT as u8); + let rows = operation & 0x000F; + for row in 0..rows { + let row_data = memory[(reg.i + row) as usize]; + for column in 0..8 { + let pixel_state = (row_data << column) & 0x80; + if pixel_state == 0x80 { + reg.v[0xF] |= display.flip_pixel((x + column) as usize, (y + (row as u8)) as usize) as u8; + } + } + } + display.redraw(); + }, + 0x0E => { + let key = keymap[reg.v[((operation & 0x0F00) >> 8) as usize] as usize]; + match operation & 0x00FF { + 0x9E => { + if display.is_key_down(key) { + reg.pc += 2; + } + }, + 0xA1 => { + if !display.is_key_down(key) { + reg.pc += 2; + } + }, + _ => panic!("Invalid instruction: {:#x}", operation), + } + }, + 0x0F => { + let register = ((operation & 0x0F00) >> 8) as usize; + match operation & 0x00FF { + 0x07 => reg.v[register] = delay_timer, + 0x0A => waiting_for_key = Some(register), + 0x15 => delay_timer = reg.v[register], + 0x18 => sound_timer = reg.v[register], + 0x1E => reg.i = reg.i.wrapping_add(reg.v[register] as u16), + 0x29 => reg.i = FONT_START_ADDR as u16 + reg.v[register] as u16 * FONT_BYTES_PER_CHAR, + 0x33 => { + memory[reg.i as usize] = reg.v[register] / 100; + memory[reg.i as usize + 1] = (reg.v[register] % 100) / 10; + memory[reg.i as usize + 2] = reg.v[register] % 10; + }, + 0x55 => { + for i in 0..=register { + memory[reg.i as usize] = reg.v[i]; + reg.i += 1; + } + }, + 0x65 => { + for i in 0..=register { + reg.v[i] = memory[reg.i as usize]; + reg.i += 1; + } + }, + _ => panic!("Invalid instruction: {:#x}", operation), + } + } + _ => panic!("Not implemented: {:#x}", operation), + } + if let Ok(()) = timer_rx.try_recv() { + delay_timer = delay_timer.saturating_sub(1); + sound_timer = sound_timer.saturating_sub(1); + } + if sound_timer > 1 { + buzzer.play(); + } else { + buzzer.pause(); + } + // saturating_sub is still unstable for durations + /*if cycle_duration > cycle_start_time.elapsed() { + thread::sleep(cycle_duration - cycle_start_time.elapsed()); + }*/ + thread::sleep(cycle_duration.saturating_sub(cycle_start_time.elapsed())); + } + end_tx.send(()).unwrap(); +} |