diff --git a/Cargo.toml b/Cargo.toml index 3778c2c..1e91e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,21 @@ edition = "2018" cc = "^1" [dependencies] -libretro-sys = "^0.1" -failure = "^0.1" -libloading = "^0.5" -num_enum = "^0.4" +libretro-sys = "0.1" +libloading = "0.5" +num_enum = "0.4" +ffmpeg-next = { version = "4.3.8", optional = true } +sdl2 = { version = "0.32", optional = true } +crossbeam-channel = { version = "0.4", optional = true } [dev-dependencies] # example: sdl2_emulator -sdl2 = "^0.32" -crossbeam-channel = "^0.4" -structopt = "^0.3" +sdl2 = "0.32" +crossbeam-channel = "0.4" +structopt = "0.3" # example: ffmpeg_recorder ffmpeg-next = "4.3.8" + +[features] +ffmpeg_comp = ["ffmpeg-next"] +sdl2_comp = ["sdl2", "crossbeam-channel"] diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index c63057a..04c7be3 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -7,11 +7,10 @@ use std::path::{Path, PathBuf}; use std::pin::Pin; use structopt::StructOpt; -use failure::Fallible; use ferretro::retro; use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo, SystemInfo}; -use ferretro::retro::wrapper::{LibretroWrapper, Handler}; +use ferretro::retro::wrapper::{LibretroWrapper, RetroCallbacks}; use ferretro::retro::wrapped_types::{Variable2}; @@ -24,6 +23,7 @@ struct MyEmulator { av_info: SystemAvInfo, audio_buf: Vec<(i16, i16)>, video_pixel_format: format::Pixel, + prev_video_frame: Option, video_frames: VecDeque, video_encoder: ffmpeg::encoder::Video, audio_encoder: ffmpeg::encoder::Audio, @@ -204,7 +204,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, audio_encoder.set_time_base(Rational::new(1, 60)); audio_output.set_time_base(Rational::new(1, 60)); - let mut audio_encoder = audio_encoder.open_as(acodec).unwrap(); + let audio_encoder = audio_encoder.open_as(acodec).unwrap(); //audio_output.set_parameters(&audio_encoder); let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); @@ -220,6 +220,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, av_info: av_info.clone(), audio_buf: Default::default(), video_pixel_format: format::Pixel::RGB555, + prev_video_frame: None, video_frames: Default::default(), video_encoder, audio_encoder, @@ -234,7 +235,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, let mut pin_emu = Box::pin(emu); retro::wrapper::set_handler(pin_emu.as_mut()); pin_emu.retro.init(); - pin_emu.set_system_av_info(av_info); + pin_emu.set_system_av_info(&av_info); pin_emu } @@ -357,7 +358,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } - pub fn load_game(&self, rom: impl AsRef) { + pub fn load_game(&mut self, rom: impl AsRef) { let path = rom.as_ref(); let mut data = None; let mut v = Vec::new(); @@ -380,13 +381,26 @@ static bool ffmpeg_init_config(struct ff_config_param *params, self.receive_and_write_packets(EncoderToWriteFrom::Audio); self.octx.write_trailer().unwrap(); } + + pub fn unserialize(&mut self, state: impl AsRef) -> Result<(), Box> { + let path = state.as_ref(); + let mut v = Vec::new(); + if let Ok(mut f) = std::fs::File::open(path) { + if f.read_to_end(&mut v).is_ok(){ + return self.retro.unserialize(v.as_ref()); + } + } + Err("Couldn't read file to unserialize".into()) + } } -impl retro::wrapper::Handler for MyEmulator { +impl retro::wrapper::LibretroWrapperAccess for MyEmulator { fn libretro_core(&mut self) -> &mut LibretroWrapper { &mut self.retro } +} +impl retro::wrapper::RetroCallbacks for MyEmulator { fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { let mut vframe = frame::Video::new(self.video_pixel_format, width, height); @@ -409,9 +423,19 @@ impl retro::wrapper::Handler for MyEmulator { //vframe.set_pts(Some(self.frame as i64)); + self.prev_video_frame.replace(vframe.clone()); self.video_frames.push_back(vframe); } + fn video_refresh_dupe(&mut self, width: u32, height: u32, _pitch: u32) { + if let Some(frame) = &self.prev_video_frame { + self.video_frames.push_back(frame.clone()); + } else { + let vframe = frame::Video::new(self.video_pixel_format, width, height); + self.video_frames.push_back(vframe); + } + } + fn audio_sample(&mut self, left: i16, right: i16) { self.audio_buf.push((left, right)); } @@ -423,15 +447,13 @@ impl retro::wrapper::Handler for MyEmulator { stereo_pcm.len() } - fn get_can_dupe(&mut self) -> Option { Some(false) } - fn get_system_directory(&mut self) -> Option { self.sys_path.clone() } - fn set_pixel_format(&mut self, format: PixelFormat) -> bool { + fn set_pixel_format(&mut self, format: PixelFormat) -> Option { if self.frame_properties_locked { - return true; + return Some(true); } self.video_pixel_format = match format { @@ -440,12 +462,12 @@ impl retro::wrapper::Handler for MyEmulator { PixelFormat::RGB565 => format::Pixel::RGB565, }; self.video_filter = video_filter(&self.video_encoder, &self.av_info, format).unwrap(); - true + Some(true) } - fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> Option { if self.frame_properties_locked { - return true; + return Some(true); } //self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); @@ -454,20 +476,20 @@ impl retro::wrapper::Handler for MyEmulator { if system_av_info.timing.sample_rate.round() as i32 > 0 { self.audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); } - self.av_info.timing = system_av_info.timing; - self.set_geometry(system_av_info.geometry); - true + self.av_info.timing = system_av_info.timing.clone(); + self.set_geometry(&system_av_info.geometry); + Some(true) } - fn set_geometry(&mut self, geometry: GameGeometry) -> bool { + fn set_geometry(&mut self, geometry: &GameGeometry) -> Option { if self.frame_properties_locked { - return true; + return Some(true); } self.video_encoder.set_width(geometry.base_width); self.video_encoder.set_height(geometry.base_height); //self.video_encoder.set_aspect_ratio(geometry.aspect_ratio as f64); - self.av_info.geometry = geometry; + self.av_info.geometry = geometry.clone(); let pixel_format = match self.video_pixel_format { format::Pixel::RGB555 => PixelFormat::ARGB1555, format::Pixel::RGB32 => PixelFormat::ARGB8888, @@ -475,7 +497,7 @@ impl retro::wrapper::Handler for MyEmulator { _ => unimplemented!(), }; self.video_filter = video_filter(&self.video_encoder, &self.av_info, pixel_format).unwrap(); - true + Some(true) } @@ -488,11 +510,11 @@ impl retro::wrapper::Handler for MyEmulator { } } - fn set_variables(&mut self, variables: Vec) -> bool { + fn set_variables(&mut self, variables: &Vec) -> Option { for v in variables { eprintln!("{:?}", v); } - true + Some(true) } fn log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) { @@ -511,12 +533,15 @@ struct Opt { /// Recorded video to write. #[structopt(short, long, parse(from_os_str))] video: PathBuf, + /// Save state to load at startup. + #[structopt(long, parse(from_os_str))] + state: Option, /// System directory, often containing BIOS files #[structopt(short, long, parse(from_os_str))] system: Option, } -fn main() -> Fallible<()> { +fn main() -> Result<(), Box> { let opt: Opt = Opt::from_args(); ffmpeg::log::set_level(ffmpeg::log::Level::Trace); ffmpeg::init().unwrap(); @@ -535,6 +560,10 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; + if let Some(state) = opt.state { + emu.unserialize(state)?; + } + //for frame in 0..60*10 { for frame in 0..800 { eprintln!("🖼️ frame: {}", frame); @@ -542,7 +571,7 @@ fn main() -> Fallible<()> { } let mut packet = Packet::empty(); - eprintln!("flushed: {:?}", emu.video_encoder.flush(&mut packet).unwrap()); + eprintln!("flushed: {:?}", emu.video_encoder.flush(&mut packet)?); emu.end(); //octx.write_trailer().unwrap(); diff --git a/examples/multifunction_emulator.rs b/examples/multifunction_emulator.rs new file mode 100644 index 0000000..3526331 --- /dev/null +++ b/examples/multifunction_emulator.rs @@ -0,0 +1,66 @@ +extern crate crossbeam_channel; +extern crate ferretro; +extern crate ffmpeg_next as ffmpeg; +extern crate sdl2; + +use std::path::PathBuf; +use structopt::StructOpt; + +use ferretro::prelude::*; + +use ferretro::components::provided::{ + ffmpeg::FfmpegComponent, + sdl2::Sdl2Component, + stdlib::{PathBufComponent, StderrLogComponent}, +}; +use ferretro::components::ControlFlow; +use ferretro::components::provided::stdlib::StderrSysInfoLogComponent; + +#[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, + /// Save state to load at startup. + #[structopt(long, parse(from_os_str))] + state: Option, + /// System directory, often containing BIOS files + #[structopt(short, long, parse(from_os_str))] + system: Option, + /// Recorded video to write. + #[structopt(short, long, parse(from_os_str))] + video: Option, +} + +pub fn main() { + let opt: Opt = Opt::from_args(); + let mut emu = RetroComponentBase::new(&opt.core); + let sdl2_comp = Sdl2Component::new(emu.libretro_core()); + emu.register_component(sdl2_comp); + emu.register_component(StderrLogComponent::default()); + emu.register_component(StderrSysInfoLogComponent::default()); + emu.register_component(PathBufComponent { + sys_path: opt.system.clone(), + libretro_path: Some(opt.core.to_path_buf()), + core_assets_path: None, + save_path: Some(std::env::temp_dir()), + }); + if let Some(video) = opt.video { + ffmpeg::log::set_level(ffmpeg::log::Level::Info); + ffmpeg::init().unwrap(); + let ffmpeg_comp = FfmpegComponent::new(emu.libretro_core(), video); + emu.register_component(ffmpeg_comp); + } + emu.load_game(&opt.rom).unwrap(); + if let Some(state) = opt.state { + emu.unserialize_path(state).unwrap(); + } + let mut frame = 0; + while let ControlFlow::Continue = emu.run() { + frame += 1; + } + eprintln!("Ran for {} frames.", frame); +} diff --git a/examples/sdl2_emulator.rs b/examples/sdl2_emulator.rs index 293dc90..b714222 100644 --- a/examples/sdl2_emulator.rs +++ b/examples/sdl2_emulator.rs @@ -135,7 +135,7 @@ impl MyEmulator { pin_emu } - pub fn run(&mut self) { + pub fn run_loop(&mut self) { self.audio_device.resume(); let mut event_pump = self.sdl_context.event_pump().unwrap(); 'running: loop { @@ -167,7 +167,7 @@ impl MyEmulator { } } - pub fn load_game(&self, rom: impl AsRef) { + pub fn load_game(&mut self, rom: impl AsRef) { let path = rom.as_ref(); let mut data = None; let mut v = Vec::new(); @@ -204,11 +204,13 @@ impl Drop for MyEmulator { } } -impl retro::wrapper::Handler for MyEmulator { +impl retro::wrapper::LibretroWrapperAccess for MyEmulator { fn libretro_core(&mut self) -> &mut LibretroWrapper { &mut self.retro } +} +impl retro::wrapper::RetroCallbacks for MyEmulator { fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { let rect = Rect::new(0, 0, width, height); @@ -262,19 +264,17 @@ impl retro::wrapper::Handler for MyEmulator { } } - 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 { + fn set_pixel_format(&mut self, pix_fmt: retro::ffi::PixelFormat) -> Option { 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 + Some(true) } fn get_variable(&mut self, key: &str) -> Option { match key { @@ -285,11 +285,11 @@ impl retro::wrapper::Handler for MyEmulator { } } - fn set_variables(&mut self, variables: Vec) -> bool { + fn set_variables(&mut self, variables: &Vec) -> Option { for v in variables { eprintln!("{:?}", v); } - true + Some(true) } fn get_libretro_path(&mut self) -> Option { @@ -305,18 +305,18 @@ impl retro::wrapper::Handler for MyEmulator { 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_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { + self.set_geometry(&av_info.geometry); + self.av_info = av_info.clone(); + Some(true) } - fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { + fn set_subsystem_info(&mut self, subsystem_info: &Vec) -> Option { println!("subsystem info: {:?}", subsystem_info); - true + Some(true) } - fn set_controller_info(&mut self, controller_info: Vec) -> bool { + fn set_controller_info(&mut self, controller_info: &Vec) -> Option { for ci in controller_info { // so we can have analog support in beetle/mednafen saturn if ci.name.as_str() == "3D Control Pad" { @@ -324,21 +324,21 @@ impl retro::wrapper::Handler for MyEmulator { break; } } - true + Some(true) } - fn set_input_descriptors(&mut self, descriptors: Vec) -> bool { + fn set_input_descriptors(&mut self, descriptors: &Vec) -> Option { for id in descriptors { println!("{:?}", id); } - true + Some(true) } - fn set_geometry(&mut self, geom: GameGeometry) -> bool { + fn set_geometry(&mut self, geom: &GameGeometry) -> Option { 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 + self.av_info.geometry = geom.clone(); + Some(true) } fn log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) { @@ -363,13 +363,11 @@ impl AudioCallback for MySdlAudio { } } -pub fn main() -> failure::Fallible<()> { +pub fn main() { let opt: Opt = Opt::from_args(); let mut emu = MyEmulator::new(&opt.core, &opt.system); emu.load_game(&opt.rom); - emu.run(); - - Ok(()) + emu.run_loop(); } #[derive(StructOpt)] diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..d74e63a --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,502 @@ +pub mod provided; + +use crate::prelude::*; +use crate::retro::ffi::*; +use std::os::raw::c_uint; +use std::path::{PathBuf, Path}; +use std::pin::Pin; +use std::io::Read; + +pub struct RetroComponentBase { + retro: LibretroWrapper, + libretro_path: PathBuf, + // TODO: control when things get added to this. + // probably shouldn't be after load_game? + // unless we invent a way to play back the latest set_pixel_format etc. metadata required. + components: Vec>, + + // replaying env calls for late-added components + cached_rom_path: Option, + cached_pixel_format: Option, + cached_input_descriptors: Option>, + cached_hw_render_callback: Option, + cached_variables: Option>, + cached_support_no_game: Option, + cached_system_av_info: Option, + cached_subsystem_info: Option>, + cached_controller_info: Option>, + cached_memory_map: Option, + cached_geometry: Option, +} + +// TODO: replace with std::ops::ControlFlow when it becomes stable +pub enum ControlFlow { + Continue, + Break, +} + +pub type Result = std::result::Result>; + +#[rustfmt::skip] +#[allow(unused_variables)] +pub trait RetroComponent: RetroCallbacks { + fn pre_run(&mut self, retro: &mut LibretroWrapper) -> ControlFlow { ControlFlow::Continue } + fn post_run(&mut self, retro: &mut LibretroWrapper) -> ControlFlow { ControlFlow::Continue } + fn pre_load_game(&mut self, retro: &mut LibretroWrapper, rom: &Path) -> Result<()> { Ok(()) } + fn post_load_game(&mut self, retro: &mut LibretroWrapper, rom: &Path) -> Result<()> { Ok(()) } +} + +impl RetroComponentBase { + // TODO: constructor & wrapper that uses a statically linked libretro? + pub fn new(core_path: impl AsRef) -> Pin> { + let lib = libloading::Library::new(core_path.as_ref()).unwrap(); + let raw_retro = crate::retro::loading::LibretroApi::from_library(lib).unwrap(); + let retro = LibretroWrapper::from(raw_retro); + + let emu = RetroComponentBase { + retro, + libretro_path: core_path.as_ref().to_path_buf(), + components: Vec::new(), + cached_rom_path: None, + cached_pixel_format: None, + cached_input_descriptors: None, + cached_hw_render_callback: None, + cached_variables: None, + cached_support_no_game: None, + cached_system_av_info: None, + cached_subsystem_info: None, + cached_controller_info: None, + cached_memory_map: None, + cached_geometry: None + }; + + let mut pin_emu = Box::pin(emu); + crate::retro::wrapper::set_handler(pin_emu.as_mut()); + pin_emu.retro.init(); + pin_emu + } + + pub fn register_component(&mut self, comp: T) -> Option<()> // TODO: Result + where T: RetroComponent + { + // TODO: match comp.schedule { BeforeInit, BeforeLoad, BeforeFirstRun, Anytime } + let mut comp = Box::new(comp); + if let Some(cached) = &self.cached_pixel_format { + if let Some(false) = comp.set_pixel_format(*cached) { + // TODO: error, and propagate this pattern downward + } + } + if let Some(cached) = &self.cached_input_descriptors { + comp.set_input_descriptors(cached); + } + if let Some(cached) = &self.cached_hw_render_callback { + comp.set_hw_render(cached); + } + if let Some(cached) = &self.cached_variables { + comp.set_variables(cached); + } + if let Some(cached) = &self.cached_support_no_game { + comp.set_support_no_game(*cached); + } + if let Some(cached) = &self.cached_system_av_info { + comp.set_system_av_info(cached); + } + if let Some(cached) = &self.cached_subsystem_info { + comp.set_subsystem_info(cached); + } + if let Some(cached) = &self.cached_controller_info { + comp.set_controller_info(cached); + } + if let Some(cached) = &self.cached_memory_map { + comp.set_memory_maps(cached); + } + if let Some(cached) = &self.cached_geometry { + comp.set_geometry(cached); + } + + if let Some(cached) = &self.cached_rom_path { + comp.post_load_game(&mut self.retro, &cached); + } + + self.components.push(comp); + + Some(()) + } + + pub fn load_game(&mut self, rom: impl AsRef) -> Result<()> { + let path = rom.as_ref(); + self.cached_rom_path = Some(path.to_path_buf()); + + for comp in &mut self.components { + comp.pre_load_game(&mut self.retro, path)?; + } + + let mut data = None; + let mut v = Vec::new(); + if !self.retro.get_system_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)?; + + for comp in &mut self.components { + comp.post_load_game(&mut self.retro, path)?; + } + + Ok(()) + } + + pub fn run(&mut self) -> ControlFlow { + for comp in &mut self.components { + if let ControlFlow::Break = comp.pre_run(&mut self.retro) { + return ControlFlow::Break; + } + } + self.retro.run(); + for comp in &mut self.components { + if let ControlFlow::Break = comp.post_run(&mut self.retro) { + return ControlFlow::Break; + } + } + ControlFlow::Continue + } + + pub fn unserialize_path(&mut self, state: impl AsRef) -> Result<()> { + let path = state.as_ref(); + let mut v = Vec::new(); + if let Ok(mut f) = std::fs::File::open(path) { + if f.read_to_end(&mut v).is_ok(){ + return self.unserialize_buf(v); + } + } + Err("Couldn't read file to unserialize".into()) + } + + pub fn unserialize_buf(&mut self, data: impl AsRef<[u8]>) -> Result<()> { + self.retro.unserialize(data.as_ref()) + } +} + +impl LibretroWrapperAccess for RetroComponentBase { + fn libretro_core(&mut self) -> &mut LibretroWrapper { + &mut self.retro + } +} + +impl RetroCallbacks for RetroComponentBase { + fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) { + for comp in &mut self.components { + comp.video_refresh(data, width, height, pitch); + } + } + + fn video_refresh_dupe(&mut self, width: c_uint, height: c_uint, pitch: c_uint) { + for comp in &mut self.components { + comp.video_refresh_dupe(width, height, pitch); + } + } + + fn audio_sample(&mut self, left: i16, right: i16) { + for comp in &mut self.components { + comp.audio_sample(left, right); + } + } + + fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { + self.components.iter_mut() + .map(|comp| comp.audio_sample_batch(stereo_pcm)) + .max() + .unwrap_or_default() + } + + fn input_poll(&mut self) { + for comp in &mut self.components { + comp.input_poll(); + } + } + + fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { + self.components.iter_mut() + .map(|comp| comp.input_state(port, device, index)) + .filter(|x| *x != 0) + // TODO: is this really the semantic we want? + .last() + .unwrap_or_default() + } + + fn set_rotation(&mut self, rotation: EnvRotation) -> Option { + self.components.iter_mut() + .map(|comp| comp.set_rotation(rotation)) + .flatten() + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + .into() + } + + fn get_overscan(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_overscan()) + .fold(None, |x, y| match (x, y) { + (Some(a), Some(b)) => Some(a || b), + (Some(a), None) | (None, Some(a)) => Some(a), + (None, None) => None, + }) + } + + fn set_message(&mut self, message: &Message) -> Option { + self.components.iter_mut() + .map(|comp| comp.set_message(message)) + .flatten() + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + .into() + } + + fn shutdown(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.shutdown()) + .flatten() + .all(|x| x) + .into() + } + + fn set_performance_level(&mut self, level: c_uint) -> Option { + self.components.iter_mut() + .map(|comp| comp.set_performance_level(level)) + .flatten() + .all(|x| x) + .into() + } + + fn get_system_directory(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_system_directory()) + .flatten() + .next() + } + + fn set_pixel_format(&mut self, format: PixelFormat) -> Option { + self.cached_pixel_format = Some(format); + self.components.iter_mut() + .map(|comp| comp.set_pixel_format(format)) + .flatten() + .all(|x| x) + .into() + } + + fn set_input_descriptors(&mut self, input_descriptors: &Vec) -> Option { + self.cached_input_descriptors = Some(input_descriptors.to_vec()); + self.components.iter_mut() + .map(|comp| comp.set_input_descriptors(input_descriptors)) + .flatten() + .fold(false, |x, y| x || y) + .into() + } + + fn set_hw_render(&mut self, hw_render_callback: &HwRenderCallback) -> Option { + self.cached_hw_render_callback = Some(hw_render_callback.to_owned()); + self.components.iter_mut() + .map(|comp| comp.set_hw_render(hw_render_callback)) + .flatten() + .all(|x| x) + .into() + } + + fn get_variable(&mut self, key: &str) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_variable(key)) + .flatten() + .next() + } + + fn set_variables(&mut self, variables: &Vec) -> Option { + self.cached_variables = Some(variables.to_vec()); + self.components.iter_mut() + .map(|comp| comp.set_variables(variables)) + .flatten() + .fold(false, |x, y| x || y) + .into() + } + + fn get_variable_update(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_variable_update()) + .flatten() + .reduce(|x, y| x || y) + } + + fn set_support_no_game(&mut self, supports_no_game: bool) -> Option { + self.cached_support_no_game = Some(supports_no_game); + self.components.iter_mut() + .map(|comp| comp.set_support_no_game(supports_no_game)) + .flatten() + .all(|x| x) + .into() + } + + // allow it to be overridden, but we *do* have the answer at this level since we loaded it. + fn get_libretro_path(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_libretro_path()) + .flatten() + .next() + .unwrap_or_else(|| self.libretro_path.clone()) + .into() + } + + fn get_input_device_capabilities(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_input_device_capabilities()) + .flatten() + .reduce(|x, y| x & y) + } + + fn get_core_assets_directory(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_core_assets_directory()) + .flatten() + .next() + } + + fn get_save_directory(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_save_directory()) + .flatten() + .next() + } + + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> Option { + self.cached_system_av_info = Some(system_av_info.to_owned()); + self.cached_geometry = Some(system_av_info.geometry.clone()); + self.components.iter_mut() + .map(|comp| comp.set_system_av_info(system_av_info)) + .flatten() + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + .into() + } + + fn set_subsystem_info(&mut self, subsystem_info: &Vec) -> Option { + self.cached_subsystem_info = Some(subsystem_info.to_vec()); + self.components.iter_mut() + .map(|comp| comp.set_subsystem_info(subsystem_info)) + .flatten() + .all(|x| x) + .into() + } + + fn set_controller_info(&mut self, controller_info: &Vec) -> Option { + self.cached_controller_info = Some(controller_info.to_vec()); + self.components.iter_mut() + .map(|comp| comp.set_controller_info(controller_info)) + .flatten() + .all(|x| x) + .into() + } + + fn set_memory_maps(&mut self, memory_map: &MemoryMap) -> Option { + self.cached_memory_map = Some(memory_map.to_owned()); + self.components.iter_mut() + .map(|comp| comp.set_memory_maps(memory_map)) + .flatten() + .all(|x| x) + .into() + } + + fn set_geometry(&mut self, game_geometry: &GameGeometry) -> Option { + self.cached_geometry = Some(game_geometry.to_owned()); + self.components.iter_mut() + .map(|comp| comp.set_geometry(game_geometry)) + .flatten() + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + .into() + } + + fn get_username(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_username()) + .flatten() + .next() + } + + fn get_language(&mut self) -> Option { + self.components.iter_mut() + .map(|comp| comp.get_language()) + .flatten() + .next() + } + + fn log_print(&mut self, level: LogLevel, msg: &str) { + for comp in &mut self.components { + comp.log_print(level, msg); + } + } + + fn set_rumble_state(&mut self, port: c_uint, effect: RumbleEffect, strength: u16) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_rumble_state(port, effect, strength)) + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + } + + fn perf_get_time_usec_cb(&mut self) -> Time { + self.components.first_mut() + .map(|comp| comp.perf_get_time_usec_cb()) + .unwrap_or_default() + } + + fn perf_get_counter_cb(&mut self) -> PerfTick { + self.components.first_mut() + .map(|comp| comp.perf_get_counter_cb()) + .unwrap_or_default() + } + + fn perf_get_cpu_features_cb(&mut self) -> u64 { + self.components.first_mut() + .map(|comp| comp.perf_get_cpu_features_cb()) + .unwrap_or_default() + } + + fn perf_log_cb(&mut self) { + if let Some(comp) = self.components.first_mut() { + comp.perf_log_cb() + } + } + + fn perf_register_cb(&mut self, counter: &mut PerfCounter) { + if let Some(comp) = self.components.first_mut() { + comp.perf_register_cb(counter) + } + } + + fn perf_start_cb(&mut self, counter: &mut PerfCounter) { + if let Some(comp) = self.components.first_mut() { + comp.perf_start_cb(counter) + } + } + + fn perf_stop_cb(&mut self, counter: &mut PerfCounter) { + if let Some(comp) = self.components.first_mut() { + comp.perf_stop_cb(counter) + } + } + + fn set_sensor_state(&mut self, port: c_uint, action: SensorAction, rate: c_uint) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_sensor_state(port, action, rate)) + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + } + + fn get_sensor_input(&mut self, port: c_uint, id: c_uint) -> f32 { + self.components.iter_mut() + .map(|comp| comp.get_sensor_input(port, id)) + .filter(|x| *x != 0.0) + .last() + .unwrap_or_default() + } +} + +impl Drop for RetroComponentBase { + fn drop(&mut self) { + crate::retro::wrapper::unset_handler(); + } +} diff --git a/src/components/provided/ffmpeg.rs b/src/components/provided/ffmpeg.rs new file mode 100644 index 0000000..3b3e8ea --- /dev/null +++ b/src/components/provided/ffmpeg.rs @@ -0,0 +1,473 @@ +extern crate ffmpeg_next as ffmpeg; + +use std::collections::VecDeque; +use std::error::Error; +use std::path::Path; + +use crate::prelude::*; + +use ffmpeg::{ChannelLayout, Packet, filter, format, frame, media}; +use ffmpeg::util::rational::Rational; +use crate::components::ControlFlow; + +enum EncoderToWriteFrom { + Video, + Audio, +} + +pub struct FfmpegComponent { + av_info: SystemAvInfo, + audio_buf: Vec<(i16, i16)>, + video_pixel_format: format::Pixel, + prev_video_frame: Option, + video_frames: VecDeque, + video_encoder: ffmpeg::encoder::Video, + audio_encoder: ffmpeg::encoder::Audio, + video_filter: filter::Graph, + audio_filter: filter::Graph, + frame_properties_locked: bool, + octx: ffmpeg::format::context::Output, + frame: i64, +} + +fn video_filter( + video_encoder: &ffmpeg::encoder::video::Video, + av_info: &SystemAvInfo, + pix_fmt: PixelFormat, +) -> Result { + let mut vfilter = filter::Graph::new(); + let pix_fmt = match pix_fmt { + PixelFormat::ARGB1555 => if cfg!(target_endian = "big") { "rgb555be" } else { "rgb555le" }, + PixelFormat::ARGB8888 => "argb", + PixelFormat::RGB565 => if cfg!(target_endian = "big") { "rgb565be" } else { "rgb565le" }, + }; + let pixel_aspect = av_info.geometry.aspect_ratio / (av_info.geometry.base_width as f32 / av_info.geometry.base_height as f32); + let fps = if av_info.timing.fps == 0.0 { 60.0 } else { av_info.timing.fps }; + let args = format!( + "width={}:height={}:pix_fmt={}:frame_rate={}:pixel_aspect={}:time_base=1/{}", + av_info.geometry.base_width, + av_info.geometry.base_height, + pix_fmt, + fps, + pixel_aspect, + fps, + ); + eprintln!("🎥 filter args: {}", args); + vfilter.add(&filter::find("buffer").unwrap(), "in", &args)?; + //scale? + vfilter.add(&filter::find("buffersink").unwrap(), "out", "")?; + + { + let mut out = vfilter.get("out").unwrap(); + out.set_pixel_format(video_encoder.format()); + } + + vfilter.output("in", 0)? + .input("out", 0)? + .parse("null")?; // passthrough filter for video + + vfilter.validate()?; + // human-readable filter graph + eprintln!("{}", vfilter.dump()); + + Ok(vfilter) +} + +fn audio_filter( + audio_encoder: &ffmpeg::codec::encoder::Audio, + sample_rate: f64, +) -> Result { + let mut afilter = filter::Graph::new(); + let sample_rate = if sample_rate == 0.0 { 32040.0 } else { sample_rate }; + let args = format!("sample_rate={}:sample_fmt=s16:channel_layout=stereo:time_base=1/60", sample_rate); + eprintln!("🔊 filter args: {}", args); + afilter.add(&filter::find("abuffer").unwrap(), "in", &args)?; + //aresample? + afilter.add(&filter::find("abuffersink").unwrap(), "out", "")?; + + { + let mut out = afilter.get("out").unwrap(); + out.set_sample_format(audio_encoder.format()); + out.set_channel_layout(audio_encoder.channel_layout()); + out.set_sample_rate(audio_encoder.rate()); + } + + afilter.output("in", 0)? + .input("out", 0)? + .parse("anull")?; + afilter.validate()?; + // human-readable filter graph + eprintln!("{}", afilter.dump()); + + if let Some(codec) = audio_encoder.codec() { + if !codec + .capabilities() + .contains(ffmpeg::codec::capabilities::Capabilities::VARIABLE_FRAME_SIZE) + { + eprintln!("setting constant frame size {}", audio_encoder.frame_size()); + afilter + .get("out") + .unwrap() + .sink() + .set_frame_size(audio_encoder.frame_size()); + } + } + + Ok(afilter) +} + +impl RetroComponent for FfmpegComponent { + fn pre_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { + self.frame += 1; + ControlFlow::Continue + } + + fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { + match self.video_frames.pop_front() { + Some(mut vframe) => { + vframe.set_pts(Some(self.frame)); + eprintln!("🎞 queue frame pts {:?}", vframe.pts()); + self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); + let mut filtered_vframe = frame::Video::empty(); + + loop { + match self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe) { + Ok(..) => { + eprintln!("🎥 Got filtered video frame {}x{} pts {:?}", filtered_vframe.width(), filtered_vframe.height(), filtered_vframe.pts()); + if self.video_filter.get("in").unwrap().source().failed_requests() > 0 { + println!("🎥 failed to put filter input frame"); + } + //filtered_vframe.set_pts(Some(frame)); + self.video_encoder.send_frame(&filtered_vframe).unwrap(); + + self.receive_and_write_packets(EncoderToWriteFrom::Video); + }, + Err(e) => { + eprintln!("Error getting filtered video frame: {:?}", e); + break; + } + } + } + + let mut aframe = frame::Audio::new( + format::Sample::I16(format::sample::Type::Packed), + self.audio_buf.len(), + ChannelLayout::STEREO + ); + if aframe.planes() > 0 { + aframe.set_channels(2); + aframe.set_rate(44100); + aframe.set_pts(Some(self.frame)); + let aplane: &mut [(i16, i16)] = aframe.plane_mut(0); + eprintln!("Audio buffer length {} -> {}", self.audio_buf.len(), aplane.len()); + aplane.copy_from_slice(self.audio_buf.as_ref()); + //eprintln!("src: {:?}, dest: {:?}", self.audio_buf, aplane); + self.audio_buf.clear(); + + eprintln!("frame audio: {:?}", aframe); + + eprintln!("🎞 queue frame pts {:?}", aframe.pts()); + self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); + + let mut filtered_aframe = frame::Audio::empty(); + loop { + match self.audio_filter.get("out").unwrap().sink().frame(&mut filtered_aframe) { + Ok(..) => { + eprintln!("🔊 Got filtered audio frame {:?} pts {:?}", filtered_aframe, filtered_aframe.pts()); + if self.audio_filter.get("in").unwrap().source().failed_requests() > 0 { + println!("🎥 failed to put filter input frame"); + } + //let faplane: &[f32] = filtered_aframe.plane(0); + //filtered_aframe.set_pts(Some(frame)); + + self.audio_encoder.send_frame(&filtered_aframe).unwrap(); + self.receive_and_write_packets(EncoderToWriteFrom::Audio); + }, + Err(e) => { + eprintln!("Error getting filtered audio frame: {:?}", e); + break; + } + } + } + } + }, + None => println!("Video not ready during frame {}", self.frame) + } + ControlFlow::Continue + } + + fn post_load_game(&mut self, _retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box> { + self.frame_properties_locked = true; + Ok(()) + } +} + +impl FfmpegComponent { + pub fn new( + retro: &LibretroWrapper, + video_path: impl AsRef, + ) -> Self { + let mut octx = format::output(&video_path).unwrap(); + + let mut av_info = retro.get_system_av_info(); + + let fps_int = av_info.timing.fps.round() as i32; + let fps_int = if fps_int == 0 { 60 } else { fps_int }; + + let detected_vcodec = octx.format().codec(&video_path, media::Type::Video); + //let detected_acodec = octx.format().codec(&video_path, media::Type::Audio); +let wavname = Path::new("out.wav"); + let detected_acodec = octx.format().codec(&wavname, media::Type::Audio); + + let vcodec = ffmpeg::encoder::find(detected_vcodec).unwrap().video().unwrap(); + let acodec = ffmpeg::encoder::find(detected_acodec).unwrap().audio().unwrap(); + + let mut video_output = octx.add_stream(vcodec).unwrap(); + video_output.set_time_base(Rational::new(1, 60)); + let mut video_encoder = video_output.codec().encoder().video().unwrap(); + + video_encoder.set_bit_rate(2560000); + video_encoder.set_format(video_encoder.codec().unwrap().video().unwrap().formats().unwrap().nth(0).unwrap()); + + video_encoder.set_time_base(Rational::new(1, 60)); + video_encoder.set_frame_rate(Some(Rational::new(fps_int, 1))); + + //video_encoder.set_frame_rate(av_info.timing.fps.into()); + + if av_info.geometry.base_height == 0 && av_info.geometry.base_width == 0 { + av_info.geometry.base_width = 320; + av_info.geometry.base_height = 224; + av_info.geometry.aspect_ratio = 4.33; + } + if av_info.timing.sample_rate == 0.0 { + av_info.timing.sample_rate = 44100.0; + } + video_encoder.set_width(av_info.geometry.base_width); + video_encoder.set_height(av_info.geometry.base_height); + //video_encoder.set_aspect_ratio(av_info.geometry.aspect_ratio as f64); + + let pix_fmt = PixelFormat::ARGB1555; // temporary until env call is made + let video_filter = video_filter(&video_encoder, &av_info, pix_fmt).unwrap(); + + let video_encoder = video_encoder.open_as(vcodec).unwrap(); + //video_output.set_parameters(&video_encoder); + + let mut audio_output = octx.add_stream(acodec).unwrap(); + let mut audio_encoder = audio_output.codec().encoder().audio().unwrap(); + + //let mut video_encoder = octx.add_stream(vcodec).unwrap().codec().encoder().video().unwrap(); + /* + let mut audio_output = octx.add_stream(acodec).unwrap(); + let mut audio_encoder = audio_output.codec().encoder().audio().unwrap(); + */ + /* + retroarch inits +static bool ffmpeg_init_config(struct ff_config_param *params, + if (!ffmpeg_init_muxer_pre(handle)) + if (!ffmpeg_init_video(handle)) + av_frame_alloc + */ + + + audio_encoder.set_bit_rate(640000); + audio_encoder.set_max_bit_rate(990000); + + //audio_encoder.set_rate(44100); + audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); + audio_encoder.set_channels(2); + audio_encoder.set_channel_layout(ChannelLayout::STEREO); + audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); + audio_encoder.set_time_base(Rational::new(1, 60)); + audio_output.set_time_base(Rational::new(1, 60)); + + let audio_encoder = audio_encoder.open_as(acodec).unwrap(); + //audio_output.set_parameters(&audio_encoder); + + let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); + + //audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); + + octx.write_header().unwrap(); + ffmpeg::format::context::output::dump(&octx, 0, None); + + let mut comp = FfmpegComponent { + av_info: av_info.clone(), + audio_buf: Default::default(), + video_pixel_format: format::Pixel::RGB555, + prev_video_frame: None, + video_frames: Default::default(), + video_encoder, + audio_encoder, + video_filter, + audio_filter, + frame_properties_locked: false, + octx, + frame: 0 + }; + comp.set_system_av_info(&av_info); + comp + } + + fn receive_and_write_packets(&mut self, encoder: EncoderToWriteFrom) + { + let stream_index = match encoder { + EncoderToWriteFrom::Video => 0, + EncoderToWriteFrom::Audio => 1, + }; + let mut encoded_packet = ffmpeg::Packet::empty(); + loop + { + match match encoder { + EncoderToWriteFrom::Video => self.video_encoder.receive_packet(&mut encoded_packet), + EncoderToWriteFrom::Audio => self.audio_encoder.receive_packet(&mut encoded_packet), + } { + Ok(..) => { + //if encoded_packet.size() > 0 { + encoded_packet.set_stream(stream_index); + eprintln!("📦 Writing packet, pts {:?} dts {:?} size {}", encoded_packet.pts(), encoded_packet.dts(), encoded_packet.size()); + if stream_index == 0 { + encoded_packet.rescale_ts(Rational(1, 60), self.octx.stream(stream_index).unwrap().time_base()); + } + eprintln!("📦 rescaled , pts {:?} dts {:?} size {}", encoded_packet.pts(), encoded_packet.dts(), encoded_packet.size()); + + match encoded_packet.write_interleaved(&mut self.octx) { + Ok(..) => eprintln!("Write OK"), + Err(e) => eprintln!("Error writing: {}", e), + } + //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA + //} + //else { + //eprintln!("Did not try to write 0-length packet"); + //} + }, + Err(e) => { + eprintln!("Error writing packet: {:?}", e); + break; + } + } + } + } + + pub fn end(&mut self) { + let mut packet = Packet::empty(); + eprintln!("flushed: {:?}", self.video_encoder.flush(&mut packet).unwrap()); + + self.video_encoder.send_eof().unwrap(); + self.receive_and_write_packets(EncoderToWriteFrom::Video); + self.audio_encoder.send_eof().unwrap(); + self.receive_and_write_packets(EncoderToWriteFrom::Audio); + self.octx.write_trailer().unwrap(); + } +} + +impl Drop for FfmpegComponent { + fn drop(&mut self) { + self.end(); + } +} + +impl RetroCallbacks for FfmpegComponent { + fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { + let mut vframe = frame::Video::new(self.video_pixel_format, width, height); + + let stride = vframe.stride(0); + let pitch = pitch as usize; + + let vplane = vframe.data_mut(0); + if data.len() == vplane.len() && pitch == stride { + vplane.copy_from_slice(&data); + } else { + for y in 0..(height as usize) { + let ffbegin = y * stride; + let lrbegin = y * pitch; + let min = usize::min(stride, pitch); + vplane[ffbegin..(ffbegin + min)].copy_from_slice( + &data[lrbegin..(lrbegin + min)] + ); + } + } + + //vframe.set_pts(Some(self.frame as i64)); + + self.prev_video_frame.replace(vframe.clone()); + self.video_frames.push_back(vframe); + } + + fn video_refresh_dupe(&mut self, width: u32, height: u32, _pitch: u32) { + if let Some(frame) = &self.prev_video_frame { + self.video_frames.push_back(frame.clone()); + } else { + let vframe = frame::Video::new(self.video_pixel_format, width, height); + self.video_frames.push_back(vframe); + } + } + + fn audio_sample(&mut self, left: i16, right: i16) { + self.audio_buf.push((left, right)); + } + + fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { + let left_iter = stereo_pcm.iter().step_by(2).cloned(); + let right_iter = stereo_pcm.iter().skip(1).step_by(2).cloned(); + self.audio_buf.extend(Iterator::zip(left_iter, right_iter)); + stereo_pcm.len() + } + + fn set_pixel_format(&mut self, format: PixelFormat) -> Option { + if self.frame_properties_locked { + return Some(false); + } + + self.video_pixel_format = match format { + PixelFormat::ARGB1555 => format::Pixel::RGB555, + PixelFormat::ARGB8888 => format::Pixel::RGB32, + PixelFormat::RGB565 => format::Pixel::RGB565, + }; + self.video_filter = video_filter(&self.video_encoder, &self.av_info, format).unwrap(); + Some(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_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> Option { + if self.frame_properties_locked { + return Some(false); + } + + //self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); + //self.video_encoder.set_time_base(Rational::new(1, 60)); + //self.video_encoder.set_frame_rate(Some(Rational::new(1, 60))); + if system_av_info.timing.sample_rate.round() as i32 > 0 { + self.audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); + } + self.av_info.timing = system_av_info.timing.clone(); + self.set_geometry(&system_av_info.geometry); + Some(true) + } + + fn set_geometry(&mut self, geometry: &GameGeometry) -> Option { + if self.frame_properties_locked { + return Some(false); + } + + self.video_encoder.set_width(geometry.base_width); + self.video_encoder.set_height(geometry.base_height); + //self.video_encoder.set_aspect_ratio(geometry.aspect_ratio as f64); + self.av_info.geometry = geometry.clone(); + let pixel_format = match self.video_pixel_format { + format::Pixel::RGB555 => PixelFormat::ARGB1555, + format::Pixel::RGB32 => PixelFormat::ARGB8888, + format::Pixel::RGB565 => PixelFormat::RGB565, + _ => unimplemented!(), + }; + self.video_filter = video_filter(&self.video_encoder, &self.av_info, pixel_format).unwrap(); + Some(true) + } +} diff --git a/src/components/provided/mod.rs b/src/components/provided/mod.rs new file mode 100644 index 0000000..41b9467 --- /dev/null +++ b/src/components/provided/mod.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "ffmpeg_comp")] +pub mod ffmpeg; + +#[cfg(feature = "sdl2_comp")] +pub mod sdl2; + +pub mod stdlib; diff --git a/src/components/provided/sdl2.rs b/src/components/provided/sdl2.rs new file mode 100644 index 0000000..5b21e05 --- /dev/null +++ b/src/components/provided/sdl2.rs @@ -0,0 +1,323 @@ +use std::error::Error; +use std::ffi::CStr; +use std::path::Path; +use std::time::{Duration, Instant}; + +use crate::prelude::*; +use crate::components::ControlFlow; + +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; + +// TODO: split up between video/audio/input! +pub struct Sdl2Component { + preferred_pad: Option, + + 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, + + // timing and events + frame_begin: Instant, + event_pump: sdl2::EventPump, +} + +impl RetroComponent for Sdl2Component { + fn pre_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { + self.frame_begin = Instant::now(); + + for event in self.event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => return ControlFlow::Break, + _ => {} + } + } + + ControlFlow::Continue + } + + fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { + // The rest of the game loop goes here... + 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(self.frame_begin.elapsed()) + .map(std::thread::sleep); + + + ControlFlow::Continue + } + + fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box> { + if let Some(device) = self.preferred_pad { + for port in 0..self.gamepads.len() as u32 { + retro.set_controller_port_device(port, device); + } + } + self.audio_device.resume(); + Ok(()) + } +} + +impl Sdl2Component { + pub fn new(retro: &LibretroWrapper) -> Self { + let sys_info = retro.get_system_info(); + let title = format!( + "{} - ferretro", + 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 { + eprintln!("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 event_pump = sdl_context.event_pump().unwrap(); + + Sdl2Component { + preferred_pad: None, + av_info, + _sdl_context: sdl_context, + canvas, + pixel_format, + audio_buffer: Default::default(), + audio_spec: audio_spec.unwrap(), + audio_device, + audio_sender, + gamepad_subsys, + gamepads, + frame_begin: Instant::now(), + event_pump, + } + } + + 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 RetroCallbacks for Sdl2Component { + 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 set_pixel_format(&mut self, pix_fmt: PixelFormat) -> Option { + self.pixel_format = match pix_fmt { + PixelFormat::ARGB1555 => sdl2::pixels::PixelFormatEnum::RGB555, + PixelFormat::ARGB8888 => sdl2::pixels::PixelFormatEnum::ARGB8888, + PixelFormat::RGB565 => sdl2::pixels::PixelFormatEnum::RGB565, + }; + Some(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 get_input_device_capabilities(&mut self) -> Option { + let bits = (1 << (DeviceType::Joypad as u32)) | (1 << (DeviceType::Analog as u32)); + Some(bits as u64) + } + + fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { + self.set_geometry(&av_info.geometry); + self.av_info = av_info.clone(); + Some(true) + } + + fn set_controller_info(&mut self, controller_info: &Vec) -> Option { + 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; + } + } + Some(true) + } + + fn set_geometry(&mut self, geom: &GameGeometry) -> Option { + 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.clone(); + Some(true) + } +} + +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()]); + } + } + } +} + +fn button_map(retro_button: &JoypadButton) -> Option