extern crate crossbeam_channel; extern crate ferretro; extern crate sdl2; use ferretro::retro; use ferretro::retro::ffi::{GameGeometry, SystemInfo, SystemAvInfo}; use ferretro::retro::constants::{InputIndex, JoypadButton, AnalogAxis, DeviceType}; use ferretro::retro::wrapped_types::{ControllerDescription2, InputDescriptor2, InputDeviceId, SubsystemInfo2, Variable2}; use ferretro::retro::wrapper::LibretroWrapper; use std::ffi::CStr; use std::io::Read; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::time::{Duration, Instant}; use structopt::StructOpt; use sdl2::audio::{AudioCallback, AudioFormat, AudioSpec, AudioSpecDesired, AudioDevice}; use sdl2::controller::{GameController, Button, Axis}; use sdl2::event::Event; use sdl2::keyboard::Keycode; use sdl2::rect::Rect; use sdl2::render::WindowCanvas; struct MyEmulator { retro: retro::wrapper::LibretroWrapper, core_path: PathBuf, sys_path: Option, preferred_pad: Option, sys_info: SystemInfo, av_info: SystemAvInfo, sdl_context: sdl2::Sdl, // video bits canvas: WindowCanvas, pixel_format: sdl2::pixels::PixelFormatEnum, // audio bits audio_buffer: Vec, audio_spec: AudioSpec, audio_device: AudioDevice, audio_sender: crossbeam_channel::Sender>, // input bits gamepad_subsys: sdl2::GameControllerSubsystem, gamepads: Vec, } impl MyEmulator { pub fn new(core_path: impl AsRef, sys_path: &Option>) -> Pin> { let core_path = PathBuf::from(core_path.as_ref()); let lib = libloading::Library::new(&core_path).unwrap(); let raw_retro = retro::loading::LibretroApi::from_library(lib).unwrap(); let retro = retro::wrapper::LibretroWrapper::from(raw_retro); let sys_info = retro.get_system_info(); let title = format!( "{} - rust libretro", unsafe { CStr::from_ptr(sys_info.library_name) }.to_string_lossy() ); let mut av_info = retro.get_system_av_info(); let pixel_format = sdl2::pixels::PixelFormatEnum::ARGB1555; // HACK: some cores don't report this 'til we get an environ call to set_system_av_info... // which is too late for this constructor to pass along to SDL. if av_info.timing.sample_rate == 0.0 { av_info.timing.sample_rate = 32040.0; } let sdl_context = sdl2::init().unwrap(); let window = sdl_context .video() .unwrap() .window(title.as_str(), av_info.geometry.base_width, av_info.geometry.base_height) .opengl() .build() .unwrap(); let canvas = window.into_canvas().build().unwrap(); let (audio_sender, audio_receiver) = crossbeam_channel::bounded(2); let audio = sdl_context.audio().unwrap(); let desired_spec = AudioSpecDesired { freq: Some(av_info.timing.sample_rate.round() as i32), channels: Some(2), samples: None, }; let mut audio_spec = None; let audio_device = audio .open_playback(None, &desired_spec, |spec| { if spec.format != AudioFormat::S16LSB { println!("unsupported audio format {:?}", spec.format); } audio_spec = Some(spec.clone()); MySdlAudio { audio_spec: spec, audio_receiver, } }) .unwrap(); let gamepad_subsys = sdl_context.game_controller().unwrap(); let mut gamepads = Vec::new(); for i in 0..gamepad_subsys.num_joysticks().unwrap() { gamepads.extend(gamepad_subsys.open(i).into_iter()); } let emu = MyEmulator { retro, core_path, sys_path: sys_path.as_ref().map(|p| p.as_ref().to_path_buf()), preferred_pad: None, av_info, sys_info, sdl_context, canvas, pixel_format, audio_buffer: Default::default(), audio_spec: audio_spec.unwrap(), audio_device, audio_sender, gamepad_subsys, gamepads, }; let mut pin_emu = Box::pin(emu); retro::wrapper::set_handler(pin_emu.as_mut()); pin_emu.retro.init(); pin_emu } pub fn run(&mut self) { self.audio_device.resume(); let mut event_pump = self.sdl_context.event_pump().unwrap(); 'running: loop { let frame_begin = Instant::now(); for event in event_pump.poll_iter() { match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => break 'running, _ => {} } } // The rest of the game loop goes here... self.retro.run(); self.canvas.present(); // similar hack to the sample rate, make sure we don't divide by zero. let mut spf = 1.0 / self.av_info.timing.fps; if spf.is_nan() || spf.is_infinite() { spf = 1.0 / 60.0; } Duration::from_secs_f64(spf) .checked_sub(frame_begin.elapsed()) .map(std::thread::sleep); } } pub fn load_game(&self, rom: impl AsRef) { let path = rom.as_ref(); let mut data = None; let mut v = Vec::new(); if !self.sys_info.need_fullpath { if let Ok(mut f) = std::fs::File::open(path) { if f.read_to_end(&mut v).is_ok() { data = Some(v.as_ref()); } } } self.retro .load_game(Some(path), data, None) .unwrap(); if let Some(device) = self.preferred_pad { for port in 0..self.gamepads.len() as u32 { self.retro.set_controller_port_device(port, device); } } } fn send_audio_samples(&mut self) { let stereo_samples = self.audio_spec.samples as usize * 2; while self.audio_buffer.len() >= stereo_samples { let remainder = self.audio_buffer.split_off(stereo_samples); let msg = std::mem::replace(&mut self.audio_buffer, remainder); let _ = self.audio_sender.try_send(msg); } } } impl Drop for MyEmulator { fn drop(&mut self) { retro::wrapper::unset_handler(); } } impl retro::wrapper::Handler for MyEmulator { fn libretro_core(&mut self) -> &mut LibretroWrapper { &mut self.retro } fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { let rect = Rect::new(0, 0, width, height); if let Ok(mut tex) = self.canvas .texture_creator() .create_texture_static(self.pixel_format, width, height) { if tex.update(rect, data, pitch as usize).is_ok() { self.canvas.clear(); self.canvas.copy(&tex, None, None).unwrap(); } } } fn audio_sample(&mut self, left: i16, right: i16) { self.audio_buffer.push(left); self.audio_buffer.push(right); self.send_audio_samples() } fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { self.audio_buffer.extend(stereo_pcm); self.send_audio_samples(); stereo_pcm.len() } fn input_poll(&mut self) { self.gamepad_subsys.update(); } fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { match self.gamepads.get(port as usize) { Some(gamepad) => { match device { InputDeviceId::Joypad(button) => { match button_map(&button) { Some(x) => gamepad.button(x) as i16, None => match button { JoypadButton::L2 => gamepad.axis(Axis::TriggerLeft), JoypadButton::R2 => gamepad.axis(Axis::TriggerRight), _ => 0, } } } InputDeviceId::Analog(axis) => gamepad.axis(axis_map(index, axis)), _ => 0, } } None => 0, } } fn get_can_dupe(&mut self) -> Option { Some(true) } fn get_system_directory(&mut self) -> Option { self.sys_path.clone() } fn set_pixel_format(&mut self, pix_fmt: retro::ffi::PixelFormat) -> bool { self.pixel_format = match pix_fmt { retro::ffi::PixelFormat::ARGB1555 => sdl2::pixels::PixelFormatEnum::RGB555, retro::ffi::PixelFormat::ARGB8888 => sdl2::pixels::PixelFormatEnum::ARGB8888, retro::ffi::PixelFormat::RGB565 => sdl2::pixels::PixelFormatEnum::RGB565, }; true } fn get_variable(&mut self, key: &str) -> Option { match key { "beetle_saturn_analog_stick_deadzone" => Some("15%".to_string()), "parallel-n64-gfxplugin" => Some("angrylion".to_string()), "parallel-n64-astick-deadzone" => Some("15%".to_string()), _ => None, } } fn set_variables(&mut self, variables: Vec) -> bool { for v in variables { eprintln!("{:?}", v); } true } fn get_libretro_path(&mut self) -> Option { Some(self.core_path.clone()) } fn get_input_device_capabilities(&mut self) -> Option { let bits = (1 << (DeviceType::Joypad as u32)) | (1 << (DeviceType::Analog as u32)); Some(bits as u64) } fn get_save_directory(&mut self) -> Option { Some(std::env::temp_dir()) } fn set_system_av_info(&mut self, av_info: SystemAvInfo) -> bool { self.set_geometry(av_info.geometry.clone()); self.av_info = av_info; true } fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { println!("subsystem info: {:?}", subsystem_info); true } fn set_controller_info(&mut self, controller_info: Vec) -> bool { for ci in controller_info { // so we can have analog support in beetle/mednafen saturn if ci.name.as_str() == "3D Control Pad" { self.preferred_pad = Some(ci.device_id()); break; } } true } fn set_input_descriptors(&mut self, descriptors: Vec) -> bool { for id in descriptors { println!("{:?}", id); } true } fn set_geometry(&mut self, geom: GameGeometry) -> bool { let _ = self.canvas.window_mut().set_size(geom.base_width, geom.base_height); let _ = self.canvas.set_logical_size(geom.base_width, geom.base_height); self.av_info.geometry = geom; true } fn log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) { eprint!("[{:?}] {}", level, msg); } } struct MySdlAudio { audio_spec: AudioSpec, audio_receiver: crossbeam_channel::Receiver>, } impl AudioCallback for MySdlAudio { type Channel = i16; fn callback(&mut self, out: &mut [Self::Channel]) { if self.audio_spec.format == AudioFormat::S16LSB { if let Ok(samples) = self.audio_receiver.recv() { out.copy_from_slice(&samples[..out.len()]); } } } } pub fn main() -> failure::Fallible<()> { let opt: Opt = Opt::from_args(); let mut emu = MyEmulator::new(&opt.core, &opt.system); emu.load_game(&opt.rom); emu.run(); Ok(()) } #[derive(StructOpt)] struct Opt { /// Core module to use. #[structopt(short, long, parse(from_os_str))] core: PathBuf, /// ROM to load using the core. #[structopt(short, long, parse(from_os_str))] rom: PathBuf, /// System directory, often containing BIOS files #[structopt(short, long, parse(from_os_str))] system: Option, } fn button_map(retro_button: &JoypadButton) -> Option