From dafe029b010ade1f35d4938e4d6d903320bb67f7 Mon Sep 17 00:00:00 2001 From: lif Date: Sun, 24 Nov 2019 01:14:22 -0800 Subject: [PATCH 01/36] WIP ffmpeg example, no real functionality yet --- examples/ffmpeg_recorder.rs | 351 ++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 examples/ffmpeg_recorder.rs diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs new file mode 100644 index 0000000..57eefe7 --- /dev/null +++ b/examples/ffmpeg_recorder.rs @@ -0,0 +1,351 @@ +extern crate ferretro; +extern crate ffmpeg4 as ffmpeg; + +use std::collections::VecDeque; +use std::env; +use std::path::{Path, PathBuf}; +use std::pin::Pin; + +use ferretro::retro; +use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo}; +use ferretro::retro::wrapper::LibretroWrapper; + +use ffmpeg::{codec, filter, format, frame, media, ChannelLayout}; +use ffmpeg::{rescale, Rescale}; + +struct MyEmulator { + retro: retro::wrapper::LibretroWrapper, + audio_buf: Vec<(i16, i16)>, + video_pixfmt: format::Pixel, + video_frames: VecDeque, + video_encoder: ffmpeg::encoder::Video, + audio_encoder: ffmpeg::encoder::Audio, + sys_path: Option, +} + +impl MyEmulator { + pub fn new(core_path: impl AsRef, sys_path: &Option>, ffstream: format::stream::StreamMut) -> Pin> { + let lib = libloading::Library::new(core_path).unwrap(); + let raw_retro = retro::loading::LibretroApi::from_library(lib).unwrap(); + let retro = retro::wrapper::LibretroWrapper::from(raw_retro); + + let mut av_info = retro.get_system_av_info(); + let video_pixfmt = format::Pixel::RGB555; + + let emu = MyEmulator { + retro, + audio_buf: Default::default(), + video_pixfmt, + video_frames: Default::default(), + video_encoder, + audio_encoder, + sys_path: sys_path.map(|x| x.as_ref().to_path_buf()), + }; + + let mut pin_emu = Box::pin(emu); + retro::wrapper::set_handler(pin_emu.as_mut()); + pin_emu.retro.init(); + pin_emu + } + + pub fn run(&mut self) { + self.retro.run(); + + let vframe = self.video_frames.pop_front().unwrap(); + let mut aframe = frame::Audio::new( + format::Sample::I16(format::sample::Type::Packed), + self.audio_buf.len(), + ChannelLayout::STEREO + ); + let aplane: &mut [(i16, i16)] = aframe.plane_mut(0); + aplane.copy_from_slice(self.audio_buf.as_ref()); + self.audio_buf.clear(); + + let mut out = ffmpeg::Packet::empty(); + self.video_encoder.encode(&vframe, &mut out); + self.audio_encoder.encode(&aframe, &mut out); + } +} + +impl retro::wrapper::Handler for MyEmulator { + fn libretro_core(&mut self) -> &mut LibretroWrapper { + &mut self.retro + } + + fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { + let mut vframe = frame::Video::new(self.video_pixfmt, 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); + let x = vplane[ffbegin..(ffbegin + min)].copy_from_slice( + &data[lrbegin..(lrbegin + min)] + ); + } + } + + 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) -> bool { + self.video_pixfmt = match format { + PixelFormat::ARGB1555 => format::Pixel::RGB555, + PixelFormat::ARGB8888 => format::Pixel::RGB32, + PixelFormat::RGB565 => format::Pixel::RGB565, + }; + true + } + + fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { + self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); + self.audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); + self.set_geometry(system_av_info.geometry); + true + } + + fn set_geometry(&mut self, geometry: GameGeometry) -> bool { + 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); + true + } +} + +fn filter( + spec: &str, + decoder: &codec::decoder::Audio, + encoder: &codec::encoder::Audio, +) -> Result { + let mut filter = filter::Graph::new(); + + let args = format!( + "time_base={}:sample_rate={}:sample_fmt={}:channel_layout=0x{:x}", + decoder.time_base(), + decoder.rate(), + decoder.format().name(), + decoder.channel_layout().bits() + ); + + filter.add(&filter::find("abuffer").unwrap(), "in", &args)?; + filter.add(&filter::find("abuffersink").unwrap(), "out", "")?; + + { + let mut out = filter.get("out").unwrap(); + + out.set_sample_format(encoder.format()); + out.set_channel_layout(encoder.channel_layout()); + out.set_sample_rate(encoder.rate()); + } + + filter.output("in", 0)?.input("out", 0)?.parse(spec)?; + filter.validate()?; + + println!("{}", filter.dump()); + + if let Some(codec) = encoder.codec() { + if !codec + .capabilities() + .contains(ffmpeg::codec::capabilities::Capabilities::VARIABLE_FRAME_SIZE) + { + filter + .get("out") + .unwrap() + .sink() + .set_frame_size(encoder.frame_size()); + } + } + + Ok(filter) +} + +struct Transcoder { + stream: usize, + filter: filter::Graph, + decoder: codec::decoder::Audio, + encoder: codec::encoder::Audio, +} + +fn transcoder>( + ictx: &mut format::context::Input, + octx: &mut format::context::Output, + path: &P, + filter_spec: &str, +) -> Result { + let input = ictx + .streams() + .best(media::Type::Audio) + .expect("could not find best audio stream"); + let mut decoder = input.codec().decoder().audio()?; + let codec = ffmpeg::encoder::find(octx.format().codec(path, media::Type::Audio)) + .expect("failed to find encoder") + .audio()?; + let global = octx + .format() + .flags() + .contains(ffmpeg::format::flag::Flags::GLOBAL_HEADER); + + decoder.set_parameters(input.parameters())?; + + let mut output = octx.add_stream(codec)?; + let mut encoder = output.codec().encoder().audio()?; + + let channel_layout = codec + .channel_layouts() + .map(|cls| cls.best(decoder.channel_layout().channels())) + .unwrap_or(ffmpeg::channel_layout::ChannelLayout::STEREO); + + if global { + encoder.set_flags(ffmpeg::codec::flag::Flags::GLOBAL_HEADER); + } + + encoder.set_rate(decoder.rate() as i32); + encoder.set_channel_layout(channel_layout); + encoder.set_channels(channel_layout.channels()); + encoder.set_format( + codec + .formats() + .expect("unknown supported formats") + .next() + .unwrap(), + ); + encoder.set_bit_rate(decoder.bit_rate()); + encoder.set_max_bit_rate(decoder.max_bit_rate()); + + encoder.set_time_base((1, decoder.rate() as i32)); + output.set_time_base((1, decoder.rate() as i32)); + + let encoder = encoder.open_as(codec)?; + output.set_parameters(&encoder); + + let filter = filter(filter_spec, &decoder, &encoder)?; + + Ok(Transcoder { + stream: input.index(), + filter: filter, + decoder: decoder, + encoder: encoder, + }) +} + +// Transcode the `best` audio stream of the input file into a the output file while applying a +// given filter. If no filter was specified the stream gets copied (`anull` filter). +// +// Example 1: Transcode *.mp3 file to *.wmv while speeding it up +// transcode-audio in.mp3 out.wmv "atempo=1.2" +// +// Example 2: Overlay an audio file +// transcode-audio in.mp3 out.mp3 "amovie=overlay.mp3 [ov]; [in][ov] amerge [out]" +// +// Example 3: Seek to a specified position (in seconds) +// transcode-audio in.mp3 out.mp3 anull 30 +fn main() { + ffmpeg::init().unwrap(); + + let input = env::args().nth(1).expect("missing input"); + let output = env::args().nth(2).expect("missing output"); + let filter = env::args().nth(3).unwrap_or_else(|| "anull".to_owned()); + let seek = env::args().nth(4).and_then(|s| s.parse::().ok()); + + let mut ictx = format::input(&input).unwrap(); + let mut octx = format::output(&output).unwrap(); + let mut transcoder = transcoder(&mut ictx, &mut octx, &output, &filter).unwrap(); + + if let Some(position) = seek { + // If the position was given in seconds, rescale it to ffmpegs base timebase. + let position = position.rescale((1, 1), rescale::TIME_BASE); + // If this seek was embedded in the transcoding loop, a call of `flush()` + // for every opened buffer after the successful seek would be advisable. + ictx.seek(position, ..position).unwrap(); + } + + octx.set_metadata(ictx.metadata().to_owned()); + octx.write_header().unwrap(); + + let in_time_base = transcoder.decoder.time_base(); + let out_time_base = octx.stream(0).unwrap().time_base(); + + let mut decoded = frame::Audio::empty(); + let mut encoded = ffmpeg::Packet::empty(); + + for (stream, mut packet) in ictx.packets() { + if stream.index() == transcoder.stream { + packet.rescale_ts(stream.time_base(), in_time_base); + + if let Ok(true) = transcoder.decoder.decode(&packet, &mut decoded) { + let timestamp = decoded.timestamp(); + decoded.set_pts(timestamp); + + transcoder + .filter + .get("in") + .unwrap() + .source() + .add(&decoded) + .unwrap(); + + while let Ok(..) = transcoder + .filter + .get("out") + .unwrap() + .sink() + .frame(&mut decoded) + { + if let Ok(true) = transcoder.encoder.encode(&decoded, &mut encoded) { + encoded.set_stream(0); + encoded.rescale_ts(in_time_base, out_time_base); + encoded.write_interleaved(&mut octx).unwrap(); + } + } + } + } + } + + transcoder + .filter + .get("in") + .unwrap() + .source() + .flush() + .unwrap(); + + while let Ok(..) = transcoder + .filter + .get("out") + .unwrap() + .sink() + .frame(&mut decoded) + { + if let Ok(true) = transcoder.encoder.encode(&decoded, &mut encoded) { + encoded.set_stream(0); + encoded.rescale_ts(in_time_base, out_time_base); + encoded.write_interleaved(&mut octx).unwrap(); + } + } + + if let Ok(true) = transcoder.encoder.flush(&mut encoded) { + encoded.set_stream(0); + encoded.rescale_ts(in_time_base, out_time_base); + encoded.write_interleaved(&mut octx).unwrap(); + } + + octx.write_trailer().unwrap(); +} From ffc53708d13867cce4f0cec2304e98276b0313ca Mon Sep 17 00:00:00 2001 From: lif Date: Sun, 24 Nov 2019 22:55:30 -0800 Subject: [PATCH 02/36] WIP more - now gets meaningful ffmpeg errors, still need to make a filter pipeline to do resampling and pixel format conv --- examples/ffmpeg_recorder.rs | 328 +++++++++++------------------------- 1 file changed, 102 insertions(+), 226 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 57eefe7..8590ce1 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -2,21 +2,24 @@ extern crate ferretro; extern crate ffmpeg4 as ffmpeg; use std::collections::VecDeque; -use std::env; +use std::io::Read; use std::path::{Path, PathBuf}; use std::pin::Pin; -use ferretro::retro; -use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo}; -use ferretro::retro::wrapper::LibretroWrapper; +use structopt::StructOpt; -use ffmpeg::{codec, filter, format, frame, media, ChannelLayout}; -use ffmpeg::{rescale, Rescale}; +use ferretro::retro; +use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo, SystemInfo}; +use ferretro::retro::wrapper::{LibretroWrapper, Handler}; + +use ffmpeg::{format, frame, media, ChannelLayout}; +use ffmpeg::util::rational::Rational; struct MyEmulator { retro: retro::wrapper::LibretroWrapper, + sys_info: SystemInfo, audio_buf: Vec<(i16, i16)>, - video_pixfmt: format::Pixel, + video_pixel_format: format::Pixel, video_frames: VecDeque, video_encoder: ffmpeg::encoder::Video, audio_encoder: ffmpeg::encoder::Audio, @@ -24,27 +27,49 @@ struct MyEmulator { } impl MyEmulator { - pub fn new(core_path: impl AsRef, sys_path: &Option>, ffstream: format::stream::StreamMut) -> Pin> { - let lib = libloading::Library::new(core_path).unwrap(); + pub fn new( + core_path: impl AsRef, + sys_path: &Option>, + mut video_encoder: ffmpeg::encoder::video::Video, + mut audio_encoder: ffmpeg::encoder::audio::Audio, + ) -> 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); - let mut av_info = retro.get_system_av_info(); - let video_pixfmt = format::Pixel::RGB555; + let sys_info = retro.get_system_info(); + let av_info = retro.get_system_av_info(); + + video_encoder.set_time_base(Rational::new(1, av_info.timing.fps.round() as i32)); + video_encoder.set_frame_rate(av_info.timing.fps.into()); + video_encoder.set_width(av_info.geometry.base_width); + video_encoder.set_height(av_info.geometry.base_height); + video_encoder.set_format(format::Pixel::YUV420P); + video_encoder.set_aspect_ratio(av_info.geometry.aspect_ratio as f64); + + audio_encoder.set_rate(44100); + audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); + audio_encoder.set_channels(2); + audio_encoder.set_channel_layout(ChannelLayout::STEREO); + + let video_encoder_2 = video_encoder.open().unwrap(); + let audio_encoder_2 = audio_encoder.open().unwrap(); let emu = MyEmulator { retro, + sys_info, audio_buf: Default::default(), - video_pixfmt, + video_pixel_format: format::Pixel::RGB555, video_frames: Default::default(), - video_encoder, - audio_encoder, - sys_path: sys_path.map(|x| x.as_ref().to_path_buf()), + video_encoder: video_encoder_2, + audio_encoder: audio_encoder_2, + sys_path: sys_path.as_ref().map(|x| x.as_ref().to_path_buf()), }; 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 } @@ -52,6 +77,7 @@ impl MyEmulator { self.retro.run(); let vframe = self.video_frames.pop_front().unwrap(); + eprintln!("video {}x{}, {} audio samples", vframe.width(), vframe.height(), self.audio_buf.len()); let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), self.audio_buf.len(), @@ -62,8 +88,24 @@ impl MyEmulator { self.audio_buf.clear(); let mut out = ffmpeg::Packet::empty(); - self.video_encoder.encode(&vframe, &mut out); - self.audio_encoder.encode(&aframe, &mut out); + self.video_encoder.encode(&vframe, &mut out).unwrap(); + self.audio_encoder.encode(&aframe, &mut out).unwrap(); + } + + 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(); } } @@ -73,7 +115,7 @@ impl retro::wrapper::Handler for MyEmulator { } fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { - let mut vframe = frame::Video::new(self.video_pixfmt, width, height); + let mut vframe = frame::Video::new(self.video_pixel_format, width, height); let stride = vframe.stride(0); let pitch = pitch as usize; @@ -86,7 +128,7 @@ impl retro::wrapper::Handler for MyEmulator { let ffbegin = y * stride; let lrbegin = y * pitch; let min = usize::min(stride, pitch); - let x = vplane[ffbegin..(ffbegin + min)].copy_from_slice( + vplane[ffbegin..(ffbegin + min)].copy_from_slice( &data[lrbegin..(lrbegin + min)] ); } @@ -106,8 +148,14 @@ 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 { - self.video_pixfmt = match format { + self.video_pixel_format = match format { PixelFormat::ARGB1555 => format::Pixel::RGB555, PixelFormat::ARGB8888 => format::Pixel::RGB32, PixelFormat::RGB565 => format::Pixel::RGB565, @@ -130,221 +178,49 @@ impl retro::wrapper::Handler for MyEmulator { } } -fn filter( - spec: &str, - decoder: &codec::decoder::Audio, - encoder: &codec::encoder::Audio, -) -> Result { - let mut filter = filter::Graph::new(); - - let args = format!( - "time_base={}:sample_rate={}:sample_fmt={}:channel_layout=0x{:x}", - decoder.time_base(), - decoder.rate(), - decoder.format().name(), - decoder.channel_layout().bits() - ); - - filter.add(&filter::find("abuffer").unwrap(), "in", &args)?; - filter.add(&filter::find("abuffersink").unwrap(), "out", "")?; - - { - let mut out = filter.get("out").unwrap(); - - out.set_sample_format(encoder.format()); - out.set_channel_layout(encoder.channel_layout()); - out.set_sample_rate(encoder.rate()); - } - - filter.output("in", 0)?.input("out", 0)?.parse(spec)?; - filter.validate()?; - - println!("{}", filter.dump()); - - if let Some(codec) = encoder.codec() { - if !codec - .capabilities() - .contains(ffmpeg::codec::capabilities::Capabilities::VARIABLE_FRAME_SIZE) - { - filter - .get("out") - .unwrap() - .sink() - .set_frame_size(encoder.frame_size()); - } - } - - Ok(filter) +#[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, + /// Recorded video to write. + #[structopt(short, long, parse(from_os_str))] + video: PathBuf, + /// System directory, often containing BIOS files + #[structopt(short, long, parse(from_os_str))] + system: Option, } -struct Transcoder { - stream: usize, - filter: filter::Graph, - decoder: codec::decoder::Audio, - encoder: codec::encoder::Audio, -} - -fn transcoder>( - ictx: &mut format::context::Input, - octx: &mut format::context::Output, - path: &P, - filter_spec: &str, -) -> Result { - let input = ictx - .streams() - .best(media::Type::Audio) - .expect("could not find best audio stream"); - let mut decoder = input.codec().decoder().audio()?; - let codec = ffmpeg::encoder::find(octx.format().codec(path, media::Type::Audio)) - .expect("failed to find encoder") - .audio()?; - let global = octx - .format() - .flags() - .contains(ffmpeg::format::flag::Flags::GLOBAL_HEADER); - - decoder.set_parameters(input.parameters())?; - - let mut output = octx.add_stream(codec)?; - let mut encoder = output.codec().encoder().audio()?; - - let channel_layout = codec - .channel_layouts() - .map(|cls| cls.best(decoder.channel_layout().channels())) - .unwrap_or(ffmpeg::channel_layout::ChannelLayout::STEREO); - - if global { - encoder.set_flags(ffmpeg::codec::flag::Flags::GLOBAL_HEADER); - } - - encoder.set_rate(decoder.rate() as i32); - encoder.set_channel_layout(channel_layout); - encoder.set_channels(channel_layout.channels()); - encoder.set_format( - codec - .formats() - .expect("unknown supported formats") - .next() - .unwrap(), - ); - encoder.set_bit_rate(decoder.bit_rate()); - encoder.set_max_bit_rate(decoder.max_bit_rate()); - - encoder.set_time_base((1, decoder.rate() as i32)); - output.set_time_base((1, decoder.rate() as i32)); - - let encoder = encoder.open_as(codec)?; - output.set_parameters(&encoder); - - let filter = filter(filter_spec, &decoder, &encoder)?; - - Ok(Transcoder { - stream: input.index(), - filter: filter, - decoder: decoder, - encoder: encoder, - }) -} - -// Transcode the `best` audio stream of the input file into a the output file while applying a -// given filter. If no filter was specified the stream gets copied (`anull` filter). -// -// Example 1: Transcode *.mp3 file to *.wmv while speeding it up -// transcode-audio in.mp3 out.wmv "atempo=1.2" -// -// Example 2: Overlay an audio file -// transcode-audio in.mp3 out.mp3 "amovie=overlay.mp3 [ov]; [in][ov] amerge [out]" -// -// Example 3: Seek to a specified position (in seconds) -// transcode-audio in.mp3 out.mp3 anull 30 fn main() { + let opt: Opt = Opt::from_args(); ffmpeg::init().unwrap(); - let input = env::args().nth(1).expect("missing input"); - let output = env::args().nth(2).expect("missing output"); - let filter = env::args().nth(3).unwrap_or_else(|| "anull".to_owned()); - let seek = env::args().nth(4).and_then(|s| s.parse::().ok()); - - let mut ictx = format::input(&input).unwrap(); - let mut octx = format::output(&output).unwrap(); - let mut transcoder = transcoder(&mut ictx, &mut octx, &output, &filter).unwrap(); - - if let Some(position) = seek { - // If the position was given in seconds, rescale it to ffmpegs base timebase. - let position = position.rescale((1, 1), rescale::TIME_BASE); - // If this seek was embedded in the transcoding loop, a call of `flush()` - // for every opened buffer after the successful seek would be advisable. - ictx.seek(position, ..position).unwrap(); - } - - octx.set_metadata(ictx.metadata().to_owned()); - octx.write_header().unwrap(); - - let in_time_base = transcoder.decoder.time_base(); - let out_time_base = octx.stream(0).unwrap().time_base(); - - let mut decoded = frame::Audio::empty(); - let mut encoded = ffmpeg::Packet::empty(); - - for (stream, mut packet) in ictx.packets() { - if stream.index() == transcoder.stream { - packet.rescale_ts(stream.time_base(), in_time_base); - - if let Ok(true) = transcoder.decoder.decode(&packet, &mut decoded) { - let timestamp = decoded.timestamp(); - decoded.set_pts(timestamp); - - transcoder - .filter - .get("in") - .unwrap() - .source() - .add(&decoded) - .unwrap(); - - while let Ok(..) = transcoder - .filter - .get("out") - .unwrap() - .sink() - .frame(&mut decoded) - { - if let Ok(true) = transcoder.encoder.encode(&decoded, &mut encoded) { - encoded.set_stream(0); - encoded.rescale_ts(in_time_base, out_time_base); - encoded.write_interleaved(&mut octx).unwrap(); - } - } - } - } - } - - transcoder - .filter - .get("in") + let mut octx = format::output(&opt.video).unwrap(); + let vcodec = ffmpeg::encoder::find(octx.format().codec(&opt.video, media::Type::Video)) .unwrap() - .source() - .flush() + .video() + .unwrap(); + let acodec = ffmpeg::encoder::find(octx.format().codec(&opt.video, media::Type::Audio)) + .unwrap() + .audio() .unwrap(); - while let Ok(..) = transcoder - .filter - .get("out") - .unwrap() - .sink() - .frame(&mut decoded) - { - if let Ok(true) = transcoder.encoder.encode(&decoded, &mut encoded) { - encoded.set_stream(0); - encoded.rescale_ts(in_time_base, out_time_base); - encoded.write_interleaved(&mut octx).unwrap(); - } - } + let mut video_encoder = octx.add_stream(vcodec).unwrap().codec().encoder().video().unwrap(); + let mut audio_encoder = octx.add_stream(acodec).unwrap().codec().encoder().audio().unwrap(); - if let Ok(true) = transcoder.encoder.flush(&mut encoded) { - encoded.set_stream(0); - encoded.rescale_ts(in_time_base, out_time_base); - encoded.write_interleaved(&mut octx).unwrap(); + video_encoder.set_bit_rate(64000); + audio_encoder.set_bit_rate(64000); + + let mut emu = MyEmulator::new(opt.core, &opt.system, video_encoder, audio_encoder); + emu.load_game(opt.rom); + + octx.write_header().unwrap(); + + for _ in 0..60*60 { + emu.run(); } octx.write_trailer().unwrap(); From b9023a01319d5cf4e1338e8ddadd794ebd960873 Mon Sep 17 00:00:00 2001 From: lif Date: Mon, 9 Dec 2019 00:01:27 -0800 Subject: [PATCH 03/36] WIP ffmpeg filter graphs --- examples/ffmpeg_recorder.rs | 114 ++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 23 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 8590ce1..600800e 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -7,12 +7,13 @@ 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 ffmpeg::{format, frame, media, ChannelLayout}; +use ffmpeg::{format, filter, frame, media, ChannelLayout}; use ffmpeg::util::rational::Rational; struct MyEmulator { @@ -23,9 +24,71 @@ struct MyEmulator { video_frames: VecDeque, video_encoder: ffmpeg::encoder::Video, audio_encoder: ffmpeg::encoder::Audio, + video_filter: filter::Graph, + audio_filter: filter::Graph, sys_path: Option, } +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 args = format!( + "buffer=width={}:height={}:pix_fmt={}:frame_rate={}:pixel_aspect={}", + av_info.geometry.base_width, + av_info.geometry.base_height, + pix_fmt, + av_info.timing.fps, + pixel_aspect, + ); + 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)?; + vfilter.validate()?; + + Ok(vfilter) +} + +fn audio_filter( + audio_encoder: &ffmpeg::encoder::audio::Audio, + sample_rate: f64, +) -> Result { + let mut afilter = filter::Graph::new(); + let args = format!("sample_rate={}:sample_fmt=s16:channel_layout=stereo", sample_rate); + 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)?; + afilter.validate()?; + + Ok(afilter) +} + impl MyEmulator { pub fn new( core_path: impl AsRef, @@ -44,16 +107,15 @@ impl MyEmulator { video_encoder.set_frame_rate(av_info.timing.fps.into()); video_encoder.set_width(av_info.geometry.base_width); video_encoder.set_height(av_info.geometry.base_height); - video_encoder.set_format(format::Pixel::YUV420P); video_encoder.set_aspect_ratio(av_info.geometry.aspect_ratio as f64); - audio_encoder.set_rate(44100); - audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); - audio_encoder.set_channels(2); - audio_encoder.set_channel_layout(ChannelLayout::STEREO); + audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); - let video_encoder_2 = video_encoder.open().unwrap(); - let audio_encoder_2 = audio_encoder.open().unwrap(); + let video_filter = video_filter(&video_encoder, &av_info).unwrap(); + let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); + + let video_encoder = video_encoder.open().unwrap(); + let audio_encoder = audio_encoder.open().unwrap(); let emu = MyEmulator { retro, @@ -61,8 +123,10 @@ impl MyEmulator { audio_buf: Default::default(), video_pixel_format: format::Pixel::RGB555, video_frames: Default::default(), - video_encoder: video_encoder_2, - audio_encoder: audio_encoder_2, + video_encoder, + audio_encoder, + video_filter, + audio_filter, sys_path: sys_path.as_ref().map(|x| x.as_ref().to_path_buf()), }; @@ -77,7 +141,6 @@ impl MyEmulator { self.retro.run(); let vframe = self.video_frames.pop_front().unwrap(); - eprintln!("video {}x{}, {} audio samples", vframe.width(), vframe.height(), self.audio_buf.len()); let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), self.audio_buf.len(), @@ -194,25 +257,29 @@ struct Opt { system: Option, } -fn main() { +fn main() -> Fallible<()> { let opt: Opt = Opt::from_args(); ffmpeg::init().unwrap(); - let mut octx = format::output(&opt.video).unwrap(); - let vcodec = ffmpeg::encoder::find(octx.format().codec(&opt.video, media::Type::Video)) - .unwrap() - .video() - .unwrap(); - let acodec = ffmpeg::encoder::find(octx.format().codec(&opt.video, media::Type::Audio)) - .unwrap() - .audio() - .unwrap(); + let mut octx = format::output(&opt.video)?; - let mut video_encoder = octx.add_stream(vcodec).unwrap().codec().encoder().video().unwrap(); - let mut audio_encoder = octx.add_stream(acodec).unwrap().codec().encoder().audio().unwrap(); + let detected_vcodec = octx.format().codec(&opt.video, media::Type::Video); + let detected_acodec = octx.format().codec(&opt.video, media::Type::Audio); + + let vcodec = ffmpeg::encoder::find(detected_vcodec).unwrap().video()?; + let acodec = ffmpeg::encoder::find(detected_acodec).unwrap().audio()?; + + let mut video_encoder = octx.add_stream(vcodec)?.codec().encoder().video()?; + let mut audio_encoder = octx.add_stream(acodec)?.codec().encoder().audio()?; video_encoder.set_bit_rate(64000); + video_encoder.set_format(video_encoder.codec().unwrap().video()?.formats().unwrap().nth(0).unwrap()); + audio_encoder.set_bit_rate(64000); + audio_encoder.set_rate(44100); + audio_encoder.set_format(audio_encoder.codec().unwrap().audio()?.formats().unwrap().nth(0).unwrap()); + audio_encoder.set_channels(2); + audio_encoder.set_channel_layout(ChannelLayout::STEREO); let mut emu = MyEmulator::new(opt.core, &opt.system, video_encoder, audio_encoder); emu.load_game(opt.rom); @@ -224,4 +291,5 @@ fn main() { } octx.write_trailer().unwrap(); + Ok(()) } From e4046986d2532d211c296f26cfcd321e3d963d54 Mon Sep 17 00:00:00 2001 From: lif Date: Fri, 24 Jan 2020 23:58:04 -0800 Subject: [PATCH 04/36] WIP get compiling again --- Cargo.toml | 2 +- examples/ffmpeg_recorder.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b76f12..e3c6866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ sdl2 = "^0.32" crossbeam-channel = "^0.4" structopt = "^0.3" # example: ffmpeg_recorder -ffmpeg4 = "*" +ffmpeg4 = "0.4.0" diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 600800e..938de87 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -19,6 +19,7 @@ use ffmpeg::util::rational::Rational; struct MyEmulator { retro: retro::wrapper::LibretroWrapper, sys_info: SystemInfo, + av_info: SystemAvInfo, audio_buf: Vec<(i16, i16)>, video_pixel_format: format::Pixel, video_frames: VecDeque, @@ -109,9 +110,10 @@ impl MyEmulator { video_encoder.set_height(av_info.geometry.base_height); video_encoder.set_aspect_ratio(av_info.geometry.aspect_ratio as f64); - audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); + audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); - let video_filter = video_filter(&video_encoder, &av_info).unwrap(); + let pix_fmt = PixelFormat::ARGB1555; // temporary until env call is made + let video_filter = video_filter(&video_encoder, &av_info, pix_fmt).unwrap(); let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); let video_encoder = video_encoder.open().unwrap(); @@ -120,6 +122,7 @@ impl MyEmulator { let emu = MyEmulator { retro, sys_info, + av_info: av_info.clone(), audio_buf: Default::default(), video_pixel_format: format::Pixel::RGB555, video_frames: Default::default(), @@ -223,6 +226,7 @@ impl retro::wrapper::Handler for MyEmulator { PixelFormat::ARGB8888 => format::Pixel::RGB32, PixelFormat::RGB565 => format::Pixel::RGB565, }; + self.video_filter = video_filter(&self.video_encoder, &self.av_info, format).unwrap(); true } @@ -230,6 +234,7 @@ impl retro::wrapper::Handler for MyEmulator { self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); self.audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); self.set_geometry(system_av_info.geometry); + self.av_info.timing = system_av_info.timing; true } @@ -237,6 +242,7 @@ impl retro::wrapper::Handler for MyEmulator { 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; true } } From b1bde52bb01fa09b3bb8192742b0827041d9abdf Mon Sep 17 00:00:00 2001 From: lif Date: Sat, 25 Jan 2020 12:15:59 -0800 Subject: [PATCH 05/36] WIP - fixing some parameters in video filter --- examples/ffmpeg_recorder.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 938de87..5fd3dcb 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -43,16 +43,17 @@ fn video_filter( }; let pixel_aspect = av_info.geometry.aspect_ratio / (av_info.geometry.base_width as f32 / av_info.geometry.base_height as f32); let args = format!( - "buffer=width={}:height={}:pix_fmt={}:frame_rate={}:pixel_aspect={}", + "width={}:height={}:pix_fmt={}:frame_rate={}:pixel_aspect={}", av_info.geometry.base_width, av_info.geometry.base_height, pix_fmt, - av_info.timing.fps, + if av_info.timing.fps == 0.0 { 60.0 } else { av_info.timing.fps }, pixel_aspect, ); - vfilter.add(&filter::find("buffer").unwrap(), "in", &args); + eprintln!("{}", args); + vfilter.add(&filter::find("buffer").unwrap(), "in", &args)?; //scale? - vfilter.add(&filter::find("buffersink").unwrap(), "out", ""); + vfilter.add(&filter::find("buffersink").unwrap(), "out", "")?; { let mut out = vfilter.get("out").unwrap(); @@ -72,9 +73,9 @@ fn audio_filter( ) -> Result { let mut afilter = filter::Graph::new(); let args = format!("sample_rate={}:sample_fmt=s16:channel_layout=stereo", sample_rate); - afilter.add(&filter::find("abuffer").unwrap(), "in", &args); + afilter.add(&filter::find("abuffer").unwrap(), "in", &args)?; //aresample? - afilter.add(&filter::find("abuffersink").unwrap(), "out", ""); + afilter.add(&filter::find("abuffersink").unwrap(), "out", "")?; { let mut out = afilter.get("out").unwrap(); @@ -233,8 +234,8 @@ impl retro::wrapper::Handler for MyEmulator { fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); self.audio_encoder.set_rate(system_av_info.timing.sample_rate.round() as i32); - self.set_geometry(system_av_info.geometry); self.av_info.timing = system_av_info.timing; + self.set_geometry(system_av_info.geometry); true } @@ -243,6 +244,13 @@ impl retro::wrapper::Handler for MyEmulator { self.video_encoder.set_height(geometry.base_height); self.video_encoder.set_aspect_ratio(geometry.aspect_ratio as f64); self.av_info.geometry = geometry; + 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(); true } } From f94f5c836fe93eb049956404f1321f2eba830b60 Mon Sep 17 00:00:00 2001 From: lif Date: Sat, 25 Jan 2020 19:32:12 -0800 Subject: [PATCH 06/36] WIP: getting closer to actually encoding a frame!! --- examples/ffmpeg_recorder.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 5fd3dcb..4fe04e4 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -42,13 +42,15 @@ fn video_filter( 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={}", + "width={}:height={}:pix_fmt={}:frame_rate={}:pixel_aspect={}:time_base=1/{}", av_info.geometry.base_width, av_info.geometry.base_height, pix_fmt, - if av_info.timing.fps == 0.0 { 60.0 } else { av_info.timing.fps }, + fps, pixel_aspect, + fps, ); eprintln!("{}", args); vfilter.add(&filter::find("buffer").unwrap(), "in", &args)?; @@ -61,7 +63,9 @@ fn video_filter( } vfilter.output("in", 0)? - .input("out", 0)?; + .input("out", 0)? + .parse("null")?; + vfilter.validate()?; Ok(vfilter) @@ -72,7 +76,9 @@ fn audio_filter( 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", sample_rate); + eprintln!("{}", args); afilter.add(&filter::find("abuffer").unwrap(), "in", &args)?; //aresample? afilter.add(&filter::find("abuffersink").unwrap(), "out", "")?; @@ -85,7 +91,8 @@ fn audio_filter( } afilter.output("in", 0)? - .input("out", 0)?; + .input("out", 0)? + .parse("anull")?; afilter.validate()?; Ok(afilter) @@ -105,13 +112,16 @@ impl MyEmulator { let sys_info = retro.get_system_info(); let av_info = retro.get_system_av_info(); - video_encoder.set_time_base(Rational::new(1, av_info.timing.fps.round() as i32)); + let fps_int = av_info.timing.fps.round() as i32; + let fps_int = if fps_int == 0 { 60 } else { fps_int }; + + video_encoder.set_time_base(Rational::new(1, fps_int)); video_encoder.set_frame_rate(av_info.timing.fps.into()); 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); - audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); + audio_encoder.set_rate(44100); let pix_fmt = PixelFormat::ARGB1555; // temporary until env call is made let video_filter = video_filter(&video_encoder, &av_info, pix_fmt).unwrap(); @@ -144,7 +154,7 @@ impl MyEmulator { pub fn run(&mut self) { self.retro.run(); - let vframe = self.video_frames.pop_front().unwrap(); + let mut vframe = self.video_frames.pop_front().unwrap(); let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), self.audio_buf.len(), @@ -155,8 +165,14 @@ impl MyEmulator { self.audio_buf.clear(); let mut out = ffmpeg::Packet::empty(); - self.video_encoder.encode(&vframe, &mut out).unwrap(); - self.audio_encoder.encode(&aframe, &mut out).unwrap(); + self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); + self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); + while let Ok(..) = self.video_filter.get("out").unwrap().sink().frame(&mut vframe) { + self.video_encoder.encode(&vframe, &mut out).unwrap(); + } + while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut aframe) { + self.audio_encoder.encode(&aframe, &mut out).unwrap(); + } } pub fn load_game(&self, rom: impl AsRef) { From d1e70199022496236e6c11925b8e7b252b06d744 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 02:01:07 -0800 Subject: [PATCH 07/36] some fumbling --- examples/ffmpeg_recorder.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 4fe04e4..2ea4080 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -110,16 +110,21 @@ impl MyEmulator { let retro = retro::wrapper::LibretroWrapper::from(raw_retro); let sys_info = retro.get_system_info(); - let av_info = retro.get_system_av_info(); + 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 }; video_encoder.set_time_base(Rational::new(1, fps_int)); 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 = 640; + av_info.geometry.base_height = 480; + av_info.geometry.aspect_ratio = 4.33; + } 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); + //video_encoder.set_aspect_ratio(av_info.geometry.aspect_ratio as f64); audio_encoder.set_rate(44100); @@ -249,7 +254,9 @@ impl retro::wrapper::Handler for MyEmulator { fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); + 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 @@ -289,6 +296,7 @@ struct Opt { fn main() -> Fallible<()> { let opt: Opt = Opt::from_args(); + ffmpeg::log::set_level(ffmpeg::log::Level::Trace); ffmpeg::init().unwrap(); let mut octx = format::output(&opt.video)?; From 997be77c8abd440ab900cafe45cfcc7238d70e15 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 02:01:39 -0800 Subject: [PATCH 08/36] switch to ffmpeg-next --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e3c6866..a350c75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ sdl2 = "^0.32" crossbeam-channel = "^0.4" structopt = "^0.3" # example: ffmpeg_recorder -ffmpeg4 = "0.4.0" +ffmpeg-next = "4.3.7" From 764485b2b539f410df83fb345b342896f9954a16 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 02:02:32 -0800 Subject: [PATCH 09/36] update crate ref --- examples/ffmpeg_recorder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 2ea4080..212a24a 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -1,5 +1,5 @@ extern crate ferretro; -extern crate ffmpeg4 as ffmpeg; +extern crate ffmpeg_next as ffmpeg; use std::collections::VecDeque; use std::io::Read; From bdba596d07779408c3fcd6d5477155afb49d6c2b Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 02:27:37 -0800 Subject: [PATCH 10/36] stop frame properties from changing after starting, ffmpeg doesn't like that --- examples/ffmpeg_recorder.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 212a24a..bf136ac 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -28,6 +28,7 @@ struct MyEmulator { video_filter: filter::Graph, audio_filter: filter::Graph, sys_path: Option, + frame_properties_locked: bool, } fn video_filter( @@ -147,6 +148,7 @@ impl MyEmulator { video_filter, audio_filter, sys_path: sys_path.as_ref().map(|x| x.as_ref().to_path_buf()), + frame_properties_locked: false, }; let mut pin_emu = Box::pin(emu); @@ -243,6 +245,10 @@ impl retro::wrapper::Handler for MyEmulator { } fn set_pixel_format(&mut self, format: PixelFormat) -> bool { + if self.frame_properties_locked { + return true; + } + self.video_pixel_format = match format { PixelFormat::ARGB1555 => format::Pixel::RGB555, PixelFormat::ARGB8888 => format::Pixel::RGB32, @@ -253,16 +259,26 @@ impl retro::wrapper::Handler for MyEmulator { } fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { - self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); - 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); + if self.frame_properties_locked { + return true; } + + self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); + /* + 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 } fn set_geometry(&mut self, geometry: GameGeometry) -> bool { + if self.frame_properties_locked { + return 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); @@ -322,6 +338,7 @@ fn main() -> Fallible<()> { let mut emu = MyEmulator::new(opt.core, &opt.system, video_encoder, audio_encoder); emu.load_game(opt.rom); + emu.frame_properties_locked = true; octx.write_header().unwrap(); for _ in 0..60*60 { From 15c830f0b1c416bf7d6c188398f2707d0f0f78a1 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 15:12:10 -0800 Subject: [PATCH 11/36] add a lot of debug output --- examples/ffmpeg_recorder.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index bf136ac..6299eb3 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -53,7 +53,7 @@ fn video_filter( pixel_aspect, fps, ); - eprintln!("{}", args); + eprintln!("đŸŽĨ filter args: {}", args); vfilter.add(&filter::find("buffer").unwrap(), "in", &args)?; //scale? vfilter.add(&filter::find("buffersink").unwrap(), "out", "")?; @@ -65,9 +65,11 @@ fn video_filter( vfilter.output("in", 0)? .input("out", 0)? - .parse("null")?; + .parse("null")?; // passthrough filter for video vfilter.validate()?; + // human-readable filter graph + eprintln!("{}", vfilter.dump()); Ok(vfilter) } @@ -79,7 +81,7 @@ fn audio_filter( 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", sample_rate); - eprintln!("{}", args); + eprintln!("🔊 filter args: {}", args); afilter.add(&filter::find("abuffer").unwrap(), "in", &args)?; //aresample? afilter.add(&filter::find("abuffersink").unwrap(), "out", "")?; @@ -175,6 +177,12 @@ impl MyEmulator { self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); while let Ok(..) = self.video_filter.get("out").unwrap().sink().frame(&mut vframe) { + if self.video_filter.get("in").unwrap().source().failed_requests() > 0 { + println!("đŸŽĨ failed to put filter input frame"); + } + if self.video_filter.get("out").unwrap().source().failed_requests() > 0 { + println!("đŸŽĨ failed to get filter output frame"); + } self.video_encoder.encode(&vframe, &mut out).unwrap(); } while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut aframe) { @@ -292,6 +300,11 @@ impl retro::wrapper::Handler for MyEmulator { self.video_filter = video_filter(&self.video_encoder, &self.av_info, pixel_format).unwrap(); true } + + + fn log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) { + eprint!("🕹ī¸ [{:?}] {}", level, msg); +} } #[derive(StructOpt)] @@ -341,7 +354,7 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; octx.write_header().unwrap(); - for _ in 0..60*60 { + eprintln!("đŸ–ŧī¸ frame: {}", frame); emu.run(); } From c78d1cb9fdac263a698a62e7e9431daa1b02b62d Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 15:12:27 -0800 Subject: [PATCH 12/36] reduce # of frames run while poking around --- examples/ffmpeg_recorder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 6299eb3..b78c962 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -354,6 +354,8 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; octx.write_header().unwrap(); + //for frame in 0..60*10 { + for frame in 0..10 { eprintln!("đŸ–ŧī¸ frame: {}", frame); emu.run(); } From 439e57598218df59bea8e8923e8ac3793c1e650a Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 15:12:48 -0800 Subject: [PATCH 13/36] make audio encode failure non-fatal so we can run to the end at least --- examples/ffmpeg_recorder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index b78c962..f40f0ef 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -186,7 +186,7 @@ impl MyEmulator { self.video_encoder.encode(&vframe, &mut out).unwrap(); } while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut aframe) { - self.audio_encoder.encode(&aframe, &mut out).unwrap(); + self.audio_encoder.encode(&aframe, &mut out);//.unwrap(); } } From c119c5eacd0a9a7a1cc92928b40e207e2e5082fa Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 15:13:24 -0800 Subject: [PATCH 14/36] audio sample rate hacks. todo: review which ones are actually needed --- examples/ffmpeg_recorder.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index f40f0ef..c460925 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -125,6 +125,9 @@ impl MyEmulator { av_info.geometry.base_height = 480; av_info.geometry.aspect_ratio = 4.33; } + if av_info.timing.sample_rate == 0.0 { + av_info.timing.sample_rate = 32040.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); @@ -136,7 +139,8 @@ impl MyEmulator { let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); let video_encoder = video_encoder.open().unwrap(); - let audio_encoder = audio_encoder.open().unwrap(); + let mut audio_encoder = audio_encoder.open().unwrap(); + audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); let emu = MyEmulator { retro, @@ -169,6 +173,7 @@ impl MyEmulator { self.audio_buf.len(), ChannelLayout::STEREO ); + aframe.set_rate(32040); let aplane: &mut [(i16, i16)] = aframe.plane_mut(0); aplane.copy_from_slice(self.audio_buf.as_ref()); self.audio_buf.clear(); From ba90e89cdbf19f1107c8fdc1b518bc35f7c2b9b2 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 29 Nov 2020 15:13:45 -0800 Subject: [PATCH 15/36] hardcoded match for sega genesis video geometry --- examples/ffmpeg_recorder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index c460925..d91759b 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -121,8 +121,8 @@ impl MyEmulator { video_encoder.set_time_base(Rational::new(1, fps_int)); 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 = 640; - av_info.geometry.base_height = 480; + 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 { From 2408654651d28a6309736338f475150a36513f67 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Mon, 19 Apr 2021 01:06:47 -0700 Subject: [PATCH 16/36] writes some garbage to file each frame (using .mp4 output). i'm not sure if this is an improvement --- Cargo.toml | 2 +- examples/ffmpeg_recorder.rs | 43 +++++++++++++++++++++++++++++++------ examples/sdl2_emulator.rs | 2 +- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a350c75..3778c2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ sdl2 = "^0.32" crossbeam-channel = "^0.4" structopt = "^0.3" # example: ffmpeg_recorder -ffmpeg-next = "4.3.7" +ffmpeg-next = "4.3.8" diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index d91759b..3d78e21 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -13,7 +13,7 @@ use ferretro::retro; use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo, SystemInfo}; use ferretro::retro::wrapper::{LibretroWrapper, Handler}; -use ffmpeg::{format, filter, frame, media, ChannelLayout}; +use ffmpeg::{ChannelLayout, Packet, filter, format, frame, media}; use ffmpeg::util::rational::Rational; struct MyEmulator { @@ -164,7 +164,7 @@ impl MyEmulator { pin_emu } - pub fn run(&mut self) { + pub fn run(&mut self, octx: &mut format::context::Output, frame: i64) { self.retro.run(); let mut vframe = self.video_frames.pop_front().unwrap(); @@ -188,11 +188,30 @@ impl MyEmulator { if self.video_filter.get("out").unwrap().source().failed_requests() > 0 { println!("đŸŽĨ failed to get filter output frame"); } - self.video_encoder.encode(&vframe, &mut out).unwrap(); + vframe.set_pts(Some(frame)); + let encode_result = self.video_encoder.encode(&vframe, &mut out).unwrap(); + eprintln!("encoded: {:?}", encode_result); + if encode_result { + out.set_stream(0); + out.write_interleaved(octx).unwrap(); + } + + /* + while self.video_encoder.receive_packet(&mut encoded).is_ok() { + panic!("actually trying to write something!!"); + encoded.set_stream(0); // use stream index... + // rescale_ts?? + encoded.write_interleaved(&mut octx).unwrap(); + } + */ + //eprintln!("packet: {:?}", &out.data()); + //self.video_encoder.flush(&mut out).unwrap(); + //eprintln!("flushed packet: {:?}", &out.data()); } while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut aframe) { self.audio_encoder.encode(&aframe, &mut out);//.unwrap(); } + } pub fn load_game(&self, rom: impl AsRef) { @@ -224,9 +243,11 @@ impl retro::wrapper::Handler for MyEmulator { let pitch = pitch as usize; let vplane = vframe.data_mut(0); + /* sus 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; @@ -235,7 +256,7 @@ impl retro::wrapper::Handler for MyEmulator { &data[lrbegin..(lrbegin + min)] ); } - } + //} self.video_frames.push_back(vframe); } @@ -360,10 +381,20 @@ fn main() -> Fallible<()> { octx.write_header().unwrap(); //for frame in 0..60*10 { - for frame in 0..10 { + for frame in 0..120 { eprintln!("đŸ–ŧī¸ frame: {}", frame); - emu.run(); + emu.run(&mut octx, frame); + + let mut encoded = ffmpeg::Packet::empty(); + while emu.video_encoder.receive_packet(&mut encoded).is_ok() { + panic!("actually trying to write something!!"); + encoded.set_stream(0); // use stream index... + // rescale_ts?? + encoded.write_interleaved(&mut octx).unwrap(); + } } + let mut packet = Packet::empty(); + eprintln!("flushed: {:?}", emu.video_encoder.flush(&mut packet).unwrap()); octx.write_trailer().unwrap(); Ok(()) diff --git a/examples/sdl2_emulator.rs b/examples/sdl2_emulator.rs index 8ca7db2..293dc90 100644 --- a/examples/sdl2_emulator.rs +++ b/examples/sdl2_emulator.rs @@ -158,7 +158,7 @@ impl MyEmulator { // 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() { + if spf.is_nan() || spf.is_infinite() { spf = 1.0 / 60.0; } Duration::from_secs_f64(spf) From d3a88573d788507f61d5078e90a976d4d2dab444 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Tue, 20 Apr 2021 23:57:39 -0700 Subject: [PATCH 17/36] move around some things but now my debugger doesn't work for some inexplicable reason --- examples/ffmpeg_recorder.rs | 98 +++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 3d78e21..0d83ca8 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -29,6 +29,7 @@ struct MyEmulator { audio_filter: filter::Graph, sys_path: Option, frame_properties_locked: bool, + //octx: ffmpeg::format::context::Output, } fn video_filter( @@ -105,8 +106,8 @@ impl MyEmulator { pub fn new( core_path: impl AsRef, sys_path: &Option>, - mut video_encoder: ffmpeg::encoder::video::Video, - mut audio_encoder: ffmpeg::encoder::audio::Audio, + 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(); @@ -118,7 +119,32 @@ impl MyEmulator { 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 vcodec = ffmpeg::encoder::find(detected_vcodec).unwrap().video().unwrap(); + let acodec = ffmpeg::encoder::find(detected_acodec).unwrap().audio().unwrap(); + + let mut audio_encoder = octx.add_stream(acodec).unwrap().codec().encoder().audio().unwrap(); + //let mut video_encoder = octx.add_stream(vcodec).unwrap().codec().encoder().video().unwrap(); + let mut video_output = octx.add_stream(vcodec).unwrap(); + let mut video_encoder = video_output.codec().encoder().video().unwrap(); + /* + let mut audio_output = octx.add_stream(acodec).unwrap(); + let mut audio_encoder = audio_output.codec().encoder().audio().unwrap(); + */ + + video_encoder.set_bit_rate(64000); + video_encoder.set_format(video_encoder.codec().unwrap().video().unwrap().formats().unwrap().nth(0).unwrap()); + + audio_encoder.set_bit_rate(64000); + audio_encoder.set_rate(44100); + audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); + audio_encoder.set_channels(2); + audio_encoder.set_channel_layout(ChannelLayout::STEREO); + video_encoder.set_time_base(Rational::new(1, fps_int)); + video_output.set_time_base(Rational::new(1, fps_int)); 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; @@ -138,9 +164,11 @@ impl MyEmulator { let video_filter = video_filter(&video_encoder, &av_info, pix_fmt).unwrap(); let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); - let video_encoder = video_encoder.open().unwrap(); - let mut audio_encoder = audio_encoder.open().unwrap(); + let video_encoder = video_encoder.open_as(vcodec).unwrap(); + video_output.set_parameters(&video_encoder); + let mut audio_encoder = audio_encoder.open_as(acodec).unwrap(); audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); + //audio_output.set_parameters(&audio_encoder); let emu = MyEmulator { retro, @@ -155,6 +183,7 @@ impl MyEmulator { audio_filter, sys_path: sys_path.as_ref().map(|x| x.as_ref().to_path_buf()), frame_properties_locked: false, + //octx, // AAA }; let mut pin_emu = Box::pin(emu); @@ -164,7 +193,7 @@ impl MyEmulator { pin_emu } - pub fn run(&mut self, octx: &mut format::context::Output, frame: i64) { + pub fn run(&mut self, frame: i64) { self.retro.run(); let mut vframe = self.video_frames.pop_front().unwrap(); @@ -181,29 +210,27 @@ impl MyEmulator { let mut out = ffmpeg::Packet::empty(); self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); - while let Ok(..) = self.video_filter.get("out").unwrap().sink().frame(&mut vframe) { + let mut filtered_vframe = frame::Video::empty(); + while self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe).is_ok() { if self.video_filter.get("in").unwrap().source().failed_requests() > 0 { println!("đŸŽĨ failed to put filter input frame"); } - if self.video_filter.get("out").unwrap().source().failed_requests() > 0 { - println!("đŸŽĨ failed to get filter output frame"); - } vframe.set_pts(Some(frame)); - let encode_result = self.video_encoder.encode(&vframe, &mut out).unwrap(); + let encode_result = self.video_encoder.send_frame(&filtered_vframe).unwrap(); eprintln!("encoded: {:?}", encode_result); - if encode_result { + /*if encode_result { out.set_stream(0); out.write_interleaved(octx).unwrap(); } - - /* - while self.video_encoder.receive_packet(&mut encoded).is_ok() { - panic!("actually trying to write something!!"); - encoded.set_stream(0); // use stream index... - // rescale_ts?? - encoded.write_interleaved(&mut octx).unwrap(); - } */ + + let mut encoded_packet = ffmpeg::Packet::empty(); + while self.video_encoder.receive_packet(&mut encoded_packet).is_ok() { + //eprintln!("Attempting write: {:?}", encoded_packet); + encoded_packet.set_stream(0); // use stream index... + //encoded_packet.rescale_ts(Rational(1, 1), self.time_base); + //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA + } //eprintln!("packet: {:?}", &out.data()); //self.video_encoder.flush(&mut out).unwrap(); //eprintln!("flushed packet: {:?}", &out.data()); @@ -356,46 +383,21 @@ fn main() -> Fallible<()> { let mut octx = format::output(&opt.video)?; - let detected_vcodec = octx.format().codec(&opt.video, media::Type::Video); - let detected_acodec = octx.format().codec(&opt.video, media::Type::Audio); - - let vcodec = ffmpeg::encoder::find(detected_vcodec).unwrap().video()?; - let acodec = ffmpeg::encoder::find(detected_acodec).unwrap().audio()?; - - let mut video_encoder = octx.add_stream(vcodec)?.codec().encoder().video()?; - let mut audio_encoder = octx.add_stream(acodec)?.codec().encoder().audio()?; - - video_encoder.set_bit_rate(64000); - video_encoder.set_format(video_encoder.codec().unwrap().video()?.formats().unwrap().nth(0).unwrap()); - - audio_encoder.set_bit_rate(64000); - audio_encoder.set_rate(44100); - audio_encoder.set_format(audio_encoder.codec().unwrap().audio()?.formats().unwrap().nth(0).unwrap()); - audio_encoder.set_channels(2); - audio_encoder.set_channel_layout(ChannelLayout::STEREO); - - let mut emu = MyEmulator::new(opt.core, &opt.system, video_encoder, audio_encoder); + let mut emu = MyEmulator::new(opt.core, &opt.system, &opt.video, octx); emu.load_game(opt.rom); emu.frame_properties_locked = true; - octx.write_header().unwrap(); + //octx.write_header().unwrap(); //for frame in 0..60*10 { for frame in 0..120 { eprintln!("đŸ–ŧī¸ frame: {}", frame); - emu.run(&mut octx, frame); + emu.run(frame); - let mut encoded = ffmpeg::Packet::empty(); - while emu.video_encoder.receive_packet(&mut encoded).is_ok() { - panic!("actually trying to write something!!"); - encoded.set_stream(0); // use stream index... - // rescale_ts?? - encoded.write_interleaved(&mut octx).unwrap(); - } } let mut packet = Packet::empty(); eprintln!("flushed: {:?}", emu.video_encoder.flush(&mut packet).unwrap()); - octx.write_trailer().unwrap(); + //octx.write_trailer().unwrap(); Ok(()) } From 88d1ab0b0ed7452eac82d573171f276ea5e9a66f Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Thu, 22 Apr 2021 00:16:09 -0700 Subject: [PATCH 18/36] actually write a video!!!!! --- examples/ffmpeg_recorder.rs | 94 ++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 0d83ca8..88feba0 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -29,7 +29,7 @@ struct MyEmulator { audio_filter: filter::Graph, sys_path: Option, frame_properties_locked: bool, - //octx: ffmpeg::format::context::Output, + octx: ffmpeg::format::context::Output, } fn video_filter( @@ -125,27 +125,16 @@ impl MyEmulator { let vcodec = ffmpeg::encoder::find(detected_vcodec).unwrap().video().unwrap(); let acodec = ffmpeg::encoder::find(detected_acodec).unwrap().audio().unwrap(); - let mut audio_encoder = octx.add_stream(acodec).unwrap().codec().encoder().audio().unwrap(); - //let mut video_encoder = octx.add_stream(vcodec).unwrap().codec().encoder().video().unwrap(); let mut video_output = octx.add_stream(vcodec).unwrap(); let mut video_encoder = video_output.codec().encoder().video().unwrap(); - /* - let mut audio_output = octx.add_stream(acodec).unwrap(); - let mut audio_encoder = audio_output.codec().encoder().audio().unwrap(); - */ video_encoder.set_bit_rate(64000); video_encoder.set_format(video_encoder.codec().unwrap().video().unwrap().formats().unwrap().nth(0).unwrap()); - audio_encoder.set_bit_rate(64000); - audio_encoder.set_rate(44100); - audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); - audio_encoder.set_channels(2); - audio_encoder.set_channel_layout(ChannelLayout::STEREO); - video_encoder.set_time_base(Rational::new(1, fps_int)); video_output.set_time_base(Rational::new(1, fps_int)); 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; @@ -158,18 +147,42 @@ impl MyEmulator { video_encoder.set_height(av_info.geometry.base_height); //video_encoder.set_aspect_ratio(av_info.geometry.aspect_ratio as f64); - audio_encoder.set_rate(44100); - let pix_fmt = PixelFormat::ARGB1555; // temporary until env call is made let video_filter = video_filter(&video_encoder, &av_info, pix_fmt).unwrap(); - let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).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(64000); + audio_encoder.set_rate(44100); + audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); + audio_encoder.set_channels(2); + audio_encoder.set_channel_layout(ChannelLayout::STEREO); + + audio_encoder.set_rate(44100); + let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); + let mut audio_encoder = audio_encoder.open_as(acodec).unwrap(); audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); //audio_output.set_parameters(&audio_encoder); - +octx.write_header().unwrap(); let emu = MyEmulator { retro, sys_info, @@ -183,7 +196,7 @@ impl MyEmulator { audio_filter, sys_path: sys_path.as_ref().map(|x| x.as_ref().to_path_buf()), frame_properties_locked: false, - //octx, // AAA + octx, }; let mut pin_emu = Box::pin(emu); @@ -207,17 +220,28 @@ impl MyEmulator { aplane.copy_from_slice(self.audio_buf.as_ref()); self.audio_buf.clear(); + //vframe.set_pts(Some(frame)); + unsafe + { + //(*vframe.as_mut_ptr()).pts = frame; + //(*vframe.as_mut_ptr()).pkt_dts = frame; + } + let mut out = ffmpeg::Packet::empty(); self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); let mut filtered_vframe = frame::Video::empty(); + + // ffmpeg_push_video_thread takes ffmpeg_t handle which is a retroarch struct holding ffmpeg context + // video_frame_record_scale + // grabs frame from input (record_video_data struct) and copies it to ffmpeg_t handle .video.conv_frame.data[0] + // conv_frame is an AVFrame. where's that from while self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe).is_ok() { if self.video_filter.get("in").unwrap().source().failed_requests() > 0 { println!("đŸŽĨ failed to put filter input frame"); } - vframe.set_pts(Some(frame)); let encode_result = self.video_encoder.send_frame(&filtered_vframe).unwrap(); - eprintln!("encoded: {:?}", encode_result); + //eprintln!("encoded: {:?}", encode_result); /*if encode_result { out.set_stream(0); out.write_interleaved(octx).unwrap(); @@ -225,9 +249,19 @@ impl MyEmulator { */ let mut encoded_packet = ffmpeg::Packet::empty(); - while self.video_encoder.receive_packet(&mut encoded_packet).is_ok() { + while let Ok(..) = self.video_encoder.receive_packet(&mut encoded_packet) { //eprintln!("Attempting write: {:?}", encoded_packet); - encoded_packet.set_stream(0); // use stream index... + //encoded_packet.set_stream(0); // use stream index... + + unsafe { + (*self.octx.as_mut_ptr()).debug = 1; + eprintln!("oformat: {:?}", &((*self.octx.as_mut_ptr()).oformat)); + eprintln!("flags: {:?}", (*(*self.octx.as_mut_ptr()).oformat).flags); + } + + if encoded_packet.size() > 0 { + encoded_packet.write(&mut self.octx).unwrap(); // AAA + } //encoded_packet.rescale_ts(Rational(1, 1), self.time_base); //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA } @@ -270,11 +304,9 @@ impl retro::wrapper::Handler for MyEmulator { let pitch = pitch as usize; let vplane = vframe.data_mut(0); - /* sus 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; @@ -283,7 +315,9 @@ impl retro::wrapper::Handler for MyEmulator { &data[lrbegin..(lrbegin + min)] ); } - //} + } + + //vframe.set_pts(Some(69)); self.video_frames.push_back(vframe); } @@ -382,15 +416,21 @@ fn main() -> Fallible<()> { ffmpeg::init().unwrap(); let mut octx = format::output(&opt.video)?; + unsafe { + (*octx.as_mut_ptr()).debug = 1; + eprintln!("oformat: {:?}", &((*octx.as_mut_ptr()).oformat)); + eprintln!("flags: {:?}", (*(*octx.as_mut_ptr()).oformat).flags); + } + + //octx.write_header().unwrap(); let mut emu = MyEmulator::new(opt.core, &opt.system, &opt.video, octx); emu.load_game(opt.rom); emu.frame_properties_locked = true; - //octx.write_header().unwrap(); //for frame in 0..60*10 { - for frame in 0..120 { + for frame in 0..9200 { eprintln!("đŸ–ŧī¸ frame: {}", frame); emu.run(frame); From eac635dc715f33533fe133cb4e561f83037367b0 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Fri, 23 Apr 2021 20:02:07 -0700 Subject: [PATCH 19/36] video with slightly more correct time scale --- examples/ffmpeg_recorder.rs | 89 +++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 88feba0..d7df52d 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -30,6 +30,7 @@ struct MyEmulator { sys_path: Option, frame_properties_locked: bool, octx: ffmpeg::format::context::Output, + frame: u64, } fn video_filter( @@ -176,13 +177,14 @@ static bool ffmpeg_init_config(struct ff_config_param *params, audio_encoder.set_channels(2); audio_encoder.set_channel_layout(ChannelLayout::STEREO); - audio_encoder.set_rate(44100); let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); let mut audio_encoder = audio_encoder.open_as(acodec).unwrap(); audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); - //audio_output.set_parameters(&audio_encoder); -octx.write_header().unwrap(); + audio_output.set_parameters(&audio_encoder); + + octx.write_header().unwrap(); + let emu = MyEmulator { retro, sys_info, @@ -197,6 +199,7 @@ octx.write_header().unwrap(); 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); @@ -207,9 +210,32 @@ octx.write_header().unwrap(); } pub fn run(&mut self, frame: i64) { + self.frame += 1; self.retro.run(); let mut vframe = self.video_frames.pop_front().unwrap(); + self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); + let mut filtered_vframe = frame::Video::empty(); + + while self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe).is_ok() { + 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(); + + let mut encoded_packet = ffmpeg::Packet::empty(); + while let Ok(..) = self.video_encoder.receive_packet(&mut encoded_packet) { + if encoded_packet.size() > 0 { + encoded_packet.set_stream(0); // use stream index... + //encoded_packet.rescale_ts(Rational(1, 1), Rational(1, 60)); + encoded_packet.write(&mut self.octx).unwrap(); // AAA + } + //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA + } + } + + let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), self.audio_buf.len(), @@ -220,57 +246,24 @@ octx.write_header().unwrap(); aplane.copy_from_slice(self.audio_buf.as_ref()); self.audio_buf.clear(); - //vframe.set_pts(Some(frame)); - unsafe - { - //(*vframe.as_mut_ptr()).pts = frame; - //(*vframe.as_mut_ptr()).pkt_dts = frame; - } - let mut out = ffmpeg::Packet::empty(); - self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); - let mut filtered_vframe = frame::Video::empty(); - // ffmpeg_push_video_thread takes ffmpeg_t handle which is a retroarch struct holding ffmpeg context - // video_frame_record_scale - // grabs frame from input (record_video_data struct) and copies it to ffmpeg_t handle .video.conv_frame.data[0] - // conv_frame is an AVFrame. where's that from - while self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe).is_ok() { - if self.video_filter.get("in").unwrap().source().failed_requests() > 0 { + let mut filtered_aframe = frame::Audio::empty(); + while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut filtered_aframe) { + if self.audio_filter.get("in").unwrap().source().failed_requests() > 0 { println!("đŸŽĨ failed to put filter input frame"); } - let encode_result = self.video_encoder.send_frame(&filtered_vframe).unwrap(); - //eprintln!("encoded: {:?}", encode_result); - /*if encode_result { - out.set_stream(0); - out.write_interleaved(octx).unwrap(); - } - */ + self.audio_encoder.send_frame(&aframe).unwrap(); let mut encoded_packet = ffmpeg::Packet::empty(); - while let Ok(..) = self.video_encoder.receive_packet(&mut encoded_packet) { - //eprintln!("Attempting write: {:?}", encoded_packet); - //encoded_packet.set_stream(0); // use stream index... - - unsafe { - (*self.octx.as_mut_ptr()).debug = 1; - eprintln!("oformat: {:?}", &((*self.octx.as_mut_ptr()).oformat)); - eprintln!("flags: {:?}", (*(*self.octx.as_mut_ptr()).oformat).flags); - } - - if encoded_packet.size() > 0 { - encoded_packet.write(&mut self.octx).unwrap(); // AAA - } - //encoded_packet.rescale_ts(Rational(1, 1), self.time_base); - //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA + while let Ok(..) = self.audio_encoder.receive_packet(&mut encoded_packet) { + if encoded_packet.size() > 0 { + encoded_packet.set_stream(1); + //encoded_packet.rescale_ts(Rational(1, 1), Rational(1, 60)); + encoded_packet.write(&mut self.octx).unwrap(); // AAA + } } - //eprintln!("packet: {:?}", &out.data()); - //self.video_encoder.flush(&mut out).unwrap(); - //eprintln!("flushed packet: {:?}", &out.data()); - } - while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut aframe) { - self.audio_encoder.encode(&aframe, &mut out);//.unwrap(); } } @@ -317,7 +310,7 @@ impl retro::wrapper::Handler for MyEmulator { } } - //vframe.set_pts(Some(69)); + //vframe.set_pts(Some(self.frame as i64)); self.video_frames.push_back(vframe); } @@ -430,7 +423,7 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; //for frame in 0..60*10 { - for frame in 0..9200 { + for frame in 0..600 { eprintln!("đŸ–ŧī¸ frame: {}", frame); emu.run(frame); From d660a9cc94b1eccfe7f726ab79165af1a798e49f Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Fri, 23 Apr 2021 23:55:26 -0700 Subject: [PATCH 20/36] shuffle some stuff around --- examples/ffmpeg_recorder.rs | 46 ++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index d7df52d..f95936e 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -13,7 +13,7 @@ use ferretro::retro; use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo, SystemInfo}; use ferretro::retro::wrapper::{LibretroWrapper, Handler}; -use ffmpeg::{ChannelLayout, Packet, filter, format, frame, media}; +use ffmpeg::{ChannelLayout, Packet, codec, filter, format, frame, media}; use ffmpeg::util::rational::Rational; struct MyEmulator { @@ -77,7 +77,7 @@ fn video_filter( } fn audio_filter( - audio_encoder: &ffmpeg::encoder::audio::Audio, + audio_encoder: &ffmpeg::codec::encoder::Audio, sample_rate: f64, ) -> Result { let mut afilter = filter::Graph::new(); @@ -99,6 +99,21 @@ fn audio_filter( .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) + { + afilter + .get("out") + .unwrap() + .sink() + .set_frame_size(audio_encoder.frame_size()); + } + } Ok(afilter) } @@ -129,7 +144,7 @@ impl MyEmulator { let mut video_output = octx.add_stream(vcodec).unwrap(); let mut video_encoder = video_output.codec().encoder().video().unwrap(); - video_encoder.set_bit_rate(64000); + 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, fps_int)); @@ -176,10 +191,12 @@ static bool ffmpeg_init_config(struct ff_config_param *params, audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); audio_encoder.set_channels(2); audio_encoder.set_channel_layout(ChannelLayout::STEREO); - - let audio_filter = audio_filter(&audio_encoder, av_info.timing.sample_rate).unwrap(); + audio_encoder.set_time_base(Rational::new(1, fps_int)); + audio_output.set_time_base(Rational::new(1, fps_int)); let mut audio_encoder = audio_encoder.open_as(acodec).unwrap(); + 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); audio_output.set_parameters(&audio_encoder); @@ -217,7 +234,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); let mut filtered_vframe = frame::Video::empty(); - while self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe).is_ok() { + while let Ok(..) = self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe) { if self.video_filter.get("in").unwrap().source().failed_requests() > 0 { println!("đŸŽĨ failed to put filter input frame"); } @@ -229,24 +246,27 @@ static bool ffmpeg_init_config(struct ff_config_param *params, if encoded_packet.size() > 0 { encoded_packet.set_stream(0); // use stream index... //encoded_packet.rescale_ts(Rational(1, 1), Rational(1, 60)); - encoded_packet.write(&mut self.octx).unwrap(); // AAA + encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA } //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA } } + eprintln!("Audio buffer length {}", self.audio_buf.len()); let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), self.audio_buf.len(), ChannelLayout::STEREO ); + aframe.set_channels(2); aframe.set_rate(32040); + aframe.set_pts(Some(frame)); let aplane: &mut [(i16, i16)] = aframe.plane_mut(0); aplane.copy_from_slice(self.audio_buf.as_ref()); + //eprintln!("src: {:?}, dest: {:?}", self.audio_buf, aplane); self.audio_buf.clear(); - let mut out = ffmpeg::Packet::empty(); self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); let mut filtered_aframe = frame::Audio::empty(); @@ -255,13 +275,14 @@ static bool ffmpeg_init_config(struct ff_config_param *params, println!("đŸŽĨ failed to put filter input frame"); } + filtered_aframe.set_pts(Some(frame)); self.audio_encoder.send_frame(&aframe).unwrap(); let mut encoded_packet = ffmpeg::Packet::empty(); while let Ok(..) = self.audio_encoder.receive_packet(&mut encoded_packet) { if encoded_packet.size() > 0 { encoded_packet.set_stream(1); //encoded_packet.rescale_ts(Rational(1, 1), Rational(1, 60)); - encoded_packet.write(&mut self.octx).unwrap(); // AAA + encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA } } } @@ -283,6 +304,10 @@ static bool ffmpeg_init_config(struct ff_config_param *params, .load_game(Some(path), data, None) .unwrap(); } + + pub fn end(&mut self) { + self.octx.write_trailer().unwrap(); + } } impl retro::wrapper::Handler for MyEmulator { @@ -352,11 +377,9 @@ impl retro::wrapper::Handler for MyEmulator { } self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); - /* 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 @@ -431,6 +454,7 @@ fn main() -> Fallible<()> { let mut packet = Packet::empty(); eprintln!("flushed: {:?}", emu.video_encoder.flush(&mut packet).unwrap()); + emu.end(); //octx.write_trailer().unwrap(); Ok(()) } From 7920d944d4d1786dd3f7701e3c63aa524c60fc7d Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sat, 24 Apr 2021 02:18:06 -0700 Subject: [PATCH 21/36] dedupe code for receiving and writing packets --- examples/ffmpeg_recorder.rs | 66 ++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index f95936e..03237ae 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -107,6 +107,7 @@ fn audio_filter( .capabilities() .contains(ffmpeg::codec::capabilities::Capabilities::VARIABLE_FRAME_SIZE) { + eprintln!("setting constant frame size {}", audio_encoder.frame_size()); afilter .get("out") .unwrap() @@ -136,7 +137,9 @@ impl MyEmulator { 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 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(); @@ -186,8 +189,9 @@ static bool ffmpeg_init_config(struct ff_config_param *params, */ - audio_encoder.set_bit_rate(64000); - audio_encoder.set_rate(44100); + audio_encoder.set_bit_rate(640000); + //audio_encoder.set_rate(44100); + audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); audio_encoder.set_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); audio_encoder.set_channels(2); audio_encoder.set_channel_layout(ChannelLayout::STEREO); @@ -197,10 +201,11 @@ static bool ffmpeg_init_config(struct ff_config_param *params, let mut audio_encoder = audio_encoder.open_as(acodec).unwrap(); 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); + //audio_encoder.set_rate(av_info.timing.sample_rate.round() as i32); audio_output.set_parameters(&audio_encoder); octx.write_header().unwrap(); + ffmpeg::format::context::output::dump(&octx, 0, None); let emu = MyEmulator { retro, @@ -225,6 +230,25 @@ static bool ffmpeg_init_config(struct ff_config_param *params, pin_emu.set_system_av_info(av_info); pin_emu } + + 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(); + while let Ok(..) = match encoder { + EncoderToWriteFrom::Video => self.video_encoder.receive_packet(&mut encoded_packet), + EncoderToWriteFrom::Audio => self.audio_encoder.receive_packet(&mut encoded_packet), + } { + if encoded_packet.size() > 0 { + encoded_packet.set_stream(stream_index); + eprintln!("pk pts {:?} dts {:?}", encoded_packet.pts(), encoded_packet.dts()); + encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA + } + } + } pub fn run(&mut self, frame: i64) { self.frame += 1; @@ -241,18 +265,9 @@ static bool ffmpeg_init_config(struct ff_config_param *params, filtered_vframe.set_pts(Some(frame)); self.video_encoder.send_frame(&filtered_vframe).unwrap(); - let mut encoded_packet = ffmpeg::Packet::empty(); - while let Ok(..) = self.video_encoder.receive_packet(&mut encoded_packet) { - if encoded_packet.size() > 0 { - encoded_packet.set_stream(0); // use stream index... - //encoded_packet.rescale_ts(Rational(1, 1), Rational(1, 60)); - encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA - } - //encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA - } + self.receive_and_write_packets(EncoderToWriteFrom::Video); } - eprintln!("Audio buffer length {}", self.audio_buf.len()); let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), @@ -261,8 +276,8 @@ static bool ffmpeg_init_config(struct ff_config_param *params, ); aframe.set_channels(2); aframe.set_rate(32040); - 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(); @@ -274,17 +289,15 @@ static bool ffmpeg_init_config(struct ff_config_param *params, 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); + //eprintln!("filtered: {:?}", faplane); + filtered_aframe.set_pts(Some(frame)); + eprintln!("f pts {:?}", filtered_aframe.pts()); filtered_aframe.set_pts(Some(frame)); self.audio_encoder.send_frame(&aframe).unwrap(); let mut encoded_packet = ffmpeg::Packet::empty(); - while let Ok(..) = self.audio_encoder.receive_packet(&mut encoded_packet) { - if encoded_packet.size() > 0 { - encoded_packet.set_stream(1); - //encoded_packet.rescale_ts(Rational(1, 1), Rational(1, 60)); - encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA - } - } + self.receive_and_write_packets(EncoderToWriteFrom::Audio); } } @@ -306,6 +319,10 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } pub fn end(&mut self) { + 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(); } } @@ -458,3 +475,8 @@ fn main() -> Fallible<()> { //octx.write_trailer().unwrap(); Ok(()) } + +enum EncoderToWriteFrom { + Video, + Audio, +} From 41da935d424596536f8bc8cb9f541caba1c6a938 Mon Sep 17 00:00:00 2001 From: Vivian Lim Date: Sun, 25 Apr 2021 19:31:06 -0700 Subject: [PATCH 22/36] actually write real audio (was sending aframe instead of filtered_aframe to encoder) and a few other fixes / shufflings --- examples/ffmpeg_recorder.rs | 98 +++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 03237ae..34d623a 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -170,7 +170,7 @@ let wavname = Path::new("out.wav"); 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); + //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(); @@ -190,19 +190,22 @@ static bool ffmpeg_init_config(struct ff_config_param *params, 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_format(audio_encoder.codec().unwrap().audio().unwrap().formats().unwrap().nth(0).unwrap()); 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, fps_int)); audio_output.set_time_base(Rational::new(1, fps_int)); let mut 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); - audio_output.set_parameters(&audio_encoder); octx.write_header().unwrap(); ffmpeg::format::context::output::dump(&octx, 0, None); @@ -238,14 +241,30 @@ static bool ffmpeg_init_config(struct ff_config_param *params, EncoderToWriteFrom::Audio => 1, }; let mut encoded_packet = ffmpeg::Packet::empty(); - while let Ok(..) = match encoder { - EncoderToWriteFrom::Video => self.video_encoder.receive_packet(&mut encoded_packet), - EncoderToWriteFrom::Audio => self.audio_encoder.receive_packet(&mut encoded_packet), - } { - if encoded_packet.size() > 0 { - encoded_packet.set_stream(stream_index); - eprintln!("pk pts {:?} dts {:?}", encoded_packet.pts(), encoded_packet.dts()); - encoded_packet.write_interleaved(&mut self.octx).unwrap(); // AAA + 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()); + match encoded_packet.write(&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; + } } } } @@ -258,14 +277,23 @@ static bool ffmpeg_init_config(struct ff_config_param *params, self.video_filter.get("in").unwrap().source().add(&vframe).unwrap(); let mut filtered_vframe = frame::Video::empty(); - while let Ok(..) = self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe) { - 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(); + loop { + match self.video_filter.get("out").unwrap().sink().frame(&mut filtered_vframe) { + Ok(..) => { + eprintln!("Got filtered video frame"); + 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); + self.receive_and_write_packets(EncoderToWriteFrom::Video); + }, + Err(e) => { + eprintln!("Error getting filtered video frame: {:?}", e); + break; + } + } } @@ -282,22 +310,30 @@ static bool ffmpeg_init_config(struct ff_config_param *params, //eprintln!("src: {:?}, dest: {:?}", self.audio_buf, aplane); self.audio_buf.clear(); + eprintln!("frame audio: {:?}", aframe); + self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); let mut filtered_aframe = frame::Audio::empty(); - while let Ok(..) = self.audio_filter.get("out").unwrap().sink().frame(&mut filtered_aframe) { - 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); - //eprintln!("filtered: {:?}", faplane); - filtered_aframe.set_pts(Some(frame)); - eprintln!("f pts {:?}", filtered_aframe.pts()); + loop { + match self.audio_filter.get("out").unwrap().sink().frame(&mut filtered_aframe) { + Ok(..) => { + eprintln!("Got filtered audio frame {:?}", filtered_aframe); + 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)); - filtered_aframe.set_pts(Some(frame)); - self.audio_encoder.send_frame(&aframe).unwrap(); - let mut encoded_packet = ffmpeg::Packet::empty(); - self.receive_and_write_packets(EncoderToWriteFrom::Audio); + //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; + } + } } } @@ -463,7 +499,7 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; //for frame in 0..60*10 { - for frame in 0..600 { + for frame in 0..1300 { eprintln!("đŸ–ŧī¸ frame: {}", frame); emu.run(frame); From 57530da214e42f3564b2567b5d8276ecbcaaf6c0 Mon Sep 17 00:00:00 2001 From: Viv Lim Date: Sun, 25 Jul 2021 22:02:40 -0700 Subject: [PATCH 23/36] able to record ok-sounding audio from picodrive --- .gitignore | 9 +++++++++ examples/ffmpeg_recorder.rs | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b956f11..6473eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,12 @@ **/*.rs.bk /.idea /Cargo.lock + +*.so +*.dll +*.dylib +*.mp4 +*.ogv +*.gb +*.bin +.DS_Store \ No newline at end of file diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 34d623a..7770848 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -160,7 +160,7 @@ let wavname = Path::new("out.wav"); av_info.geometry.aspect_ratio = 4.33; } if av_info.timing.sample_rate == 0.0 { - av_info.timing.sample_rate = 32040.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); @@ -303,7 +303,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, ChannelLayout::STEREO ); aframe.set_channels(2); - aframe.set_rate(32040); + aframe.set_rate(44100); 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()); @@ -355,9 +355,9 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } pub fn end(&mut self) { - self.video_encoder.send_eof().unwrap(); + self.video_encoder.send_eof(); self.receive_and_write_packets(EncoderToWriteFrom::Video); - self.audio_encoder.send_eof().unwrap(); + self.audio_encoder.send_eof(); self.receive_and_write_packets(EncoderToWriteFrom::Audio); self.octx.write_trailer().unwrap(); } From ea771c8dd7d51d4c63ebc16558b64e590371b25e Mon Sep 17 00:00:00 2001 From: Viv Lim Date: Mon, 26 Jul 2021 00:42:06 -0700 Subject: [PATCH 24/36] synced a/v on some players at least. ideally i'd like to write the header with the right time bases set. --- examples/ffmpeg_recorder.rs | 47 +++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 7770848..33cc495 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -82,7 +82,7 @@ fn audio_filter( ) -> 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", 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? @@ -145,14 +145,16 @@ let wavname = Path::new("out.wav"); 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, fps_int)); - video_output.set_time_base(Rational::new(1, fps_int)); - video_encoder.set_frame_rate(av_info.timing.fps.into()); + video_encoder.set_time_base(Rational::new(1, 60)); + video_encoder.set_frame_rate(Some(Rational::new(60, 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; @@ -197,8 +199,8 @@ static bool ffmpeg_init_config(struct ff_config_param *params, 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, fps_int)); - audio_output.set_time_base(Rational::new(1, fps_int)); + 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(); //audio_output.set_parameters(&audio_encoder); @@ -250,8 +252,18 @@ static bool ffmpeg_init_config(struct ff_config_param *params, 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()); - match encoded_packet.write(&mut self.octx) { + eprintln!("đŸ“Ļ Writing packet, pts {:?} dts {:?} size {}", encoded_packet.pts(), encoded_packet.dts(), encoded_packet.size()); + if let EncoderToWriteFrom::Video = encoder{ + //encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 44100)); // slow, ~30fps + encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 15360)); + } + if let EncoderToWriteFrom::Audio = encoder{ + //encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 44100)); // slow, ~30fps + encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 44100)); + } + 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), } @@ -274,17 +286,19 @@ static bool ffmpeg_init_config(struct ff_config_param *params, self.retro.run(); let mut vframe = self.video_frames.pop_front().unwrap(); + 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"); + 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)); + //filtered_vframe.set_pts(Some(frame)); self.video_encoder.send_frame(&filtered_vframe).unwrap(); self.receive_and_write_packets(EncoderToWriteFrom::Video); @@ -312,20 +326,21 @@ static bool ffmpeg_init_config(struct ff_config_param *params, eprintln!("frame audio: {:?}", aframe); + //aframe.set_pts(Some(frame)); + 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 {:?}", filtered_aframe); + 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)); + filtered_aframe.set_pts(Some(frame)); - //filtered_aframe.set_pts(Some(frame)); self.audio_encoder.send_frame(&filtered_aframe).unwrap(); self.receive_and_write_packets(EncoderToWriteFrom::Audio); }, @@ -429,7 +444,9 @@ impl retro::wrapper::Handler for MyEmulator { return true; } - self.video_encoder.set_frame_rate(system_av_info.timing.fps.into()); + //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); } @@ -499,7 +516,7 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; //for frame in 0..60*10 { - for frame in 0..1300 { + for frame in 0..800 { eprintln!("đŸ–ŧī¸ frame: {}", frame); emu.run(frame); From ee9678af68355b866b13ceca621bfe84b2b8a327 Mon Sep 17 00:00:00 2001 From: Viv Lim Date: Mon, 26 Jul 2021 01:10:48 -0700 Subject: [PATCH 25/36] cleaner pts rescaling at packet write time. writing headers can change time_base by design so just adapt to it --- examples/ffmpeg_recorder.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 33cc495..e45ffa8 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -253,14 +253,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, //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 let EncoderToWriteFrom::Video = encoder{ - //encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 44100)); // slow, ~30fps - encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 15360)); - } - if let EncoderToWriteFrom::Audio = encoder{ - //encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 44100)); // slow, ~30fps - encoded_packet.rescale_ts(Rational(1, 60), Rational(1, 44100)); - } + 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) { From 7c03aac38886ed647454a2767a7aa20ba8eb8711 Mon Sep 17 00:00:00 2001 From: Viv Lim Date: Tue, 27 Jul 2021 00:50:11 -0700 Subject: [PATCH 26/36] Don't panic if the emulator doesn't have frames ready to render, use angrylion with parallel-n64 --- examples/ffmpeg_recorder.rs | 144 +++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 60 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index e45ffa8..a45d2a0 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -13,6 +13,8 @@ use ferretro::retro; use ferretro::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo, SystemInfo}; use ferretro::retro::wrapper::{LibretroWrapper, Handler}; +use ferretro::retro::wrapped_types::{Variable2}; + use ffmpeg::{ChannelLayout, Packet, codec, filter, format, frame, media}; use ffmpeg::util::rational::Rational; @@ -278,71 +280,77 @@ static bool ffmpeg_init_config(struct ff_config_param *params, self.frame += 1; self.retro.run(); - let mut vframe = self.video_frames.pop_front().unwrap(); - 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(); + 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"); + 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; + } } - //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; } - } + + // Assumption: if there is a video frame, there is an audio frame. + let mut aframe = frame::Audio::new( + format::Sample::I16(format::sample::Type::Packed), + self.audio_buf.len(), + ChannelLayout::STEREO + ); + aframe.set_channels(2); + aframe.set_rate(44100); + 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); + + //aframe.set_pts(Some(frame)); + 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) } - let mut aframe = frame::Audio::new( - format::Sample::I16(format::sample::Type::Packed), - self.audio_buf.len(), - ChannelLayout::STEREO - ); - aframe.set_channels(2); - aframe.set_rate(44100); - 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); - - //aframe.set_pts(Some(frame)); - 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; - } - } - } } @@ -468,9 +476,25 @@ impl retro::wrapper::Handler for MyEmulator { } + 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); -} + } } #[derive(StructOpt)] From 3beb1a8519d99c16bc801c3f39c8b2021759a631 Mon Sep 17 00:00:00 2001 From: Viv Lim Date: Wed, 28 Jul 2021 23:54:40 -0700 Subject: [PATCH 27/36] fix audio sync when being played by quicktime the thing I was doing is on the audio, i was setting the pts (presentation timestamp) after the frame was filtered by ffmpeg, which could be frames later, and could be batched. i set the pts before filtering, but that made things worse. for some reason, the audio filter is remapping the pts I use (frame count, 1/60) to the time base 1/44100. the video filter does not do this, which is why I specifically remap it later I had to make that remap step I added later skip the audio channel --- examples/ffmpeg_recorder.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index a45d2a0..2496ed3 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -154,7 +154,7 @@ let wavname = Path::new("out.wav"); 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(60, 1))); + video_encoder.set_frame_rate(Some(Rational::new(fps_int, 1))); //video_encoder.set_frame_rate(av_info.timing.fps.into()); @@ -255,7 +255,9 @@ static bool ffmpeg_init_config(struct ff_config_param *params, //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()); - encoded_packet.rescale_ts(Rational(1, 60), self.octx.stream(stream_index).unwrap().time_base()); + 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) { @@ -314,6 +316,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, ); 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()); @@ -322,7 +325,6 @@ static bool ffmpeg_init_config(struct ff_config_param *params, eprintln!("frame audio: {:?}", aframe); - //aframe.set_pts(Some(frame)); eprintln!("🎞 queue frame pts {:?}", aframe.pts()); self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); @@ -335,7 +337,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, println!("đŸŽĨ failed to put filter input frame"); } //let faplane: &[f32] = filtered_aframe.plane(0); - filtered_aframe.set_pts(Some(frame)); + //filtered_aframe.set_pts(Some(frame)); self.audio_encoder.send_frame(&filtered_aframe).unwrap(); self.receive_and_write_packets(EncoderToWriteFrom::Audio); @@ -463,7 +465,7 @@ impl retro::wrapper::Handler for MyEmulator { 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.video_encoder.set_aspect_ratio(geometry.aspect_ratio as f64); self.av_info.geometry = geometry; let pixel_format = match self.video_pixel_format { format::Pixel::RGB555 => PixelFormat::ARGB1555, From 3cad3b1e29cfc76116399e3475113b59ee22e312 Mon Sep 17 00:00:00 2001 From: viv Date: Sun, 1 Aug 2021 00:08:18 -0700 Subject: [PATCH 28/36] Don't try to write an audio frame if there aren't any planes --- examples/ffmpeg_recorder.rs | 57 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 2496ed3..c63057a 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -308,43 +308,44 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } } - // Assumption: if there is a video frame, there is an audio frame. let mut aframe = frame::Audio::new( format::Sample::I16(format::sample::Type::Packed), self.audio_buf.len(), ChannelLayout::STEREO ); - 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(); + 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!("frame audio: {:?}", aframe); - eprintln!("🎞 queue frame pts {:?}", aframe.pts()); - self.audio_filter.get("in").unwrap().source().add(&aframe).unwrap(); + 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 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; } - //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; } } } From 4a38b99c5e5a13546e8217cc2858c578b471a763 Mon Sep 17 00:00:00 2001 From: viv Date: Sat, 31 Jul 2021 23:37:29 -0700 Subject: [PATCH 29/36] add unserialize flag to ffmpeg_recorder --- examples/ffmpeg_recorder.rs | 18 ++++++++++++++++++ src/retro/loading.rs | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index c63057a..72474a3 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -380,6 +380,17 @@ 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) -> 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 retro::wrapper::Handler for MyEmulator { @@ -511,6 +522,9 @@ 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, @@ -535,6 +549,10 @@ fn main() -> Fallible<()> { emu.frame_properties_locked = true; + if let Some(state) = opt.state { + emu.unserialize(state).unwrap(); + } + //for frame in 0..60*10 { for frame in 0..800 { eprintln!("đŸ–ŧī¸ frame: {}", frame); diff --git a/src/retro/loading.rs b/src/retro/loading.rs index 552e458..a0da355 100644 --- a/src/retro/loading.rs +++ b/src/retro/loading.rs @@ -142,6 +142,12 @@ impl LibretroApi { } } pub fn unserialize(&self, data: &[u8]) -> Fallible<()> { + // validate size of the data + let size: usize = unsafe { (&self.core_api.retro_serialize_size)() }; + if data.len() != size { + return Err(failure::err_msg(format!("Size of data {} doesn't match this core's expected size {}", data.len(), size))) + } + if unsafe { (&self.core_api.retro_unserialize)(data.as_ptr() as *const c_void, data.len()) } { Ok(()) } else { From 2b73e3d836f0551886439a13f885553711b5cbb8 Mon Sep 17 00:00:00 2001 From: lifning <> Date: Tue, 10 Aug 2021 17:59:00 -0700 Subject: [PATCH 30/36] document env callbacks & continue sketching component api --- examples/ffmpeg_recorder.rs | 14 +- examples/sdl2_emulator.rs | 14 +- src/components/mod.rs | 261 +++++++++++++++++++++++++++++ src/lib.rs | 7 + src/retro/constants.rs | 18 +- src/retro/wrapper.rs | 323 ++++++++++++++++++++++++++++++++++-- 6 files changed, 598 insertions(+), 39 deletions(-) create mode 100644 src/components/mod.rs diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 72474a3..f9c5e95 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -11,7 +11,7 @@ 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}; @@ -234,7 +234,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 } @@ -393,7 +393,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } } -impl retro::wrapper::Handler for MyEmulator { +impl retro::wrapper::RetroCallbacks for MyEmulator { fn libretro_core(&mut self) -> &mut LibretroWrapper { &mut self.retro } @@ -454,7 +454,7 @@ impl retro::wrapper::Handler for MyEmulator { true } - fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { if self.frame_properties_locked { return true; } @@ -466,11 +466,11 @@ impl retro::wrapper::Handler for MyEmulator { 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); + self.set_geometry(&system_av_info.geometry); true } - fn set_geometry(&mut self, geometry: GameGeometry) -> bool { + fn set_geometry(&mut self, geometry: &GameGeometry) -> bool { if self.frame_properties_locked { return true; } @@ -478,7 +478,7 @@ impl retro::wrapper::Handler for MyEmulator { 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, diff --git a/examples/sdl2_emulator.rs b/examples/sdl2_emulator.rs index 293dc90..18b3d1b 100644 --- a/examples/sdl2_emulator.rs +++ b/examples/sdl2_emulator.rs @@ -204,7 +204,7 @@ impl Drop for MyEmulator { } } -impl retro::wrapper::Handler for MyEmulator { +impl retro::wrapper::RetroCallbacks for MyEmulator { fn libretro_core(&mut self) -> &mut LibretroWrapper { &mut self.retro } @@ -262,8 +262,6 @@ 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() } @@ -305,9 +303,9 @@ 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; + fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> bool { + self.set_geometry(&av_info.geometry); + self.av_info = av_info.clone(); true } @@ -334,10 +332,10 @@ impl retro::wrapper::Handler for MyEmulator { true } - fn set_geometry(&mut self, geom: GameGeometry) -> bool { + fn set_geometry(&mut self, geom: &GameGeometry) -> bool { let _ = self.canvas.window_mut().set_size(geom.base_width, geom.base_height); let _ = self.canvas.set_logical_size(geom.base_width, geom.base_height); - self.av_info.geometry = geom; + self.av_info.geometry = geom.clone(); true } diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..3e63a0b --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,261 @@ +use crate::prelude::*; +use std::os::raw::c_uint; +use std::rc::Rc; +use crate::retro::ffi::{Message, PixelFormat, HwRenderCallback, SensorInterface, CameraCallback, LogCallback, PerfCallback, LocationCallback, SystemAvInfo, GetProcAddressInterface, MemoryMap, GameGeometry, Language, LogLevel, RumbleEffect, Time, PerfTick, PerfCounter, SensorAction}; +use std::path::PathBuf; + +#[rustfmt::skip] +#[allow(unused)] +pub trait RetroAvCommonComponent { + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { false } +} + +#[rustfmt::skip] +#[allow(unused)] +pub trait RetroVideoComponent: RetroAvCommonComponent { + fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) {} + // -- environment callbacks -- + fn set_rotation(&mut self, rotation: EnvRotation) -> bool { false } + fn get_overscan(&mut self) -> Option { None } + fn set_pixel_format(&mut self, format: PixelFormat) -> bool { false } + fn set_hw_render(&mut self, hw_render_callback: &HwRenderCallback) -> bool { false } + fn set_geometry(&mut self, game_geometry: &GameGeometry) -> bool { false } +} + +#[rustfmt::skip] +#[allow(unused)] +pub trait RetroAudioComponent: RetroAvCommonComponent { + fn audio_sample(&mut self, left: i16, right: i16) {} + fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { 0 } +} + +pub struct RetroComponentBase { + retro: LibretroWrapper, + pub video_comps: Vec>, + pub audio_comps: Vec>, +} + +#[allow(unused)] +impl RetroCallbacks for RetroComponentBase { + fn libretro_core(&mut self) -> &mut LibretroWrapper { + &mut self.retro + } + + fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) { + for vc in &mut self.video_comps { + // FIXME: can't have more than 1 ref! + // get_mut fails if one component struct impls both Audio&Video! + Rc::get_mut(vc).unwrap().video_refresh(data, width, height, pitch); + } + } + + fn video_refresh_dupe(&mut self, width: c_uint, height: c_uint, pitch: c_uint) { + todo!() + } + + fn audio_sample(&mut self, left: i16, right: i16) { + todo!() + } + + fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { + todo!() + } + + fn input_poll(&mut self) { + todo!() + } + + fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { + todo!() + } + + fn set_rotation(&mut self, rotation: EnvRotation) -> bool { + self.video_comps.iter_mut() + .map(|x| Rc::get_mut(x).unwrap().set_rotation(rotation)) + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + } + + fn get_overscan(&mut self) -> Option { + self.video_comps.iter_mut() + .map(|x| Rc::get_mut(x).unwrap().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) -> bool { + todo!() + } + + fn shutdown(&mut self) -> bool { + todo!() + } + + fn set_performance_level(&mut self, level: c_uint) -> bool { + todo!() + } + + fn get_system_directory(&mut self) -> Option { + todo!() + } + + fn set_pixel_format(&mut self, format: PixelFormat) -> bool { + self.video_comps.iter_mut() + .map(|x| Rc::get_mut(x).unwrap().set_pixel_format(format)) + .all(|x| x) + } + + fn set_input_descriptors(&mut self, input_descriptors: Vec) -> bool { + todo!() + } + + fn set_hw_render(&mut self, hw_render_callback: &HwRenderCallback) -> bool { + self.video_comps.iter_mut() + .map(|x| Rc::get_mut(x).unwrap().set_hw_render(hw_render_callback)) + .all(|x| x) + } + + fn get_variable(&mut self, key: &str) -> Option { + todo!() + } + + fn set_variables(&mut self, variables: Vec) -> bool { + todo!() + } + + fn get_variable_update(&mut self) -> Option { + todo!() + } + + fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { + todo!() + } + + fn get_libretro_path(&mut self) -> Option { + todo!() + } + + fn get_input_device_capabilities(&mut self) -> Option { + todo!() + } + + fn get_sensor_interface(&mut self) -> Option { + todo!() + } + + fn get_camera_interface(&mut self) -> Option { + todo!() + } + + fn get_log_interface(&mut self) -> Option { + todo!() + } + + fn get_perf_interface(&mut self) -> Option { + todo!() + } + + fn get_location_interface(&mut self) -> Option { + todo!() + } + + fn get_core_assets_directory(&mut self) -> Option { + todo!() + } + + fn get_save_directory(&mut self) -> Option { + todo!() + } + + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { + // TODO: avoid calling twice for components that are registered in both simultaneously + + let mut result = false; + + for vc in &mut self.video_comps { + result |= Rc::get_mut(vc).unwrap().set_system_av_info(system_av_info); + } + + for ac in &mut self.audio_comps { + result |= Rc::get_mut(ac).unwrap().set_system_av_info(system_av_info); + } + + result + } + + fn set_proc_address_callback(&mut self, cb: GetProcAddressInterface) -> bool { + todo!() + } + + fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { + todo!() + } + + fn set_controller_info(&mut self, controller_info: Vec) -> bool { + todo!() + } + + fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool { + todo!() + } + + fn set_geometry(&mut self, game_geometry: &GameGeometry) -> bool { + self.video_comps.iter_mut() + .map(|x| Rc::get_mut(x).unwrap().set_geometry(game_geometry)) + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit + } + + fn get_username(&mut self) -> Option { + todo!() + } + + fn get_language(&mut self) -> Option { + todo!() + } + + fn log_print(&mut self, level: LogLevel, msg: &str) { + todo!() + } + + fn set_rumble_state(&mut self, port: c_uint, effect: RumbleEffect, strength: u16) -> bool { + todo!() + } + + fn perf_get_time_usec_cb(&mut self) -> Time { + todo!() + } + + fn perf_get_counter_cb(&mut self) -> PerfTick { + todo!() + } + + fn perf_get_cpu_features_cb(&mut self) -> u64 { + todo!() + } + + fn perf_log_cb(&mut self) { + todo!() + } + + fn perf_register_cb(&mut self, counter: &mut PerfCounter) { + todo!() + } + + fn perf_start_cb(&mut self, counter: &mut PerfCounter) { + todo!() + } + + fn perf_stop_cb(&mut self, counter: &mut PerfCounter) { + todo!() + } + + fn set_sensor_state(&mut self, port: c_uint, action: SensorAction, rate: c_uint) -> bool { + todo!() + } + + fn get_sensor_input(&mut self, port: c_uint, id: c_uint) -> f32 { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index e71bd1b..b3cc73e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,10 @@ extern crate failure; extern crate libloading; pub mod retro; +pub mod components; + +pub mod prelude { + pub use crate::retro::constants::*; + pub use crate::retro::wrapped_types::*; + pub use crate::retro::wrapper::{RetroCallbacks, LibretroWrapper}; +} diff --git a/src/retro/constants.rs b/src/retro/constants.rs index 98119ed..50ef118 100644 --- a/src/retro/constants.rs +++ b/src/retro/constants.rs @@ -4,7 +4,7 @@ use super::ffi::*; // NB: commented-out stuff is from newer versions of libretro.h not yet represented in libretro-sys -#[derive(TryFromPrimitive, IntoPrimitive, Debug, Clone, Copy)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum DeviceType { None = DEVICE_NONE, @@ -23,7 +23,7 @@ impl DeviceType { } } -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum InputIndex { Left = DEVICE_INDEX_ANALOG_LEFT, @@ -31,7 +31,7 @@ pub enum InputIndex { // Button = DEVICE_INDEX_ANALOG_BUTTON, } -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum JoypadButton { B = DEVICE_ID_JOYPAD_B, @@ -52,14 +52,14 @@ pub enum JoypadButton { R3 = DEVICE_ID_JOYPAD_R3, } -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum AnalogAxis { X = DEVICE_ID_ANALOG_X, Y = DEVICE_ID_ANALOG_Y, } -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum MouseButton { X = DEVICE_ID_MOUSE_X, @@ -73,7 +73,7 @@ pub enum MouseButton { HorizWheelDown = DEVICE_ID_MOUSE_HORIZ_WHEELDOWN, } -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum LightGunButton { // ScreenX = DEVICE_ID_LIGHTGUN_SCREEN_X, @@ -95,7 +95,7 @@ pub enum LightGunButton { Pause = DEVICE_ID_LIGHTGUN_PAUSE, } -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum PointerStat { X = DEVICE_ID_POINTER_X, @@ -103,7 +103,7 @@ pub enum PointerStat { Pressed = DEVICE_ID_POINTER_PRESSED, } -#[derive(TryFromPrimitive, IntoPrimitive)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum EnvRotation { None = 0, @@ -113,7 +113,7 @@ pub enum EnvRotation { } // TODO: experimental calls -#[derive(TryFromPrimitive, IntoPrimitive, Debug)] +#[derive(TryFromPrimitive, IntoPrimitive, Clone, Copy, Debug)] #[repr(u32)] pub enum EnvCmd { SetRotation = ENVIRONMENT_SET_ROTATION, diff --git a/src/retro/wrapper.rs b/src/retro/wrapper.rs index 0152361..2a3bdf7 100644 --- a/src/retro/wrapper.rs +++ b/src/retro/wrapper.rs @@ -30,48 +30,339 @@ extern "C" { #[rustfmt::skip] #[allow(unused)] -pub trait Handler: Unpin + 'static { +pub trait RetroCallbacks: Unpin + 'static { fn libretro_core(&mut self) -> &mut LibretroWrapper; - fn delegate_handler(&self) -> Option<&mut dyn Handler> { None } + fn delegate_handler(&self) -> Option<&mut dyn RetroCallbacks> { None } // -- main callbacks -- fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) {} + /// Called instead of video_refresh when the core reports a duplicate frame (NULL). + fn video_refresh_dupe(&mut self, width: c_uint, height: c_uint, pitch: c_uint) {} fn audio_sample(&mut self, left: i16, right: i16) {} fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { stereo_pcm.len() } fn input_poll(&mut self) {} fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { 0 } // -- environment callbacks -- + /// Sets screen rotation of graphics. + /// Is only implemented if rotation can be accelerated by hardware. + /// Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, + /// 270 degrees counter-clockwise respectively. fn set_rotation(&mut self, rotation: EnvRotation) -> bool { false } + /// Boolean value whether or not the implementation should use overscan, + /// or crop away overscan. fn get_overscan(&mut self) -> Option { None } - fn get_can_dupe(&mut self) -> Option { None } + /// Sets a message to be displayed in implementation-specific manner + /// for a certain amount of 'frames'. + /// Should not be used for trivial messages, which should simply be + /// logged via [Self::get_log_interface] (or as a + /// fallback, stderr). fn set_message(&mut self, message: Message) -> bool { false } + /// Requests the frontend to shutdown. + /// Should only be used if game has a specific + /// way to shutdown the game from a menu item or similar. fn shutdown(&mut self) -> bool { false } + /// Gives a hint to the frontend how demanding this implementation + /// is on a system. E.g. reporting a level of 2 means + /// this implementation should run decently on all frontends + /// of level 2 and up. + /// + /// It can be used by the frontend to potentially warn + /// about too demanding implementations. + /// + /// The levels are "floating". + /// + /// This function can be called on a per-game basis, + /// as certain games an implementation can play might be + /// particularly demanding. + /// If called, it should be called in [libretro_sys::CoreAPI::retro_load_game]. fn set_performance_level(&mut self, level: c_uint) -> bool { false } + /// Returns the "system" directory of the frontend. + /// This directory can be used to store system specific + /// content such as BIOSes, configuration data, etc. + /// The returned value can be `None`. + /// If so, no such directory is defined, + /// and it's up to the implementation to find a suitable directory. + /// + /// NOTE: Some cores used this folder also for "save" data such as + /// memory cards, etc, for lack of a better place to put it. + /// This is now discouraged, and if possible, cores should try to + /// use the new [Self::get_save_directory]. fn get_system_directory(&mut self) -> Option { None } + /// Sets the internal pixel format used by the implementation. + /// The default pixel format is [libretro_sys::PixelFormat::ARGB1555]. + /// This pixel format however, is deprecated (see enum retro_pixel_format). + /// If the call returns false, the frontend does not support this pixel + /// format. + /// + /// The core should call this function inside [libretro_sys::CoreAPI::retro_load_game] or + /// [Self::set_system_av_info]. fn set_pixel_format(&mut self, format: PixelFormat) -> bool { false } + /// Sets an array of [crate::prelude::InputDescriptor2]. + /// It is up to the frontend to present this in a usable way. + /// This function can be called at any time, but it is recommended + /// for the core to call it as early as possible. fn set_input_descriptors(&mut self, input_descriptors: Vec) -> bool { false } - fn set_hw_render(&mut self, hw_render_callback: HwRenderCallback) -> bool { false } + /// Sets an interface to let a libretro core render with + /// hardware acceleration. + /// The core should call this in [libretro_sys::CoreAPI::retro_load_game]. + /// If successful, libretro cores will be able to render to a + /// frontend-provided framebuffer. + /// The size of this framebuffer will be at least as large as + /// max_width/max_height provided in [libretro_sys::CoreAPI::retro_get_system_av_info]. + /// If HW rendering is used, pass only [libretro_sys::HW_FRAME_BUFFER_VALID] or + /// NULL to [libretro_sys::VideoRefreshFn]. + fn set_hw_render(&mut self, hw_render_callback: &HwRenderCallback) -> bool { false } + /// Interface to acquire user-defined information from environment + /// that cannot feasibly be supported in a multi-system way. + /// 'key' should be set to a key which has already been set by + /// [Self::set_variables]. fn get_variable(&mut self, key: &str) -> Option { None } + /// Allows an implementation to signal the environment + /// which variables it might want to check for later using + /// [Self::get_variable]. + /// This allows the frontend to present these variables to + /// a user dynamically. + /// The core should call this for the first time as early as + /// possible (ideally in [libretro_sys::CoreAPI::retro_set_environment]). + /// Afterward it may be called again for the core to communicate + /// updated options to the frontend, but the number of core + /// options must not change from the number in the initial call. + /// + /// [crate::prelude::Variable2::key] should be namespaced to not collide + /// with other implementations' keys. E.g. A core called + /// 'foo' should use keys named as 'foo_option'. + /// + /// [crate::prelude::Variable2::description] should contain a human readable + /// description of the key. + /// + /// [crate::prelude::Variable2::options] should contain the list of expected values. + /// The number of possible options should be very limited, + /// i.e. it should be feasible to cycle through options + /// without a keyboard. The first entry should be treated as a default. + /// + /// Only strings are operated on. The possible values will + /// generally be displayed and stored as-is by the frontend. fn set_variables(&mut self, variables: Vec) -> bool { false } + /// Result is set to true if some variables are updated by + /// frontend since last call to [Self::get_variable]. + /// Variables should be queried with [Self::get_variable]. fn get_variable_update(&mut self) -> Option { None } + /// If true, the libretro implementation supports calls to + /// [libretro_sys::CoreAPI::retro_load_game] with NULL as argument. + /// Used by cores which can run without particular game data. + /// This should be called within [libretro_sys::CoreAPI::retro_set_environment] only. fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { false } + /// Retrieves the absolute path from where this libretro + /// implementation was loaded. + /// `None` is returned if the libretro was loaded statically + /// (i.e. linked statically to frontend), or if the path cannot be + /// determined. + /// Mostly useful in cooperation with [Self::set_support_no_game] as assets can + /// be loaded without ugly hacks. fn get_libretro_path(&mut self) -> Option { None } + /// Gets a bitmask telling which device type are expected to be + /// handled properly in a call to retro_input_state_t. + /// Devices which are not handled or recognized always return + /// 0 in [Self::input_state]. + /// Example bitmask: caps = (1 << [libretro_sys::DEVICE_JOYPAD]) | (1 << [libretro_sys::DEVICE_ANALOG]). + /// Should only be called in [libretro_sys::CoreAPI::retro_run]. fn get_input_device_capabilities(&mut self) -> Option { None } + /// Gets access to the sensor interface. + /// The purpose of this interface is to allow + /// setting state related to sensors such as polling rate, + /// enabling/disable it entirely, etc. + /// Reading sensor state is done via the normal + /// [Self::input_state] API. fn get_sensor_interface(&mut self) -> Option { None } + /// Gets an interface to a video camera driver. + /// A libretro core can use this interface to get access to a + /// video camera. + /// New video frames are delivered in a callback in same + /// thread as [libretro_sys::CoreAPI::retro_run]. + /// + /// GET_CAMERA_INTERFACE should be called in [libretro_sys::CoreAPI::retro_load_game](). + /// + /// Depending on the camera implementation used, camera frames + /// will be delivered as a raw framebuffer, + /// or as an OpenGL texture directly. + /// + /// The core has to tell the frontend here which types of + /// buffers can be handled properly. + /// An OpenGL texture can only be handled when using a + /// libretro GL core ([Self::set_hw_render]). + /// It is recommended to use a libretro GL core when + /// using camera interface. + /// + /// The camera is not started automatically. The retrieved start/stop + /// functions must be used to explicitly + /// start and stop the camera driver. fn get_camera_interface(&mut self) -> Option { None } + /// Gets an interface for logging. This is useful for + /// logging in a cross-platform way + /// as certain platforms cannot use stderr for logging. + /// It also allows the frontend to + /// show logging information in a more suitable way. + /// If this interface is not used, libretro cores should + /// log to stderr as desired. fn get_log_interface(&mut self) -> Option { None } + /// Gets an interface for performance counters. This is useful + /// for performance logging in a cross-platform way and for detecting + /// architecture-specific features, such as SIMD support. fn get_perf_interface(&mut self) -> Option { None } + /// Gets access to the location interface. + /// The purpose of this interface is to be able to retrieve + /// location-based information from the host device, + /// such as current latitude / longitude. fn get_location_interface(&mut self) -> Option { None } + /// Returns the "core assets" directory of the frontend. + /// This directory can be used to store specific assets that the + /// core relies upon, such as art assets, + /// input data, etc etc. + /// The returned value can be `None`. + /// If so, no such directory is defined, + /// and it's up to the implementation to find a suitable directory. fn get_core_assets_directory(&mut self) -> Option { None } + /// Returns the "save" directory of the frontend, unless there is no + /// save directory available. The save directory should be used to + /// store SRAM, memory cards, high scores, etc, if the libretro core + /// cannot use the regular memory interface ([libretro_sys::CoreAPI::retro_get_memory_data]). + /// + /// If the frontend cannot designate a save directory, it will return + /// `None` to indicate that the core should attempt to operate without a + /// save directory set. + /// + /// NOTE: early libretro cores used the system directory for save + /// files. Cores that need to be backwards-compatible can still check + /// [Self::get_system_directory]. fn get_save_directory(&mut self) -> Option { None } - fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { false } + /// Sets a new av_info structure. This can only be called from + /// within [libretro_sys::CoreAPI::retro_run]. + /// This should *only* be used if the core is completely altering the + /// internal resolutions, aspect ratios, timings, sampling rate, etc. + /// Calling this can require a full reinitialization of video/audio + /// drivers in the frontend, + /// + /// so it is important to call it very sparingly, and usually only with + /// the users explicit consent. + /// An eventual driver reinitialize will happen so that video and + /// audio callbacks + /// happening after this call within the same [libretro_sys::CoreAPI::retro_run] call will + /// target the newly initialized driver. + /// + /// This callback makes it possible to support configurable resolutions + /// in games, which can be useful to + /// avoid setting the "worst case" in `max_width`/`max_height`. + /// + /// ***HIGHLY RECOMMENDED*** Do not call this callback every time + /// resolution changes in an emulator core if it's + /// expected to be a temporary change, for the reasons of possible + /// driver reinitialization. + /// This call is not a free pass for not trying to provide + /// correct values in [libretro_sys::CoreAPI::retro_get_system_av_info]. If you need to change + /// things like aspect ratio or nominal width/height, + /// use [Self::set_geometry], which is a softer variant + /// of [Self::set_system_av_info]. + /// + /// If this returns false, the frontend does not acknowledge a + /// changed av_info struct. + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { false } + /// Allows a libretro core to announce support for the + /// get_proc_address() interface. + /// This interface allows for a standard way to extend libretro where + /// use of environment calls are too indirect, + /// e.g. for cases where the frontend wants to call directly into the core. + /// + /// If a core wants to expose this interface, [Self::set_proc_address_callback] + /// **MUST** be called from within [libretro_sys::CoreAPI::retro_set_environment]. fn set_proc_address_callback(&mut self, cb: GetProcAddressInterface) -> bool { false } + /// This environment call introduces the concept of libretro "subsystems". + /// A subsystem is a variant of a libretro core which supports + /// different kinds of games. + /// The purpose of this is to support e.g. emulators which might + /// have special needs, e.g. Super Nintendo's Super GameBoy, Sufami Turbo. + /// It can also be used to pick among subsystems in an explicit way + /// if the libretro implementation is a multi-system emulator itself. + /// + /// Loading a game via a subsystem is done with [libretro_sys::CoreAPI::retro_load_game_special], + /// and this environment call allows a libretro core to expose which + /// subsystems are supported for use with [libretro_sys::CoreAPI::retro_load_game_special]. + /// + /// If a core wants to expose this interface, [Self::set_subsystem_info] + /// **MUST** be called from within [libretro_sys::CoreAPI::retro_set_environment]. fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { false } + /// This environment call lets a libretro core tell the frontend + /// which controller subclasses are recognized in calls to + /// [libretro_sys::CoreAPI::retro_set_controller_port_device]. + /// + /// Some emulators such as Super Nintendo support multiple lightgun + /// types which must be specifically selected from. It is therefore + /// sometimes necessary for a frontend to be able to tell the core + /// about a special kind of input device which is not specifcally + /// provided by the Libretro API. + /// + /// In order for a frontend to understand the workings of those devices, + /// they must be defined as a specialized subclass of the generic device + /// types already defined in the libretro API. + /// + /// The core must pass an array of [crate::prelude::ControllerDescription2]. Each element of the + /// array corresponds to the ascending port index + /// that is passed to [libretro_sys::CoreAPI::retro_set_controller_port_device] when that function + /// is called to indicate to the core that the frontend has changed the + /// active device subclass. + /// + /// The ascending input port indexes provided by the core in the struct + /// are generally presented by frontends as ascending User # or Player #, + /// such as Player 1, Player 2, Player 3, etc. Which device subclasses are + /// supported can vary per input port. + /// + /// Each entry in the controller_info array specifies the names and + /// codes of all device subclasses that are available for the corresponding + /// User or Player, beginning with the generic Libretro device that the + /// subclasses are derived from. The second inner element of each entry is the + /// total number of subclasses that are listed in the [crate::prelude::ControllerDescription2]. + /// + /// NOTE: Even if special device types are set in the libretro core, + /// libretro should only poll input based on the base input device types. fn set_controller_info(&mut self, controller_info: Vec) -> bool { false } + /// This environment call lets a libretro core tell the frontend + /// about the memory maps this core emulates. + /// This can be used to implement, for example, cheats in a core-agnostic way. + /// + /// Should only be used by emulators; it doesn't make much sense for + /// anything else. + /// It is recommended to expose all relevant pointers through + /// [libretro_sys::CoreAPI::retro_get_memory_data] and + /// [libretro_sys::CoreAPI::retro_get_memory_size] as well. + /// + /// Can be called from [libretro_sys::CoreAPI::retro_init] and [libretro_sys::CoreAPI::retro_load_game]. fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool { false } - fn set_geometry(&mut self, game_geometry: GameGeometry) -> bool { false } + /// This environment call is similar to [Self::set_system_av_info] for changing + /// video parameters, but provides a guarantee that drivers will not be + /// reinitialized. + /// This can only be called from within [libretro_sys::CoreAPI::retro_run]. + /// + /// The purpose of this call is to allow a core to alter nominal + /// width/heights as well as aspect ratios on-the-fly, which can be + /// useful for some emulators to change in run-time. + /// + /// max_width/max_height arguments are ignored and cannot be changed + /// with this call as this could potentially require a reinitialization or a + /// non-constant time operation. + /// If max_width/max_height are to be changed, [Self::set_system_av_info] is required. + /// + /// A frontend must guarantee that this environment call completes in + /// constant time. + fn set_geometry(&mut self, game_geometry: &GameGeometry) -> bool { false } + /// Returns the specified username of the frontend, if specified by the user. + /// This username can be used as a nickname for a core that has online facilities + /// or any other mode where personalization of the user is desirable. + /// The returned value can be `None`. + /// If this environ callback is used by a core that requires a valid username, + /// a default username should be specified by the core. fn get_username(&mut self) -> Option { None } + /// Returns the specified language of the frontend, if specified by the user. + /// It can be used by the core for localization purposes. fn get_language(&mut self) -> Option { None } // fn set_serialization_quirks(&mut self, quirks: &mut u64) -> bool { false } @@ -91,7 +382,7 @@ pub trait Handler: Unpin + 'static { #[derive(Default)] struct StaticCallbacks { - handler: Option>, + handler: Option>, } unsafe impl Sync for StaticCallbacks {} @@ -148,7 +439,7 @@ impl StaticCallbacks { match parsed_cmd? { EnvCmd::SetRotation => handler.set_rotation(Self::enum_from_void(data)?), EnvCmd::GetOverscan => Self::clone_into_void(data, &handler.get_overscan()?)?, - EnvCmd::GetCanDupe => Self::clone_into_void(data, &handler.get_can_dupe()?)?, + EnvCmd::GetCanDupe => Self::clone_into_void(data, &true)?, EnvCmd::SetMessage => handler.set_message(Self::from_void::(data)?.clone()), EnvCmd::Shutdown => handler.shutdown(), EnvCmd::SetPerformanceLevel => handler.set_performance_level(*Self::from_void(data)?), @@ -277,7 +568,7 @@ impl StaticCallbacks { } EnvCmd::GetSaveDirectory => Self::path_into_void(data, handler.get_save_directory()?)?, EnvCmd::SetSystemAvInfo => { - handler.set_system_av_info(Self::from_void::(data)?.clone()) + handler.set_system_av_info(Self::from_void::(data)?) } EnvCmd::SetProcAddressCallback => { let gpa = Self::from_void::(data)?; @@ -309,7 +600,7 @@ impl StaticCallbacks { } // TODO (experimental) EnvCmd::SetMemoryMaps => {}, EnvCmd::SetGeometry => { - handler.set_geometry(Self::from_void::(data)?.clone()) + handler.set_geometry(Self::from_void::(data)?) } EnvCmd::GetUsername => Self::string_into_void(data, handler.get_username()?)?, EnvCmd::GetLanguage => Self::clone_into_void(data, &handler.get_language()?)?, @@ -333,12 +624,14 @@ impl StaticCallbacks { height: c_uint, pitch: usize, ) { - if !data.is_null() && data != HW_FRAME_BUFFER_VALID { - if let Some(cb) = unsafe { CB_SINGLETON.handler.as_mut() } { + if let Some(cb) = unsafe { CB_SINGLETON.handler.as_mut() } { + if data.is_null() { + cb.video_refresh_dupe(width, height, pitch as c_uint); + } else if data != HW_FRAME_BUFFER_VALID { let data = data as *const u8; let len = pitch * (height as usize); let slice = unsafe { from_raw_parts(data, len) }; - cb.video_refresh(slice, width, height, pitch as u32); + cb.video_refresh(slice, width, height, pitch as c_uint); } } } @@ -571,9 +864,9 @@ impl Deref for LibretroWrapper { // a note on lifetimes: we explicitly lie about them here because as long as they live as long as // the library wrapper itself we're good (we wipe our 'static references on drop() too) -pub fn set_handler(handler: Pin<&'_ mut (dyn Handler + '_)>) { +pub fn set_handler(handler: Pin<&'_ mut (dyn RetroCallbacks + '_)>) { unsafe { - let ptr = handler.get_unchecked_mut() as *mut dyn Handler; + let ptr = handler.get_unchecked_mut() as *mut dyn RetroCallbacks; CB_SINGLETON .handler .replace(Pin::new_unchecked(ptr.as_mut().unwrap())); From 47d7113b03b2481ae4cd0aa8db9257fb6c57edae Mon Sep 17 00:00:00 2001 From: lifning <> Date: Wed, 11 Aug 2021 00:42:13 -0700 Subject: [PATCH 31/36] implement the rest of the callbacks in the dispatcher --- examples/ffmpeg_recorder.rs | 22 ++- examples/sdl2_emulator.rs | 4 +- src/components/mod.rs | 283 +++++++++++++++++++----------------- src/lib.rs | 2 +- src/retro/wrapped_types.rs | 2 +- src/retro/wrapper.rs | 136 ++++++++--------- 6 files changed, 238 insertions(+), 211 deletions(-) diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index f9c5e95..2469dd6 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -24,6 +24,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, @@ -220,6 +221,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, @@ -393,11 +395,13 @@ static bool ffmpeg_init_config(struct ff_config_param *params, } } -impl retro::wrapper::RetroCallbacks 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); @@ -420,9 +424,19 @@ impl retro::wrapper::RetroCallbacks 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)); } @@ -434,8 +448,6 @@ impl retro::wrapper::RetroCallbacks 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() } @@ -465,7 +477,7 @@ impl retro::wrapper::RetroCallbacks 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.av_info.timing = system_av_info.timing.clone(); self.set_geometry(&system_av_info.geometry); true } @@ -499,7 +511,7 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { } } - fn set_variables(&mut self, variables: Vec) -> bool { + fn set_variables(&mut self, variables: &Vec) -> bool { for v in variables { eprintln!("{:?}", v); } diff --git a/examples/sdl2_emulator.rs b/examples/sdl2_emulator.rs index 18b3d1b..eda1978 100644 --- a/examples/sdl2_emulator.rs +++ b/examples/sdl2_emulator.rs @@ -204,11 +204,13 @@ impl Drop for MyEmulator { } } -impl retro::wrapper::RetroCallbacks 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); diff --git a/src/components/mod.rs b/src/components/mod.rs index 3e63a0b..97a9754 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,83 +1,69 @@ use crate::prelude::*; +use crate::retro::ffi::*; use std::os::raw::c_uint; -use std::rc::Rc; -use crate::retro::ffi::{Message, PixelFormat, HwRenderCallback, SensorInterface, CameraCallback, LogCallback, PerfCallback, LocationCallback, SystemAvInfo, GetProcAddressInterface, MemoryMap, GameGeometry, Language, LogLevel, RumbleEffect, Time, PerfTick, PerfCounter, SensorAction}; use std::path::PathBuf; -#[rustfmt::skip] -#[allow(unused)] -pub trait RetroAvCommonComponent { - fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { false } -} - -#[rustfmt::skip] -#[allow(unused)] -pub trait RetroVideoComponent: RetroAvCommonComponent { - fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) {} - // -- environment callbacks -- - fn set_rotation(&mut self, rotation: EnvRotation) -> bool { false } - fn get_overscan(&mut self) -> Option { None } - fn set_pixel_format(&mut self, format: PixelFormat) -> bool { false } - fn set_hw_render(&mut self, hw_render_callback: &HwRenderCallback) -> bool { false } - fn set_geometry(&mut self, game_geometry: &GameGeometry) -> bool { false } -} - -#[rustfmt::skip] -#[allow(unused)] -pub trait RetroAudioComponent: RetroAvCommonComponent { - fn audio_sample(&mut self, left: i16, right: i16) {} - fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { 0 } -} - pub struct RetroComponentBase { retro: LibretroWrapper, - pub video_comps: Vec>, - pub audio_comps: Vec>, + pub components: Vec>, } -#[allow(unused)] -impl RetroCallbacks for RetroComponentBase { +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 vc in &mut self.video_comps { - // FIXME: can't have more than 1 ref! - // get_mut fails if one component struct impls both Audio&Video! - Rc::get_mut(vc).unwrap().video_refresh(data, width, height, pitch); + 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) { - todo!() + for comp in &mut self.components { + comp.video_refresh_dupe(width, height, pitch); + } } fn audio_sample(&mut self, left: i16, right: i16) { - todo!() + for comp in &mut self.components { + comp.audio_sample(left, right); + } } fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { - todo!() + self.components.iter_mut() + .map(|comp| comp.audio_sample_batch(stereo_pcm)) + .max() + .unwrap_or_default() } fn input_poll(&mut self) { - todo!() + for comp in &mut self.components { + comp.input_poll(); + } } fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { - todo!() + 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) -> bool { - self.video_comps.iter_mut() - .map(|x| Rc::get_mut(x).unwrap().set_rotation(rotation)) + self.components.iter_mut() + .map(|comp| comp.set_rotation(rotation)) .fold(false, |x, y| x || y) // not "any" because we don't short-circuit } fn get_overscan(&mut self) -> Option { - self.video_comps.iter_mut() - .map(|x| Rc::get_mut(x).unwrap().get_overscan()) + 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), @@ -85,177 +71,212 @@ impl RetroCallbacks for RetroComponentBase { }) } - fn set_message(&mut self, message: Message) -> bool { - todo!() + fn set_message(&mut self, message: &Message) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_message(message)) + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit } fn shutdown(&mut self) -> bool { - todo!() - } - - fn set_performance_level(&mut self, level: c_uint) -> bool { - todo!() - } - - fn get_system_directory(&mut self) -> Option { - todo!() - } - - fn set_pixel_format(&mut self, format: PixelFormat) -> bool { - self.video_comps.iter_mut() - .map(|x| Rc::get_mut(x).unwrap().set_pixel_format(format)) + self.components.iter_mut() + .map(|comp| comp.shutdown()) .all(|x| x) } - fn set_input_descriptors(&mut self, input_descriptors: Vec) -> bool { - todo!() + fn set_performance_level(&mut self, level: c_uint) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_performance_level(level)) + .all(|x| x) + } + + 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) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_pixel_format(format)) + .all(|x| x) + } + + fn set_input_descriptors(&mut self, input_descriptors: &Vec) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_input_descriptors(input_descriptors)) + .fold(false, |x, y| x || y) } fn set_hw_render(&mut self, hw_render_callback: &HwRenderCallback) -> bool { - self.video_comps.iter_mut() - .map(|x| Rc::get_mut(x).unwrap().set_hw_render(hw_render_callback)) + self.components.iter_mut() + .map(|comp| comp.set_hw_render(hw_render_callback)) .all(|x| x) } fn get_variable(&mut self, key: &str) -> Option { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_variable(key)) + .flatten() + .next() } - fn set_variables(&mut self, variables: Vec) -> bool { - todo!() + fn set_variables(&mut self, variables: &Vec) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_variables(variables)) + .fold(false, |x, y| x || y) } fn get_variable_update(&mut self) -> Option { - todo!() + 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) -> bool { - todo!() + self.components.iter_mut() + .map(|comp| comp.set_support_no_game(supports_no_game)) + .all(|x| x) } fn get_libretro_path(&mut self) -> Option { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_libretro_path()) + .flatten() + .next() } fn get_input_device_capabilities(&mut self) -> Option { - todo!() - } - - fn get_sensor_interface(&mut self) -> Option { - todo!() - } - - fn get_camera_interface(&mut self) -> Option { - todo!() - } - - fn get_log_interface(&mut self) -> Option { - todo!() - } - - fn get_perf_interface(&mut self) -> Option { - todo!() - } - - fn get_location_interface(&mut self) -> Option { - todo!() + 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 { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_core_assets_directory()) + .flatten() + .next() } fn get_save_directory(&mut self) -> Option { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_save_directory()) + .flatten() + .next() } fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { - // TODO: avoid calling twice for components that are registered in both simultaneously - - let mut result = false; - - for vc in &mut self.video_comps { - result |= Rc::get_mut(vc).unwrap().set_system_av_info(system_av_info); - } - - for ac in &mut self.audio_comps { - result |= Rc::get_mut(ac).unwrap().set_system_av_info(system_av_info); - } - - result + self.components.iter_mut() + .map(|comp| comp.set_system_av_info(system_av_info)) + .fold(false, |x, y| x || y) // not "any" because we don't short-circuit } - fn set_proc_address_callback(&mut self, cb: GetProcAddressInterface) -> bool { - todo!() + fn set_subsystem_info(&mut self, subsystem_info: &Vec) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_subsystem_info(subsystem_info)) + .all(|x| x) } - fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { - todo!() + fn set_controller_info(&mut self, controller_info: &Vec) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_controller_info(controller_info)) + .all(|x| x) } - fn set_controller_info(&mut self, controller_info: Vec) -> bool { - todo!() - } - - fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool { - todo!() + fn set_memory_maps(&mut self, memory_map: &MemoryMap) -> bool { + self.components.iter_mut() + .map(|comp| comp.set_memory_maps(memory_map)) + .all(|x| x) } fn set_geometry(&mut self, game_geometry: &GameGeometry) -> bool { - self.video_comps.iter_mut() - .map(|x| Rc::get_mut(x).unwrap().set_geometry(game_geometry)) + self.components.iter_mut() + .map(|comp| comp.set_geometry(game_geometry)) .fold(false, |x, y| x || y) // not "any" because we don't short-circuit } fn get_username(&mut self) -> Option { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_username()) + .flatten() + .next() } fn get_language(&mut self) -> Option { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_language()) + .flatten() + .next() } fn log_print(&mut self, level: LogLevel, msg: &str) { - todo!() + 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 { - todo!() + 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 { - todo!() + self.components.first_mut() + .map(|comp| comp.perf_get_time_usec_cb()) + .unwrap_or_default() } fn perf_get_counter_cb(&mut self) -> PerfTick { - todo!() + self.components.first_mut() + .map(|comp| comp.perf_get_counter_cb()) + .unwrap_or_default() } fn perf_get_cpu_features_cb(&mut self) -> u64 { - todo!() + self.components.first_mut() + .map(|comp| comp.perf_get_cpu_features_cb()) + .unwrap_or_default() } fn perf_log_cb(&mut self) { - todo!() + if let Some(comp) = self.components.first_mut() { + comp.perf_log_cb() + } } fn perf_register_cb(&mut self, counter: &mut PerfCounter) { - todo!() + if let Some(comp) = self.components.first_mut() { + comp.perf_register_cb(counter) + } } fn perf_start_cb(&mut self, counter: &mut PerfCounter) { - todo!() + if let Some(comp) = self.components.first_mut() { + comp.perf_start_cb(counter) + } } fn perf_stop_cb(&mut self, counter: &mut PerfCounter) { - todo!() + 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 { - todo!() + 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 { - todo!() + self.components.iter_mut() + .map(|comp| comp.get_sensor_input(port, id)) + .filter(|x| *x != 0.0) + .last() + .unwrap_or_default() } } diff --git a/src/lib.rs b/src/lib.rs index b3cc73e..e9ae16f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,5 +7,5 @@ pub mod components; pub mod prelude { pub use crate::retro::constants::*; pub use crate::retro::wrapped_types::*; - pub use crate::retro::wrapper::{RetroCallbacks, LibretroWrapper}; + pub use crate::retro::wrapper::{RetroCallbacks, LibretroWrapper, LibretroWrapperAccess}; } diff --git a/src/retro/wrapped_types.rs b/src/retro/wrapped_types.rs index c51bd76..dd9f1e6 100644 --- a/src/retro/wrapped_types.rs +++ b/src/retro/wrapped_types.rs @@ -6,7 +6,7 @@ use std::slice::from_raw_parts; use super::constants::*; use super::ffi::*; -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum InputDeviceId { None(c_uint), Joypad(JoypadButton), diff --git a/src/retro/wrapper.rs b/src/retro/wrapper.rs index 2a3bdf7..d17d916 100644 --- a/src/retro/wrapper.rs +++ b/src/retro/wrapper.rs @@ -6,6 +6,7 @@ use std::convert::TryFrom; use std::ffi::{CStr, CString}; use std::ops::Deref; use std::os::raw::{c_char, c_uint}; +use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::time::Duration; @@ -28,19 +29,41 @@ extern "C" { fn c_ext_set_log_print_cb(cb: WrappedLogPrintFn); } +pub trait LibretroWrapperAccess { + fn libretro_core(&mut self) -> &mut LibretroWrapper; +} + +// method docs largely copied/lightly-adapted-to-rust straight from libretro.h. #[rustfmt::skip] #[allow(unused)] -pub trait RetroCallbacks: Unpin + 'static { - fn libretro_core(&mut self) -> &mut LibretroWrapper; - fn delegate_handler(&self) -> Option<&mut dyn RetroCallbacks> { None } - +pub trait RetroCallbacks: LibretroWrapperAccess + Unpin + 'static { // -- main callbacks -- + /// Render a frame. Pixel format is 15-bit 0RGB1555 native endian + /// unless changed (see [Self::set_pixel_format]). + /// + /// Width and height specify dimensions of buffer. + /// Pitch specifices length in bytes between two lines in buffer. + /// + /// For performance reasons, it is highly recommended to have a frame + /// that is packed in memory, i.e. pitch == width * byte_per_pixel. + /// Certain graphic APIs, such as OpenGL ES, do not like textures + /// that are not packed in memory. fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) {} /// Called instead of video_refresh when the core reports a duplicate frame (NULL). fn video_refresh_dupe(&mut self, width: c_uint, height: c_uint, pitch: c_uint) {} + /// Renders a single audio frame. Should only be used if implementation + /// generates a single sample at a time. + /// Format is signed 16-bit native endian. fn audio_sample(&mut self, left: i16, right: i16) {} + /// Renders multiple audio frames in one go. + /// + /// One frame is defined as a sample of left and right channels, interleaved. + /// I.e. int16_t buf\[4\] = { l, r, l, r }; would be 2 frames. + /// Only one of the audio callbacks must ever be used. fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { stereo_pcm.len() } + /// Polls input. fn input_poll(&mut self) {} + /// Queries for input for player 'port'. fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { 0 } // -- environment callbacks -- @@ -57,7 +80,7 @@ pub trait RetroCallbacks: Unpin + 'static { /// Should not be used for trivial messages, which should simply be /// logged via [Self::get_log_interface] (or as a /// fallback, stderr). - fn set_message(&mut self, message: Message) -> bool { false } + fn set_message(&mut self, message: &Message) -> bool { false } /// Requests the frontend to shutdown. /// Should only be used if game has a specific /// way to shutdown the game from a menu item or similar. @@ -102,7 +125,7 @@ pub trait RetroCallbacks: Unpin + 'static { /// It is up to the frontend to present this in a usable way. /// This function can be called at any time, but it is recommended /// for the core to call it as early as possible. - fn set_input_descriptors(&mut self, input_descriptors: Vec) -> bool { false } + fn set_input_descriptors(&mut self, input_descriptors: &Vec) -> bool { false } /// Sets an interface to let a libretro core render with /// hardware acceleration. /// The core should call this in [libretro_sys::CoreAPI::retro_load_game]. @@ -143,7 +166,7 @@ pub trait RetroCallbacks: Unpin + 'static { /// /// Only strings are operated on. The possible values will /// generally be displayed and stored as-is by the frontend. - fn set_variables(&mut self, variables: Vec) -> bool { false } + fn set_variables(&mut self, variables: &Vec) -> bool { false } /// Result is set to true if some variables are updated by /// frontend since last call to [Self::get_variable]. /// Variables should be queried with [Self::get_variable]. @@ -168,53 +191,6 @@ pub trait RetroCallbacks: Unpin + 'static { /// Example bitmask: caps = (1 << [libretro_sys::DEVICE_JOYPAD]) | (1 << [libretro_sys::DEVICE_ANALOG]). /// Should only be called in [libretro_sys::CoreAPI::retro_run]. fn get_input_device_capabilities(&mut self) -> Option { None } - /// Gets access to the sensor interface. - /// The purpose of this interface is to allow - /// setting state related to sensors such as polling rate, - /// enabling/disable it entirely, etc. - /// Reading sensor state is done via the normal - /// [Self::input_state] API. - fn get_sensor_interface(&mut self) -> Option { None } - /// Gets an interface to a video camera driver. - /// A libretro core can use this interface to get access to a - /// video camera. - /// New video frames are delivered in a callback in same - /// thread as [libretro_sys::CoreAPI::retro_run]. - /// - /// GET_CAMERA_INTERFACE should be called in [libretro_sys::CoreAPI::retro_load_game](). - /// - /// Depending on the camera implementation used, camera frames - /// will be delivered as a raw framebuffer, - /// or as an OpenGL texture directly. - /// - /// The core has to tell the frontend here which types of - /// buffers can be handled properly. - /// An OpenGL texture can only be handled when using a - /// libretro GL core ([Self::set_hw_render]). - /// It is recommended to use a libretro GL core when - /// using camera interface. - /// - /// The camera is not started automatically. The retrieved start/stop - /// functions must be used to explicitly - /// start and stop the camera driver. - fn get_camera_interface(&mut self) -> Option { None } - /// Gets an interface for logging. This is useful for - /// logging in a cross-platform way - /// as certain platforms cannot use stderr for logging. - /// It also allows the frontend to - /// show logging information in a more suitable way. - /// If this interface is not used, libretro cores should - /// log to stderr as desired. - fn get_log_interface(&mut self) -> Option { None } - /// Gets an interface for performance counters. This is useful - /// for performance logging in a cross-platform way and for detecting - /// architecture-specific features, such as SIMD support. - fn get_perf_interface(&mut self) -> Option { None } - /// Gets access to the location interface. - /// The purpose of this interface is to be able to retrieve - /// location-based information from the host device, - /// such as current latitude / longitude. - fn get_location_interface(&mut self) -> Option { None } /// Returns the "core assets" directory of the frontend. /// This directory can be used to store specific assets that the /// core relies upon, such as art assets, @@ -267,15 +243,6 @@ pub trait RetroCallbacks: Unpin + 'static { /// If this returns false, the frontend does not acknowledge a /// changed av_info struct. fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { false } - /// Allows a libretro core to announce support for the - /// get_proc_address() interface. - /// This interface allows for a standard way to extend libretro where - /// use of environment calls are too indirect, - /// e.g. for cases where the frontend wants to call directly into the core. - /// - /// If a core wants to expose this interface, [Self::set_proc_address_callback] - /// **MUST** be called from within [libretro_sys::CoreAPI::retro_set_environment]. - fn set_proc_address_callback(&mut self, cb: GetProcAddressInterface) -> bool { false } /// This environment call introduces the concept of libretro "subsystems". /// A subsystem is a variant of a libretro core which supports /// different kinds of games. @@ -290,7 +257,7 @@ pub trait RetroCallbacks: Unpin + 'static { /// /// If a core wants to expose this interface, [Self::set_subsystem_info] /// **MUST** be called from within [libretro_sys::CoreAPI::retro_set_environment]. - fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { false } + fn set_subsystem_info(&mut self, subsystem_info: &Vec) -> bool { false } /// This environment call lets a libretro core tell the frontend /// which controller subclasses are recognized in calls to /// [libretro_sys::CoreAPI::retro_set_controller_port_device]. @@ -324,7 +291,7 @@ pub trait RetroCallbacks: Unpin + 'static { /// /// NOTE: Even if special device types are set in the libretro core, /// libretro should only poll input based on the base input device types. - fn set_controller_info(&mut self, controller_info: Vec) -> bool { false } + fn set_controller_info(&mut self, controller_info: &Vec) -> bool { false } /// This environment call lets a libretro core tell the frontend /// about the memory maps this core emulates. /// This can be used to implement, for example, cheats in a core-agnostic way. @@ -336,7 +303,7 @@ pub trait RetroCallbacks: Unpin + 'static { /// [libretro_sys::CoreAPI::retro_get_memory_size] as well. /// /// Can be called from [libretro_sys::CoreAPI::retro_init] and [libretro_sys::CoreAPI::retro_load_game]. - fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool { false } + fn set_memory_maps(&mut self, memory_map: &MemoryMap) -> bool { false } /// This environment call is similar to [Self::set_system_av_info] for changing /// video parameters, but provides a guarantee that drivers will not be /// reinitialized. @@ -367,14 +334,37 @@ pub trait RetroCallbacks: Unpin + 'static { // fn set_serialization_quirks(&mut self, quirks: &mut u64) -> bool { false } // -- environment-set callbacks (API extensions) -- + /// Logging function. fn log_print(&mut self, level: LogLevel, msg: &str) {} + /// Sets rumble state for joypad plugged in port 'port'. + /// Rumble effects are controlled independently, + /// and setting e.g. strong rumble does not override weak rumble. + /// Strength has a range of \[0, 0xffff\]. + /// + /// Returns true if rumble state request was honored. + /// Calling this before first [libretro_sys::CoreAPI::retro_run] is likely to return false. fn set_rumble_state(&mut self, port: c_uint, effect: RumbleEffect, strength: u16) -> bool { false } + /// Returns current time in microseconds. + /// Tries to use the most accurate timer available. fn perf_get_time_usec_cb(&mut self) -> Time { 0 } + /// A simple counter. Usually nanoseconds, but can also be CPU cycles. + /// Can be used directly if desired (when creating a more sophisticated + /// performance counter system). fn perf_get_counter_cb(&mut self) -> PerfTick { 0 } + /// Returns a bit-mask of detected CPU features ([libretro_sys]::SIMD_*). fn perf_get_cpu_features_cb(&mut self) -> u64 { 0 } + /// Asks frontend to log and/or display the state of performance counters. + /// Performance counters can always be poked into manually as well. fn perf_log_cb(&mut self) {} + /// Register a performance counter. + /// ident field must be set with a discrete value and other values in + /// retro_perf_counter must be 0. + /// Registering can be called multiple times. To avoid calling to + /// frontend redundantly, you can check registered field first. fn perf_register_cb(&mut self, counter: &mut PerfCounter) {} + /// Starts a registered counter. fn perf_start_cb(&mut self, counter: &mut PerfCounter) {} + /// Stops a registered counter. fn perf_stop_cb(&mut self, counter: &mut PerfCounter) {} fn set_sensor_state(&mut self, port: c_uint, action: SensorAction, rate: c_uint) -> bool { false } fn get_sensor_input(&mut self, port: c_uint, id: c_uint) -> f32 { 0.0 } @@ -399,7 +389,9 @@ impl StaticCallbacks { Some(true) } fn path_into_void(data: *mut c_void, source: impl AsRef) -> Option { - Self::string_into_void(data, source.as_ref().to_string_lossy()) + *unsafe { (data as *mut *const c_char).as_mut()? } = + CString::new(source.as_ref().as_os_str().as_bytes()).ok()?.into_raw(); + Some(true) } fn from_void(data: *mut c_void) -> Option<&'static mut T> { unsafe { (data as *mut T).as_mut() } @@ -440,7 +432,7 @@ impl StaticCallbacks { EnvCmd::SetRotation => handler.set_rotation(Self::enum_from_void(data)?), EnvCmd::GetOverscan => Self::clone_into_void(data, &handler.get_overscan()?)?, EnvCmd::GetCanDupe => Self::clone_into_void(data, &true)?, - EnvCmd::SetMessage => handler.set_message(Self::from_void::(data)?.clone()), + EnvCmd::SetMessage => handler.set_message(Self::from_void::(data)?), EnvCmd::Shutdown => handler.shutdown(), EnvCmd::SetPerformanceLevel => handler.set_performance_level(*Self::from_void(data)?), EnvCmd::GetSystemDirectory => { @@ -456,7 +448,7 @@ impl StaticCallbacks { descriptors.push(unsafe { input_desc.as_ref() }?.try_into().ok()?); input_desc = input_desc.wrapping_add(1); } - handler.set_input_descriptors(descriptors) + handler.set_input_descriptors(&descriptors) } EnvCmd::SetKeyboardCallback => { let kc: &mut KeyboardCallback = Self::from_void(data)?; @@ -508,7 +500,7 @@ impl StaticCallbacks { descriptors.push(unsafe { var.as_ref() }?.into()); var = var.wrapping_add(1); } - handler.set_variables(descriptors) + handler.set_variables(&descriptors) } EnvCmd::GetVariableUpdate => { Self::clone_into_void(data, &handler.get_variable_update()?)? @@ -585,7 +577,7 @@ impl StaticCallbacks { descriptors.push(unsafe { info.as_ref() }?.into()); info = info.wrapping_add(1); } - handler.set_subsystem_info(descriptors) + handler.set_subsystem_info(&descriptors) } EnvCmd::SetControllerInfo => { let info = unsafe { (data as *const ControllerInfo).as_ref() }?; @@ -596,7 +588,7 @@ impl StaticCallbacks { .map(TryInto::try_into) .filter_map(|x| x.ok()) .collect(); - handler.set_controller_info(controller_info) + handler.set_controller_info(&controller_info) } // TODO (experimental) EnvCmd::SetMemoryMaps => {}, EnvCmd::SetGeometry => { From 7b06ed61d1ad867378531f1273cb16c6a6b66c0d Mon Sep 17 00:00:00 2001 From: lifning <> Date: Wed, 11 Aug 2021 01:08:22 -0700 Subject: [PATCH 32/36] constructor for RetroComponentBase --- examples/sdl2_emulator.rs | 8 +++--- src/components/mod.rs | 21 ++++++++++++++- src/retro/loading.rs | 54 +++++++++++++++++++-------------------- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/examples/sdl2_emulator.rs b/examples/sdl2_emulator.rs index eda1978..0ae554a 100644 --- a/examples/sdl2_emulator.rs +++ b/examples/sdl2_emulator.rs @@ -285,7 +285,7 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { } } - fn set_variables(&mut self, variables: Vec) -> bool { + fn set_variables(&mut self, variables: &Vec) -> bool { for v in variables { eprintln!("{:?}", v); } @@ -311,12 +311,12 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { true } - fn set_subsystem_info(&mut self, subsystem_info: Vec) -> bool { + fn set_subsystem_info(&mut self, subsystem_info: &Vec) -> bool { println!("subsystem info: {:?}", subsystem_info); true } - fn set_controller_info(&mut self, controller_info: Vec) -> bool { + fn set_controller_info(&mut self, controller_info: &Vec) -> bool { for ci in controller_info { // so we can have analog support in beetle/mednafen saturn if ci.name.as_str() == "3D Control Pad" { @@ -327,7 +327,7 @@ impl retro::wrapper::RetroCallbacks for MyEmulator { true } - fn set_input_descriptors(&mut self, descriptors: Vec) -> bool { + fn set_input_descriptors(&mut self, descriptors: &Vec) -> bool { for id in descriptors { println!("{:?}", id); } diff --git a/src/components/mod.rs b/src/components/mod.rs index 97a9754..2cb0116 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,13 +1,32 @@ use crate::prelude::*; use crate::retro::ffi::*; use std::os::raw::c_uint; -use std::path::PathBuf; +use std::path::{PathBuf, Path}; +use std::pin::Pin; pub struct RetroComponentBase { retro: LibretroWrapper, pub components: Vec>, } +impl RetroComponentBase { + 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, + components: Vec::new(), + }; + + let mut pin_emu = Box::pin(emu); + crate::retro::wrapper::set_handler(pin_emu.as_mut()); + pin_emu.retro.init(); + pin_emu + } +} + impl LibretroWrapperAccess for RetroComponentBase { fn libretro_core(&mut self) -> &mut LibretroWrapper { &mut self.retro diff --git a/src/retro/loading.rs b/src/retro/loading.rs index a0da355..9f889f8 100644 --- a/src/retro/loading.rs +++ b/src/retro/loading.rs @@ -49,7 +49,7 @@ impl LibretroApi { pub fn set_environment(&self, cb: EnvironmentFn) { unsafe { (&self.core_api.retro_set_environment)(cb) } } - /// video_refresh() must be called before run(). + /// set_video_refresh() must be called before run(). pub fn set_video_refresh(&self, cb: VideoRefreshFn) { unsafe { (&self.core_api.retro_set_video_refresh)(cb) } } @@ -85,11 +85,11 @@ impl LibretroApi { info } } - /** Gets information about system audio/video timings and geometry. - * Can be called only after load_game() has successfully completed. - * NOTE: The implementation of this function might not initialize every variable if needed. - * E.g. geom.aspect_ratio might not be initialized if core doesn't - * desire a particular aspect ratio. */ + /// Gets information about system audio/video timings and geometry. + /// Can be called only after load_game() has successfully completed. + /// NOTE: The implementation of this function might not initialize every variable if needed. + /// E.g. geom.aspect_ratio might not be initialized if core doesn't + /// desire a particular aspect ratio. pub fn get_system_av_info(&self) -> SystemAvInfo { unsafe { let mut av_info = ::std::mem::zeroed::(); @@ -97,20 +97,19 @@ impl LibretroApi { av_info } } - /** Sets device to be used for player 'port'. - * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all - * available ports. - * Setting a particular device type is not a guarantee that libretro cores - * will only poll input based on that particular device type. It is only a - * hint to the libretro core when a core cannot automatically detect the - * appropriate input device type on its own. It is also relevant when a - * core can change its behavior depending on device type. - * - * As part of the core's implementation of retro_set_controller_port_device, - * the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the - * frontend if the descriptions for any controls have changed as a - * result of changing the device type. - */ + /// Sets device to be used for player 'port'. + /// By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all + /// available ports. + /// Setting a particular device type is not a guarantee that libretro cores + /// will only poll input based on that particular device type. It is only a + /// hint to the libretro core when a core cannot automatically detect the + /// appropriate input device type on its own. It is also relevant when a + /// core can change its behavior depending on device type. + /// + /// As part of the core's implementation of retro_set_controller_port_device, + /// the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the + /// frontend if the descriptions for any controls have changed as a + /// result of changing the device type. pub fn set_controller_port_device(&self, port: u32, device: u32) { unsafe { (&self.core_api.retro_set_controller_port_device)(port, device) } } @@ -118,14 +117,13 @@ impl LibretroApi { pub fn reset(&self) { unsafe { (&self.core_api.retro_reset)() } } - /** Runs the game for one video frame. - * During retro_run(), input_poll callback must be called at least once. - * - * If a frame is not rendered for reasons where a game "dropped" a frame, - * this still counts as a frame, and retro_run() should explicitly dupe - * a frame if GET_CAN_DUPE returns true. - * In this case, the video callback can take a NULL argument for data. - */ + /// Runs the game for one video frame. + /// During retro_run(), input_poll callback must be called at least once. + /// + /// If a frame is not rendered for reasons where a game "dropped" a frame, + /// this still counts as a frame, and retro_run() should explicitly dupe + /// a frame if GET_CAN_DUPE returns true. + /// In this case, the video callback can take a NULL argument for data. pub fn run(&self) { unsafe { (&self.core_api.retro_run)() } } From 9dcbc948478b8b9653732adb86af10e7b0b22987 Mon Sep 17 00:00:00 2001 From: lifning <> Date: Thu, 12 Aug 2021 23:58:21 -0700 Subject: [PATCH 33/36] WIP: messing with mutability for a better tomorrow --- examples/ffmpeg_recorder.rs | 4 +- examples/sdl2_emulator.rs | 2 +- src/components/ffmpeg.rs | 519 ++++++++++++++++++++++++++++++++++++ src/components/mod.rs | 2 + src/lib.rs | 1 + src/retro/loading.rs | 34 +-- src/retro/wrapper.rs | 27 +- 7 files changed, 560 insertions(+), 29 deletions(-) create mode 100644 src/components/ffmpeg.rs diff --git a/examples/ffmpeg_recorder.rs b/examples/ffmpeg_recorder.rs index 2469dd6..31cd8ff 100644 --- a/examples/ffmpeg_recorder.rs +++ b/examples/ffmpeg_recorder.rs @@ -205,7 +205,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(); @@ -359,7 +359,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(); diff --git a/examples/sdl2_emulator.rs b/examples/sdl2_emulator.rs index 0ae554a..6493e9f 100644 --- a/examples/sdl2_emulator.rs +++ b/examples/sdl2_emulator.rs @@ -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(); diff --git a/src/components/ffmpeg.rs b/src/components/ffmpeg.rs new file mode 100644 index 0000000..13893c7 --- /dev/null +++ b/src/components/ffmpeg.rs @@ -0,0 +1,519 @@ +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 crate::prelude::*; + +use ffmpeg::{ChannelLayout, Packet, codec, filter, format, frame, media}; +use ffmpeg::util::rational::Rational; + +struct MyEmulator { + retro: LibretroWrapper, + sys_info: SystemInfo, + 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, + sys_path: Option, + frame_properties_locked: bool, + octx: ffmpeg::format::context::Output, + frame: u64, +} + +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 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); + + 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; + 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 mut 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 emu = MyEmulator { + retro, + sys_info, + 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, + 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 + } + + 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 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(); + self.receive_and_write_packets(EncoderToWriteFrom::Video); + self.audio_encoder.send_eof(); + 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 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); + + 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 get_system_directory(&mut self) -> Option { + self.sys_path.clone() + } + + fn set_pixel_format(&mut self, format: PixelFormat) -> bool { + if self.frame_properties_locked { + return true; + } + + 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(); + true + } + + fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> bool { + if self.frame_properties_locked { + return true; + } + + //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); + true + } + + fn set_geometry(&mut self, geometry: &GameGeometry) -> bool { + if self.frame_properties_locked { + return 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.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(); + 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); + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 2cb0116..7a017b4 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,5 @@ +//pub mod ffmpeg; + use crate::prelude::*; use crate::retro::ffi::*; use std::os::raw::c_uint; diff --git a/src/lib.rs b/src/lib.rs index e9ae16f..d789f27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,4 +8,5 @@ pub mod prelude { pub use crate::retro::constants::*; pub use crate::retro::wrapped_types::*; pub use crate::retro::wrapper::{RetroCallbacks, LibretroWrapper, LibretroWrapperAccess}; + pub use crate::retro::ffi::{PixelFormat, GameGeometry, SystemAvInfo, SystemInfo}; } diff --git a/src/retro/loading.rs b/src/retro/loading.rs index 9f889f8..cac7572 100644 --- a/src/retro/loading.rs +++ b/src/retro/loading.rs @@ -46,31 +46,31 @@ impl LibretroApi { } } /// set_environment() must be called before init(). - pub fn set_environment(&self, cb: EnvironmentFn) { + pub fn set_environment(&mut self, cb: EnvironmentFn) { unsafe { (&self.core_api.retro_set_environment)(cb) } } /// set_video_refresh() must be called before run(). - pub fn set_video_refresh(&self, cb: VideoRefreshFn) { + pub fn set_video_refresh(&mut self, cb: VideoRefreshFn) { unsafe { (&self.core_api.retro_set_video_refresh)(cb) } } - pub fn set_audio_sample(&self, cb: AudioSampleFn) { + pub fn set_audio_sample(&mut self, cb: AudioSampleFn) { unsafe { (&self.core_api.retro_set_audio_sample)(cb) } } - pub fn set_audio_sample_batch(&self, cb: AudioSampleBatchFn) { + pub fn set_audio_sample_batch(&mut self, cb: AudioSampleBatchFn) { unsafe { (&self.core_api.retro_set_audio_sample_batch)(cb) } } - pub fn set_input_poll(&self, cb: InputPollFn) { + pub fn set_input_poll(&mut self, cb: InputPollFn) { unsafe { (&self.core_api.retro_set_input_poll)(cb) } } - pub fn set_input_state(&self, cb: InputStateFn) { + pub fn set_input_state(&mut self, cb: InputStateFn) { unsafe { (&self.core_api.retro_set_input_state)(cb) } } /// set_environment() must be called before init(). - pub fn init(&self) { + pub fn init(&mut self) { // TODO assert!(called retro_set_environment); unsafe { (&self.core_api.retro_init)() } } - pub fn deinit(&self) { + pub fn deinit(&mut self) { unsafe { (&self.core_api.retro_deinit)() } } /// Must return RETRO_API_VERSION. Used to validate ABI compatibility when the API is revised. @@ -110,11 +110,11 @@ impl LibretroApi { /// the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the /// frontend if the descriptions for any controls have changed as a /// result of changing the device type. - pub fn set_controller_port_device(&self, port: u32, device: u32) { + pub fn set_controller_port_device(&mut self, port: u32, device: u32) { unsafe { (&self.core_api.retro_set_controller_port_device)(port, device) } } /// Resets the current game. - pub fn reset(&self) { + pub fn reset(&mut self) { unsafe { (&self.core_api.retro_reset)() } } /// Runs the game for one video frame. @@ -124,11 +124,11 @@ impl LibretroApi { /// this still counts as a frame, and retro_run() should explicitly dupe /// a frame if GET_CAN_DUPE returns true. /// In this case, the video callback can take a NULL argument for data. - pub fn run(&self) { + pub fn run(&mut self) { unsafe { (&self.core_api.retro_run)() } } /// Serializes internal state. - pub fn serialize(&self) -> Fallible> { + pub fn serialize(&mut self) -> Fallible> { let size: usize = unsafe { (&self.core_api.retro_serialize_size)() }; let mut vec = Vec::with_capacity(size); vec.resize(size, 0); @@ -139,7 +139,7 @@ impl LibretroApi { Err(failure::err_msg("Serialize failed")) } } - pub fn unserialize(&self, data: &[u8]) -> Fallible<()> { + pub fn unserialize(&mut self, data: &[u8]) -> Fallible<()> { // validate size of the data let size: usize = unsafe { (&self.core_api.retro_serialize_size)() }; if data.len() != size { @@ -152,15 +152,15 @@ impl LibretroApi { Err(failure::err_msg("Unserialize failed")) } } - pub fn cheat_reset(&self) { + pub fn cheat_reset(&mut self) { unsafe { (&self.core_api.retro_cheat_reset)() } } - pub fn cheat_set(&self, index: u32, enabled: bool, code: &str) { + pub fn cheat_set(&mut self, index: u32, enabled: bool, code: &str) { unsafe { (&self.core_api.retro_cheat_set)(index, enabled, code.as_bytes().as_ptr() as *const c_char) } } /// Loads a game. pub fn load_game( - &self, + &mut self, path: Option<&Path>, data: Option<&[u8]>, meta: Option<&str>, @@ -194,7 +194,7 @@ impl LibretroApi { } } /// Unloads the currently loaded game. Called before deinit(). - pub fn unload_game(&self) { + pub fn unload_game(&mut self) { unsafe { (&self.core_api.retro_unload_game)() } } pub fn get_region(&self) -> Region { diff --git a/src/retro/wrapper.rs b/src/retro/wrapper.rs index d17d916..5c68f19 100644 --- a/src/retro/wrapper.rs +++ b/src/retro/wrapper.rs @@ -4,7 +4,7 @@ use core::slice::from_raw_parts; use std::convert::TryFrom; use std::ffi::{CStr, CString}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::os::raw::{c_char, c_uint}; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; @@ -29,14 +29,10 @@ extern "C" { fn c_ext_set_log_print_cb(cb: WrappedLogPrintFn); } -pub trait LibretroWrapperAccess { - fn libretro_core(&mut self) -> &mut LibretroWrapper; -} - // method docs largely copied/lightly-adapted-to-rust straight from libretro.h. #[rustfmt::skip] #[allow(unused)] -pub trait RetroCallbacks: LibretroWrapperAccess + Unpin + 'static { +pub trait RetroCallbacks: Unpin + 'static { // -- main callbacks -- /// Render a frame. Pixel format is 15-bit 0RGB1555 native endian /// unless changed (see [Self::set_pixel_format]). @@ -370,9 +366,16 @@ pub trait RetroCallbacks: LibretroWrapperAccess + Unpin + 'static { fn get_sensor_input(&mut self, port: c_uint, id: c_uint) -> f32 { 0.0 } } +pub trait LibretroWrapperAccess { + fn libretro_core(&mut self) -> &mut LibretroWrapper; +} + +pub trait RootRetroCallbacks : RetroCallbacks + LibretroWrapperAccess {} +impl RootRetroCallbacks for T {} + #[derive(Default)] struct StaticCallbacks { - handler: Option>, + handler: Option>, } unsafe impl Sync for StaticCallbacks {} @@ -854,11 +857,17 @@ impl Deref for LibretroWrapper { } } +impl DerefMut for LibretroWrapper { + fn deref_mut(&mut self) -> &mut LibretroApi { + &mut self.api + } +} + // a note on lifetimes: we explicitly lie about them here because as long as they live as long as // the library wrapper itself we're good (we wipe our 'static references on drop() too) -pub fn set_handler(handler: Pin<&'_ mut (dyn RetroCallbacks + '_)>) { +pub fn set_handler(handler: Pin<&'_ mut (dyn RootRetroCallbacks + '_)>) { unsafe { - let ptr = handler.get_unchecked_mut() as *mut dyn RetroCallbacks; + let ptr = handler.get_unchecked_mut() as *mut dyn RootRetroCallbacks; CB_SINGLETON .handler .replace(Pin::new_unchecked(ptr.as_mut().unwrap())); From a7c4f0909da7cf7b238bb3314e3f1f5ec3c4c028 Mon Sep 17 00:00:00 2001 From: lifning <> Date: Tue, 17 Aug 2021 04:06:40 -0700 Subject: [PATCH 34/36] sorta works! ffmpeg component might need updating to latest example code --- Cargo.toml | 20 +- examples/ffmpeg_recorder.rs | 33 ++- examples/multifunction_emulator.rs | 66 +++++ examples/sdl2_emulator.rs | 36 ++- src/components/mod.rs | 231 +++++++++++++++-- src/components/{ => provided}/ffmpeg.rs | 316 ++++++++++------------- src/components/provided/mod.rs | 7 + src/components/provided/sdl2.rs | 323 ++++++++++++++++++++++++ src/components/provided/stdlib.rs | 80 ++++++ src/lib.rs | 2 +- src/retro/loading.rs | 19 +- src/retro/wrapped_types.rs | 16 +- src/retro/wrapper.rs | 56 ++-- 13 files changed, 919 insertions(+), 286 deletions(-) create mode 100644 examples/multifunction_emulator.rs rename src/components/{ => provided}/ffmpeg.rs (84%) create mode 100644 src/components/provided/mod.rs create mode 100644 src/components/provided/sdl2.rs create mode 100644 src/components/provided/stdlib.rs 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