#![feature( cell_update, never_type, exhaustive_patterns, generators, generator_trait )] use nes::ppu::PpuStep; use nes::NesStep; use sdl2::controller::Button as SdlButton; use sdl2::event::Event as SdlEvent; use sdl2::keyboard::Keycode as SdlKeycode; use std::fs; use std::io; use std::ops::{Generator, GeneratorState}; use std::path::PathBuf; use std::pin::Pin; use std::process; use std::thread; use std::time::{Duration, Instant}; use structopt::StructOpt; use lochnes::{input, nes, rom, video}; fn main() { let opts = Options::from_args(); let log_level = match (opts.quiet, opts.verbose) { (true, _) => None, (false, 0) => Some(tracing::Level::WARN), (false, 1) => Some(tracing::Level::INFO), (false, 2) => Some(tracing::Level::DEBUG), (false, 3..) => Some(tracing::Level::TRACE), }; tracing_subscriber::fmt() .with_max_level(log_level) .without_time() .init(); let run_result = run(opts); match run_result { Ok(_) => {} Err(err) => { eprintln!("{:?}", err); process::exit(1); } } } #[derive(StructOpt, Debug)] #[structopt(name = "lochnes")] struct Options { #[structopt(name = "ROM", parse(from_os_str))] rom: PathBuf, #[structopt(long = "scale")] scale: Option, #[structopt(short = "q", long = "quiet")] quiet: bool, #[structopt(short = "v", parse(from_occurrences))] verbose: u8, } fn run(opts: Options) -> Result<(), LochnesError> { tracing::debug!("Options: {:#?}", opts); #[cfg(feature = "easter-egg")] { if opts.verbose == 6 { let bytes = include_bytes!("../tests/fixtures/egg.nes"); let mut bytes: Vec = bytes.to_vec(); let nmi = &mut bytes[0x400C]; *nmi = nmi.wrapping_add(opts.verbose / 2); let rom = rom::Rom::from_bytes(bytes.into_iter())?; run_rom(opts, rom)?; return Ok(()); } } let bytes = fs::read(&opts.rom)?; let rom = rom::Rom::from_bytes(bytes.into_iter())?; tracing::debug!("ROM header: {:#04X?}", rom.header); run_rom(opts, rom)?; Ok(()) } fn run_rom(opts: Options, rom: rom::Rom) -> Result<(), LochnesError> { const NES_REFRESH_RATE: Duration = Duration::from_nanos(1_000_000_000 / 60); const NES_WIDTH: u32 = 256; const NES_HEIGHT: u32 = 240; let scale = opts.scale.unwrap_or(1); let window_width = NES_WIDTH * scale; let window_height = NES_HEIGHT * scale; let sdl = sdl2::init().map_err(LochnesError::Sdl2Error)?; let sdl_video = sdl.video().map_err(LochnesError::Sdl2Error)?; let sdl_window = sdl_video .window("Lochnes", window_width, window_height) .opengl() .build()?; let mut sdl_canvas = sdl_window.into_canvas().build()?; let sdl_texture_creator = sdl_canvas.texture_creator(); let sdl_controllers = sdl.game_controller().map_err(LochnesError::Sdl2Error)?; let num_sdl_controllers = sdl_controllers .num_joysticks() .map_err(LochnesError::Sdl2Error)?; let sdl_controller_index = (0..num_sdl_controllers).find_map(|n| { if sdl_controllers.is_game_controller(n) { Some(n) } else { None } }); let _sdl_controller = sdl_controller_index .map(|index| sdl_controllers.open(index)) .transpose()?; let mut sdl_event_pump = sdl.event_pump().map_err(LochnesError::Sdl2Error)?; let video = &video::TextureBufferedVideo::new(&sdl_texture_creator, NES_WIDTH, NES_HEIGHT)?; let mut input_state = input::InputState::default(); let input = &input::SampledInput::new(input_state); let io = nes::NesIoWith { video, input }; let nes = nes::Nes::new(&io, rom); let mut run_nes = nes.run(); 'running: loop { let frame_start = Instant::now(); for event in sdl_event_pump.poll_iter() { match event { SdlEvent::Quit { .. } | SdlEvent::KeyUp { keycode: Some(SdlKeycode::Escape), .. } => { break 'running; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Z), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::A, .. } => { input_state.joypad_1.a = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Z), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::A, .. } => { input_state.joypad_1.a = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::X), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::B, .. } | SdlEvent::ControllerButtonDown { button: SdlButton::X, .. } => { input_state.joypad_1.b = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::X), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::B, .. } | SdlEvent::ControllerButtonUp { button: SdlButton::X, .. } => { input_state.joypad_1.b = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Return), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::Start, .. } => { input_state.joypad_1.start = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Return), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::Start, .. } => { input_state.joypad_1.start = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Backslash), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::Back, .. } => { input_state.joypad_1.select = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Backslash), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::Back, .. } => { input_state.joypad_1.select = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Up), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::DPadUp, .. } => { input_state.joypad_1.up = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Up), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::DPadUp, .. } => { input_state.joypad_1.up = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Down), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::DPadDown, .. } => { input_state.joypad_1.down = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Down), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::DPadDown, .. } => { input_state.joypad_1.down = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Left), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::DPadLeft, .. } => { input_state.joypad_1.left = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Left), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::DPadLeft, .. } => { input_state.joypad_1.left = false; } SdlEvent::KeyDown { keycode: Some(SdlKeycode::Right), .. } | SdlEvent::ControllerButtonDown { button: SdlButton::DPadRight, .. } => { input_state.joypad_1.right = true; } SdlEvent::KeyUp { keycode: Some(SdlKeycode::Right), .. } | SdlEvent::ControllerButtonUp { button: SdlButton::DPadRight, .. } => { input_state.joypad_1.right = false; } _ => {} } } input.set_state(input_state); tracing::debug!("Input: {:?}", input_state); loop { match Pin::new(&mut run_nes).resume(()) { GeneratorState::Yielded(NesStep::Ppu(PpuStep::Vblank)) => { break; } GeneratorState::Yielded(NesStep::Cpu(nes::cpu::CpuStep::Op(op))) => { tracing::trace!("{:X?}", nes.cpu); tracing::trace!("${:04X}: {}", op.pc, op.op); tracing::trace!("----------"); } GeneratorState::Yielded(_) => {} } } video .copy_to(&mut sdl_canvas) .map_err(LochnesError::Sdl2Error)?; sdl_canvas.present(); let elapsed = frame_start.elapsed(); tracing::info!("frame time: {:5.2}ms", elapsed.as_micros() as f64 / 1_000.0); let duration_until_refresh = NES_REFRESH_RATE.checked_sub(elapsed); let sleep_duration = duration_until_refresh.unwrap_or_else(|| Duration::from_secs(0)); thread::sleep(sleep_duration); } Ok(()) } #[derive(Debug)] enum LochnesError { IoError(io::Error), RomError(rom::RomError), Sdl2Error(String), } impl From for LochnesError { fn from(err: io::Error) -> Self { LochnesError::IoError(err) } } impl From for LochnesError { fn from(err: rom::RomError) -> Self { LochnesError::RomError(err) } } impl From for LochnesError { fn from(err: sdl2::video::WindowBuildError) -> Self { LochnesError::Sdl2Error(err.to_string()) } } impl From for LochnesError { fn from(err: sdl2::IntegerOrSdlError) -> Self { LochnesError::Sdl2Error(err.to_string()) } } impl From for LochnesError { fn from(err: sdl2::render::TextureValueError) -> Self { LochnesError::Sdl2Error(err.to_string()) } }