Merge pull request 'Split Sdl2Component into audio/video/input/framelimit' (#15) from liffy/sdl-split into matriarch
Reviewed-on: https://git.vvn.space/cinnabon/rustro/pulls/15
This commit is contained in:
commit
e1c0670feb
|
@ -10,11 +10,10 @@ use ferretro_components::prelude::*;
|
||||||
|
|
||||||
use ferretro_components::provided::{
|
use ferretro_components::provided::{
|
||||||
ffmpeg::FfmpegComponent,
|
ffmpeg::FfmpegComponent,
|
||||||
sdl2::Sdl2Component,
|
sdl2::*,
|
||||||
stdlib::{PathBufComponent, StderrLogComponent},
|
stdlib::{PathBufComponent, StderrLogComponent, SleepFramerateLimitComponent},
|
||||||
};
|
};
|
||||||
use ferretro_components::base::ControlFlow;
|
use ferretro_components::base::ControlFlow;
|
||||||
use ferretro_components::provided::stdlib::StderrSysInfoLogComponent;
|
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
|
@ -37,30 +36,46 @@ struct Opt {
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
let opt: Opt = Opt::from_args();
|
let opt: Opt = Opt::from_args();
|
||||||
|
|
||||||
let mut emu = RetroComponentBase::new(&opt.core);
|
let mut emu = RetroComponentBase::new(&opt.core);
|
||||||
let sdl2_comp = Sdl2Component::new(emu.libretro_core());
|
|
||||||
emu.register_component(sdl2_comp);
|
let mut sdl_context = sdl2::init().unwrap();
|
||||||
|
|
||||||
|
let sdl2_canvas = SimpleSdl2CanvasComponent::new(&mut sdl_context, emu.libretro_core());
|
||||||
|
emu.register_component(sdl2_canvas);
|
||||||
|
|
||||||
|
let sdl2_audio = SimpleSdl2AudioComponent::new(&mut sdl_context, emu.libretro_core());
|
||||||
|
emu.register_component(sdl2_audio);
|
||||||
|
|
||||||
|
emu.register_component(SimpleSdl2GamepadComponent::new(&mut sdl_context));
|
||||||
|
|
||||||
|
let sleep_fps = SleepFramerateLimitComponent::new(emu.libretro_core());
|
||||||
|
emu.register_component(sleep_fps);
|
||||||
|
|
||||||
emu.register_component(StderrLogComponent::default());
|
emu.register_component(StderrLogComponent::default());
|
||||||
emu.register_component(StderrSysInfoLogComponent::default());
|
|
||||||
emu.register_component(PathBufComponent {
|
emu.register_component(PathBufComponent {
|
||||||
sys_path: opt.system.clone(),
|
sys_path: opt.system.clone(),
|
||||||
libretro_path: Some(opt.core.to_path_buf()),
|
libretro_path: Some(opt.core.to_path_buf()),
|
||||||
core_assets_path: None,
|
core_assets_path: None,
|
||||||
save_path: Some(std::env::temp_dir()),
|
save_path: Some(std::env::temp_dir()),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(video) = opt.video {
|
if let Some(video) = opt.video {
|
||||||
ffmpeg::log::set_level(ffmpeg::log::Level::Info);
|
ffmpeg::log::set_level(ffmpeg::log::Level::Info);
|
||||||
ffmpeg::init().unwrap();
|
ffmpeg::init().unwrap();
|
||||||
let ffmpeg_comp = FfmpegComponent::new(emu.libretro_core(), video);
|
let ffmpeg_comp = FfmpegComponent::new(emu.libretro_core(), video);
|
||||||
emu.register_component(ffmpeg_comp);
|
emu.register_component(ffmpeg_comp);
|
||||||
}
|
}
|
||||||
|
|
||||||
emu.load_game(&opt.rom).unwrap();
|
emu.load_game(&opt.rom).unwrap();
|
||||||
if let Some(state) = opt.state {
|
if let Some(state) = opt.state {
|
||||||
emu.unserialize_path(state).unwrap();
|
emu.unserialize_path(state).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut frame = 0;
|
let mut frame = 0;
|
||||||
while let ControlFlow::Continue = emu.run() {
|
while let ControlFlow::Continue = emu.run() {
|
||||||
frame += 1;
|
frame += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!("Ran for {} frames.", frame);
|
eprintln!("Ran for {} frames.", frame);
|
||||||
}
|
}
|
||||||
|
|
|
@ -429,9 +429,7 @@ impl RetroCallbacks for FfmpegComponent {
|
||||||
|
|
||||||
fn get_variable(&mut self, key: &str) -> Option<String> {
|
fn get_variable(&mut self, key: &str) -> Option<String> {
|
||||||
match key {
|
match key {
|
||||||
"beetle_saturn_analog_stick_deadzone" => Some("15%".to_string()),
|
|
||||||
"parallel-n64-gfxplugin" => Some("angrylion".to_string()),
|
"parallel-n64-gfxplugin" => Some("angrylion".to_string()),
|
||||||
"parallel-n64-astick-deadzone" => Some("15%".to_string()),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,323 +1,7 @@
|
||||||
use std::error::Error;
|
mod canvas;
|
||||||
use std::ffi::CStr;
|
mod audio;
|
||||||
use std::path::Path;
|
mod gamepad;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
pub use canvas::SimpleSdl2CanvasComponent;
|
||||||
use crate::base::ControlFlow;
|
pub use audio::SimpleSdl2AudioComponent;
|
||||||
|
pub use gamepad::SimpleSdl2GamepadComponent;
|
||||||
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<u32>,
|
|
||||||
|
|
||||||
av_info: SystemAvInfo,
|
|
||||||
|
|
||||||
_sdl_context: sdl2::Sdl,
|
|
||||||
|
|
||||||
// video bits
|
|
||||||
canvas: WindowCanvas,
|
|
||||||
pixel_format: sdl2::pixels::PixelFormatEnum,
|
|
||||||
|
|
||||||
// audio bits
|
|
||||||
audio_buffer: Vec<i16>,
|
|
||||||
audio_spec: AudioSpec,
|
|
||||||
audio_device: AudioDevice<MySdlAudio>,
|
|
||||||
audio_sender: crossbeam_channel::Sender<Vec<i16>>,
|
|
||||||
|
|
||||||
// input bits
|
|
||||||
gamepad_subsys: sdl2::GameControllerSubsystem,
|
|
||||||
gamepads: Vec<GameController>,
|
|
||||||
|
|
||||||
// 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<dyn Error>> {
|
|
||||||
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<bool> {
|
|
||||||
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<String> {
|
|
||||||
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<u64> {
|
|
||||||
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<bool> {
|
|
||||||
self.set_geometry(&av_info.geometry);
|
|
||||||
self.av_info = av_info.clone();
|
|
||||||
Some(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_controller_info(&mut self, controller_info: &Vec<ControllerDescription2>) -> Option<bool> {
|
|
||||||
for ci in controller_info {
|
|
||||||
// so we can have analog support in beetle/mednafen saturn
|
|
||||||
if ci.name.as_str() == "3D Control Pad" {
|
|
||||||
self.preferred_pad = Some(ci.device_id());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_geometry(&mut self, geom: &GameGeometry) -> Option<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.clone();
|
|
||||||
Some(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MySdlAudio {
|
|
||||||
audio_spec: AudioSpec,
|
|
||||||
audio_receiver: crossbeam_channel::Receiver<Vec<i16>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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_timeout(Duration::from_millis(500)) {
|
|
||||||
out.copy_from_slice(&samples[..out.len()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn button_map(retro_button: &JoypadButton) -> Option<Button> {
|
|
||||||
match retro_button {
|
|
||||||
JoypadButton::B => Some(Button::A),
|
|
||||||
JoypadButton::Y => Some(Button::X),
|
|
||||||
JoypadButton::Select => Some(Button::Back),
|
|
||||||
JoypadButton::Start => Some(Button::Start),
|
|
||||||
JoypadButton::Up => Some(Button::DPadUp),
|
|
||||||
JoypadButton::Down => Some(Button::DPadDown),
|
|
||||||
JoypadButton::Left => Some(Button::DPadLeft),
|
|
||||||
JoypadButton::Right => Some(Button::DPadRight),
|
|
||||||
JoypadButton::A => Some(Button::B),
|
|
||||||
JoypadButton::X => Some(Button::Y),
|
|
||||||
JoypadButton::L => Some(Button::LeftShoulder),
|
|
||||||
JoypadButton::R => Some(Button::RightShoulder),
|
|
||||||
// SDL2 controller API doesn't have L2/R2 as buttons, they're considered axes
|
|
||||||
JoypadButton::L2 => None,
|
|
||||||
JoypadButton::R2 => None,
|
|
||||||
JoypadButton::L3 => Some(Button::LeftStick),
|
|
||||||
JoypadButton::R3 => Some(Button::RightStick),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn axis_map(index: InputIndex, axis: AnalogAxis) -> Axis {
|
|
||||||
match (index, axis) {
|
|
||||||
(InputIndex::Left, AnalogAxis::X) => Axis::LeftX,
|
|
||||||
(InputIndex::Left, AnalogAxis::Y) => Axis::LeftY,
|
|
||||||
(InputIndex::Right, AnalogAxis::X) => Axis::RightX,
|
|
||||||
(InputIndex::Right, AnalogAxis::Y) => Axis::RightY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use sdl2::Sdl;
|
||||||
|
use sdl2::audio::{AudioCallback, AudioDevice, AudioFormat, AudioSpec, AudioSpecDesired};
|
||||||
|
|
||||||
|
struct MySdlAudio {
|
||||||
|
audio_spec: AudioSpec,
|
||||||
|
audio_receiver: crossbeam_channel::Receiver<Vec<i16>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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_timeout(Duration::from_millis(500)) {
|
||||||
|
out.copy_from_slice(&samples[..out.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SimpleSdl2AudioComponent {
|
||||||
|
sample_rate: f64,
|
||||||
|
audio_buffer: Vec<i16>,
|
||||||
|
audio_spec: AudioSpec,
|
||||||
|
audio_device: AudioDevice<MySdlAudio>,
|
||||||
|
audio_sender: crossbeam_channel::Sender<Vec<i16>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroCallbacks for SimpleSdl2AudioComponent {
|
||||||
|
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 set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option<bool> {
|
||||||
|
self.sample_rate = av_info.timing.sample_rate;
|
||||||
|
// TODO: change/reinitialize
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroComponent for SimpleSdl2AudioComponent {
|
||||||
|
fn post_load_game(&mut self, _retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
self.audio_device.resume();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleSdl2AudioComponent {
|
||||||
|
pub fn new(sdl_context: &mut Sdl, retro: &LibretroWrapper) -> Self {
|
||||||
|
let mut sample_rate = retro.get_system_av_info().timing.sample_rate;
|
||||||
|
|
||||||
|
// HACK: some cores don't report this 'til we get an env call to set_system_av_info...
|
||||||
|
// which is too late for this constructor to pass along to SDL, so...
|
||||||
|
// less than ideal, but default to the old libsnes default value i guess.
|
||||||
|
if sample_rate == 0.0 {
|
||||||
|
sample_rate = 32040.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (audio_sender, audio_receiver) = crossbeam_channel::bounded(2);
|
||||||
|
|
||||||
|
let audio = sdl_context.audio().unwrap();
|
||||||
|
let desired_spec = AudioSpecDesired {
|
||||||
|
freq: Some(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();
|
||||||
|
|
||||||
|
SimpleSdl2AudioComponent {
|
||||||
|
sample_rate,
|
||||||
|
audio_buffer: Default::default(),
|
||||||
|
audio_spec: audio_spec.unwrap(),
|
||||||
|
audio_device,
|
||||||
|
audio_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
use crate::base::ControlFlow;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use std::ffi::CStr;
|
||||||
|
|
||||||
|
use sdl2::Sdl;
|
||||||
|
use sdl2::rect::Rect;
|
||||||
|
use sdl2::render::WindowCanvas;
|
||||||
|
|
||||||
|
pub struct SimpleSdl2CanvasComponent {
|
||||||
|
canvas: WindowCanvas,
|
||||||
|
pixel_format: sdl2::pixels::PixelFormatEnum,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleSdl2CanvasComponent {
|
||||||
|
pub fn new(sdl_context: &mut Sdl, 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 geometry = retro.get_system_av_info().geometry;
|
||||||
|
let pixel_format = sdl2::pixels::PixelFormatEnum::ARGB1555;
|
||||||
|
|
||||||
|
let window = sdl_context
|
||||||
|
.video()
|
||||||
|
.unwrap()
|
||||||
|
.window(title.as_str(), geometry.base_width, geometry.base_height)
|
||||||
|
.opengl()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let canvas = window.into_canvas().build().unwrap();
|
||||||
|
|
||||||
|
SimpleSdl2CanvasComponent {
|
||||||
|
canvas,
|
||||||
|
pixel_format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroCallbacks for SimpleSdl2CanvasComponent {
|
||||||
|
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 set_pixel_format(&mut self, pix_fmt: PixelFormat) -> Option<bool> {
|
||||||
|
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<String> {
|
||||||
|
match key {
|
||||||
|
"parallel-n64-gfxplugin" => Some("angrylion".to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option<bool> {
|
||||||
|
self.set_geometry(&av_info.geometry);
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_geometry(&mut self, geom: &GameGeometry) -> Option<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);
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroComponent for SimpleSdl2CanvasComponent {
|
||||||
|
fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow {
|
||||||
|
self.canvas.present();
|
||||||
|
ControlFlow::Continue
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
use crate::base::ControlFlow;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use sdl2::Sdl;
|
||||||
|
use sdl2::controller::{Axis, Button, GameController};
|
||||||
|
use sdl2::event::Event;
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
|
||||||
|
pub struct SimpleSdl2GamepadComponent {
|
||||||
|
preferred_pad: Option<u32>,
|
||||||
|
|
||||||
|
gamepad_subsys: sdl2::GameControllerSubsystem,
|
||||||
|
gamepads: Vec<GameController>,
|
||||||
|
event_pump: sdl2::EventPump,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroCallbacks for SimpleSdl2GamepadComponent {
|
||||||
|
fn input_poll(&mut self) {
|
||||||
|
self.gamepad_subsys.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 {
|
||||||
|
match self.gamepads.get(port as usize) {
|
||||||
|
Some(gamepad) => {
|
||||||
|
match device {
|
||||||
|
InputDeviceId::Joypad(button) => {
|
||||||
|
match button_map(&button) {
|
||||||
|
Some(x) => gamepad.button(x) as i16,
|
||||||
|
None => match button {
|
||||||
|
JoypadButton::L2 => gamepad.axis(Axis::TriggerLeft),
|
||||||
|
JoypadButton::R2 => gamepad.axis(Axis::TriggerRight),
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputDeviceId::Analog(axis) => gamepad.axis(axis_map(index, axis)),
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_variable(&mut self, key: &str) -> Option<String> {
|
||||||
|
match key {
|
||||||
|
"beetle_saturn_analog_stick_deadzone" => Some("15%".to_string()),
|
||||||
|
"parallel-n64-astick-deadzone" => Some("15%".to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_input_device_capabilities(&mut self) -> Option<u64> {
|
||||||
|
let bits = (1 << (DeviceType::Joypad as u32)) | (1 << (DeviceType::Analog as u32));
|
||||||
|
Some(bits as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_controller_info(&mut self, controller_info: &Vec<ControllerDescription2>) -> Option<bool> {
|
||||||
|
for ci in controller_info {
|
||||||
|
// so we can have analog support in beetle/mednafen saturn
|
||||||
|
if ci.name.as_str() == "3D Control Pad" {
|
||||||
|
self.preferred_pad = Some(ci.device_id());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroComponent for SimpleSdl2GamepadComponent {
|
||||||
|
fn pre_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow {
|
||||||
|
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_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
if let Some(device) = self.preferred_pad {
|
||||||
|
for port in 0..self.gamepads.len() as u32 {
|
||||||
|
retro.set_controller_port_device(port, device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleSdl2GamepadComponent {
|
||||||
|
pub fn new(sdl_context: &mut Sdl) -> Self {
|
||||||
|
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();
|
||||||
|
|
||||||
|
SimpleSdl2GamepadComponent {
|
||||||
|
preferred_pad: None,
|
||||||
|
gamepad_subsys,
|
||||||
|
gamepads,
|
||||||
|
event_pump,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button_map(retro_button: &JoypadButton) -> Option<Button> {
|
||||||
|
match retro_button {
|
||||||
|
JoypadButton::B => Some(Button::A),
|
||||||
|
JoypadButton::Y => Some(Button::X),
|
||||||
|
JoypadButton::Select => Some(Button::Back),
|
||||||
|
JoypadButton::Start => Some(Button::Start),
|
||||||
|
JoypadButton::Up => Some(Button::DPadUp),
|
||||||
|
JoypadButton::Down => Some(Button::DPadDown),
|
||||||
|
JoypadButton::Left => Some(Button::DPadLeft),
|
||||||
|
JoypadButton::Right => Some(Button::DPadRight),
|
||||||
|
JoypadButton::A => Some(Button::B),
|
||||||
|
JoypadButton::X => Some(Button::Y),
|
||||||
|
JoypadButton::L => Some(Button::LeftShoulder),
|
||||||
|
JoypadButton::R => Some(Button::RightShoulder),
|
||||||
|
// SDL2 controller API doesn't have L2/R2 as buttons, they're considered axes
|
||||||
|
JoypadButton::L2 => None,
|
||||||
|
JoypadButton::R2 => None,
|
||||||
|
JoypadButton::L3 => Some(Button::LeftStick),
|
||||||
|
JoypadButton::R3 => Some(Button::RightStick),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis_map(index: InputIndex, axis: AnalogAxis) -> Axis {
|
||||||
|
match (index, axis) {
|
||||||
|
(InputIndex::Left, AnalogAxis::X) => Axis::LeftX,
|
||||||
|
(InputIndex::Left, AnalogAxis::Y) => Axis::LeftY,
|
||||||
|
(InputIndex::Right, AnalogAxis::X) => Axis::RightX,
|
||||||
|
(InputIndex::Right, AnalogAxis::Y) => Axis::RightY,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::base::ControlFlow;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -78,3 +81,42 @@ impl RetroCallbacks for StderrCallTraceComponent {
|
||||||
}
|
}
|
||||||
// TODO: etc...
|
// TODO: etc...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SleepFramerateLimitComponent {
|
||||||
|
fps: f64,
|
||||||
|
frame_begin: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroCallbacks for SleepFramerateLimitComponent {
|
||||||
|
fn set_system_av_info(&mut self, system_av_info: &SystemAvInfo) -> Option<bool> {
|
||||||
|
self.fps = system_av_info.timing.fps;
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetroComponent for SleepFramerateLimitComponent {
|
||||||
|
fn pre_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow {
|
||||||
|
self.frame_begin = Instant::now();
|
||||||
|
ControlFlow::Continue
|
||||||
|
}
|
||||||
|
fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow {
|
||||||
|
// similar hack to the sample rate, make sure we don't divide by zero.
|
||||||
|
let mut spf = 1.0 / self.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SleepFramerateLimitComponent {
|
||||||
|
pub fn new(retro: &mut LibretroWrapper) -> Self {
|
||||||
|
SleepFramerateLimitComponent {
|
||||||
|
fps: retro.get_system_av_info().timing.fps,
|
||||||
|
frame_begin: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue