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 31cd8ff..04c7be3 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -7,7 +7,6 @@ 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}; @@ -383,7 +382,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, self.octx.write_trailer().unwrap(); } - pub fn unserialize(&mut self, state: impl AsRef) -> Fallible<()> { + 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) { @@ -391,7 +390,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, return self.retro.unserialize(v.as_ref()); } } - Err(failure::err_msg("Couldn't read file to unserialize")) + Err("Couldn't read file to unserialize".into()) } } @@ -452,9 +451,9 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { 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 { @@ -463,12 +462,12 @@ impl retro::wrapper::RetroCallbacks 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()); @@ -479,12 +478,12 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { } self.av_info.timing = system_av_info.timing.clone(); self.set_geometry(&system_av_info.geometry); - true + 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); @@ -498,7 +497,7 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { _ => unimplemented!(), }; self.video_filter = video_filter(&self.video_encoder, &self.av_info, pixel_format).unwrap(); - true + Some(true) } @@ -511,11 +510,11 @@ impl retro::wrapper::RetroCallbacks 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) { @@ -542,7 +541,7 @@ struct Opt { 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(); @@ -562,7 +561,7 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; if let Some(state) = opt.state { - emu.unserialize(state).unwrap(); + emu.unserialize(state)?; } //for frame in 0..60*10 { @@ -572,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 6493e9f..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 { @@ -268,13 +268,13 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { 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::RetroCallbacks 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::RetroCallbacks for MyEmulator { Some(std::env::temp_dir()) } - fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> bool { + fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { self.set_geometry(&av_info.geometry); self.av_info = av_info.clone(); - true + 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::RetroCallbacks 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.clone(); - true + 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 index 7a017b4..d74e63a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,17 +1,53 @@ -//pub mod ffmpeg; +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, - pub components: Vec>, + 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(); @@ -19,7 +55,19 @@ impl RetroComponentBase { 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); @@ -27,6 +75,109 @@ impl RetroComponentBase { 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 { @@ -76,10 +227,12 @@ impl RetroCallbacks for RetroComponentBase { .unwrap_or_default() } - fn set_rotation(&mut self, rotation: EnvRotation) -> bool { + 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 { @@ -92,22 +245,28 @@ impl RetroCallbacks for RetroComponentBase { }) } - fn set_message(&mut self, message: &Message) -> bool { + 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) -> bool { + 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) -> bool { + 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 { @@ -117,22 +276,31 @@ impl RetroCallbacks for RetroComponentBase { .next() } - fn set_pixel_format(&mut self, format: PixelFormat) -> bool { + 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) -> bool { + 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) -> bool { + 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 { @@ -142,10 +310,13 @@ impl RetroCallbacks for RetroComponentBase { .next() } - fn set_variables(&mut self, variables: &Vec) -> bool { + 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 { @@ -155,17 +326,23 @@ impl RetroCallbacks for RetroComponentBase { .reduce(|x, y| x || y) } - fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { + 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 { @@ -189,34 +366,50 @@ impl RetroCallbacks for RetroComponentBase { .next() } - fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { + 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) -> bool { + 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) -> bool { + 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) -> bool { + 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) -> bool { + 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 { @@ -301,3 +494,9 @@ impl RetroCallbacks for RetroComponentBase { .unwrap_or_default() } } + +impl Drop for RetroComponentBase { + fn drop(&mut self) { + crate::retro::wrapper::unset_handler(); + } +} diff --git a/src/components/ffmpeg.rs b/src/components/provided/ffmpeg.rs similarity index 84% rename from src/components/ffmpeg.rs rename to src/components/provided/ffmpeg.rs index 13893c7..3b3e8ea 100644 --- a/src/components/ffmpeg.rs +++ b/src/components/provided/ffmpeg.rs @@ -1,21 +1,21 @@ extern crate ffmpeg_next as ffmpeg; use std::collections::VecDeque; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::pin::Pin; - -use structopt::StructOpt; -use failure::Fallible; +use std::error::Error; +use std::path::Path; use crate::prelude::*; -use ffmpeg::{ChannelLayout, Packet, codec, filter, format, frame, media}; +use ffmpeg::{ChannelLayout, Packet, filter, format, frame, media}; use ffmpeg::util::rational::Rational; +use crate::components::ControlFlow; -struct MyEmulator { - retro: LibretroWrapper, - sys_info: SystemInfo, +enum EncoderToWriteFrom { + Video, + Audio, +} + +pub struct FfmpegComponent { av_info: SystemAvInfo, audio_buf: Vec<(i16, i16)>, video_pixel_format: format::Pixel, @@ -25,10 +25,9 @@ struct MyEmulator { audio_encoder: ffmpeg::encoder::Audio, video_filter: filter::Graph, audio_filter: filter::Graph, - sys_path: Option, frame_properties_locked: bool, octx: ffmpeg::format::context::Output, - frame: u64, + frame: i64, } fn video_filter( @@ -117,18 +116,99 @@ fn audio_filter( Ok(afilter) } -impl MyEmulator { - pub fn new( - core_path: impl AsRef, - sys_path: &Option>, - video_path: impl AsRef, - mut octx: ffmpeg::format::context::Output, - ) -> Pin> { - let lib = libloading::Library::new(core_path.as_ref()).unwrap(); - let raw_retro = retro::loading::LibretroApi::from_library(lib).unwrap(); - let retro = retro::wrapper::LibretroWrapper::from(raw_retro); +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 sys_info = retro.get_system_info(); let mut av_info = retro.get_system_av_info(); let fps_int = av_info.timing.fps.round() as i32; @@ -200,7 +280,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(); @@ -210,9 +290,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, octx.write_header().unwrap(); ffmpeg::format::context::output::dump(&octx, 0, None); - let emu = MyEmulator { - retro, - sys_info, + let mut comp = FfmpegComponent { av_info: av_info.clone(), audio_buf: Default::default(), video_pixel_format: format::Pixel::RGB555, @@ -222,17 +300,12 @@ static bool ffmpeg_init_config(struct ff_config_param *params, audio_encoder, video_filter, audio_filter, - sys_path: sys_path.as_ref().map(|x| x.as_ref().to_path_buf()), frame_properties_locked: false, octx, frame: 0 }; - - 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 + comp.set_system_av_info(&av_info); + comp } fn receive_and_write_packets(&mut self, encoder: EncoderToWriteFrom) @@ -275,128 +348,25 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } } - pub fn run(&mut self, frame: i64) { - self.frame += 1; - self.retro.run(); - - match self.video_frames.pop_front() { - Some(mut vframe) => { - vframe.set_pts(Some(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(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) - } - - - - } - - 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(); - } - pub fn end(&mut self) { - self.video_encoder.send_eof(); + 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(); + self.audio_encoder.send_eof().unwrap(); self.receive_and_write_packets(EncoderToWriteFrom::Audio); self.octx.write_trailer().unwrap(); } +} - pub fn unserialize(&mut self, state: impl AsRef) -> Fallible<()> { - 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(failure::err_msg("Couldn't read file to unserialize")) +impl Drop for FfmpegComponent { + fn drop(&mut self) { + self.end(); } } -impl retro::wrapper::LibretroWrapperAccess for MyEmulator { - fn libretro_core(&mut self) -> &mut LibretroWrapper { - &mut self.retro - } -} - -impl retro::wrapper::RetroCallbacks for MyEmulator { +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); @@ -443,13 +413,9 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { stereo_pcm.len() } - 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(false); } self.video_pixel_format = match format { @@ -458,12 +424,21 @@ impl retro::wrapper::RetroCallbacks 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 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 true; + return Some(false); } //self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); @@ -474,12 +449,12 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { } self.av_info.timing = system_av_info.timing.clone(); self.set_geometry(&system_av_info.geometry); - true + 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(false); } self.video_encoder.set_width(geometry.base_width); @@ -493,27 +468,6 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { _ => unimplemented!(), }; self.video_filter = video_filter(&self.video_encoder, &self.av_info, pixel_format).unwrap(); - 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 log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) { - eprint!("🕹ī¸ [{:?}] {}", level, msg); + 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