ferretro/ferretro_base/examples/sdl2_emulator.rs

473 lines
16 KiB
Rust
Raw Normal View History

2019-11-16 08:03:30 +01:00
extern crate crossbeam_channel;
2021-08-18 23:47:57 +02:00
extern crate ferretro_base;
2019-11-15 06:21:58 +01:00
extern crate sdl2;
2021-08-18 23:47:57 +02:00
use ferretro_base::retro;
use ferretro_base::retro::ffi::{GameGeometry, SystemInfo, SystemAvInfo};
use ferretro_base::retro::constants::{InputIndex, JoypadButton, AnalogAxis, DeviceType};
use ferretro_base::retro::wrapped_types::{ControllerDescription2, InputDescriptor2, InputDeviceId, SubsystemInfo2, Variable2};
use ferretro_base::retro::wrapper::LibretroWrapper;
use std::ffi::CStr;
2019-11-16 08:03:30 +01:00
use std::io::Read;
use std::path::{Path, PathBuf};
2019-11-16 05:59:08 +01:00
use std::pin::Pin;
use std::time::{Duration, Instant};
use structopt::StructOpt;
2019-11-16 08:03:30 +01:00
use sdl2::audio::{AudioCallback, AudioFormat, AudioSpec, AudioSpecDesired, AudioDevice};
2019-11-17 05:27:42 +01:00
use sdl2::controller::{GameController, Button, Axis};
2019-11-15 10:15:51 +01:00
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
2019-11-16 08:03:30 +01:00
use sdl2::rect::Rect;
use sdl2::render::WindowCanvas;
2019-11-15 06:21:58 +01:00
struct MyEmulator {
retro: retro::wrapper::LibretroWrapper,
core_path: PathBuf,
sys_path: Option<PathBuf>,
preferred_pad: Option<u32>,
2019-11-17 07:25:03 +01:00
sys_info: SystemInfo,
2019-11-15 10:15:51 +01:00
av_info: SystemAvInfo,
2019-11-16 08:03:30 +01:00
sdl_context: sdl2::Sdl,
// video bits
canvas: WindowCanvas,
2019-11-15 06:21:58 +01:00
pixel_format: sdl2::pixels::PixelFormatEnum,
2019-11-16 08:03:30 +01:00
// audio bits
audio_buffer: Vec<i16>,
audio_spec: AudioSpec,
audio_device: AudioDevice<MySdlAudio>,
audio_sender: crossbeam_channel::Sender<Vec<i16>>,
2019-11-17 05:27:42 +01:00
// input bits
gamepad_subsys: sdl2::GameControllerSubsystem,
gamepads: Vec<GameController>,
2019-12-23 09:22:35 +01:00
pressed_keys: Vec<Keycode>,
2019-11-15 06:21:58 +01:00
}
impl MyEmulator {
pub fn new(core_path: impl AsRef<Path>, sys_path: &Option<impl AsRef<Path>>) -> Pin<Box<Self>> {
let core_path = PathBuf::from(core_path.as_ref());
let lib = libloading::Library::new(&core_path).unwrap();
2019-11-15 06:21:58 +01:00
let raw_retro = retro::loading::LibretroApi::from_library(lib).unwrap();
let retro = retro::wrapper::LibretroWrapper::from(raw_retro);
2019-11-17 07:34:01 +01:00
let sys_info = retro.get_system_info();
let title = format!(
"{} - rust libretro",
2019-11-17 07:25:03 +01:00
unsafe { CStr::from_ptr(sys_info.library_name) }.to_string_lossy()
2019-11-16 08:03:30 +01:00
);
2019-11-15 06:21:58 +01:00
2019-11-17 07:34:01 +01:00
let mut av_info = retro.get_system_av_info();
let pixel_format = sdl2::pixels::PixelFormatEnum::ARGB1555;
2019-11-15 06:21:58 +01:00
2019-11-17 07:04:15 +01:00
// 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;
}
2019-11-16 08:03:30 +01:00
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)
2019-11-16 08:03:30 +01:00
.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();
2019-11-17 07:04:15 +01:00
let desired_spec = AudioSpecDesired {
2019-11-16 08:03:30 +01:00
freq: Some(av_info.timing.sample_rate.round() as i32),
channels: Some(2),
samples: None,
};
let mut audio_spec = None;
let audio_device = audio
.open_playback(None, &desired_spec, |spec| {
if spec.format != AudioFormat::S16LSB {
println!("unsupported audio format {:?}", spec.format);
}
2019-11-16 08:03:30 +01:00
audio_spec = Some(spec.clone());
MySdlAudio {
audio_spec: spec,
audio_receiver,
}
})
.unwrap();
2019-11-17 05:27:42 +01:00
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());
}
2019-12-23 09:22:35 +01:00
let pressed_keys = Vec::new();
2019-11-16 08:03:30 +01:00
let emu = MyEmulator {
retro,
core_path,
sys_path: sys_path.as_ref().map(|p| p.as_ref().to_path_buf()),
preferred_pad: None,
2019-11-16 08:03:30 +01:00
av_info,
2019-11-17 07:25:03 +01:00
sys_info,
2019-11-16 08:03:30 +01:00
sdl_context,
canvas,
pixel_format,
audio_buffer: Default::default(),
audio_spec: audio_spec.unwrap(),
audio_device,
audio_sender,
2019-11-17 05:27:42 +01:00
gamepad_subsys,
gamepads,
2019-12-23 09:22:35 +01:00
pressed_keys,
2019-11-16 08:03:30 +01:00
};
let mut pin_emu = Box::pin(emu);
2019-11-17 05:27:42 +01:00
retro::wrapper::set_handler(pin_emu.as_mut());
2019-11-17 07:34:01 +01:00
pin_emu.retro.init();
2019-11-16 05:59:08 +01:00
pin_emu
2019-11-15 06:21:58 +01:00
}
pub fn run_loop(&mut self) {
2019-11-16 08:03:30 +01:00
self.audio_device.resume();
let mut event_pump = self.sdl_context.event_pump().unwrap();
'running: loop {
let frame_begin = Instant::now();
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => break 'running,
_ => {}
}
}
2019-12-23 09:22:35 +01:00
self.update_key_state(&event_pump.keyboard_state());
2019-11-16 08:03:30 +01:00
// The rest of the game loop goes here...
2019-11-17 07:34:01 +01:00
self.retro.run();
2019-11-16 08:03:30 +01:00
self.canvas.present();
2019-11-15 10:15:51 +01:00
2019-11-18 10:01:12 +01:00
// 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() {
2019-11-18 10:01:12 +01:00
spf = 1.0 / 60.0;
}
Duration::from_secs_f64(spf)
2019-11-16 08:03:30 +01:00
.checked_sub(frame_begin.elapsed())
.map(std::thread::sleep);
}
2019-11-15 06:21:58 +01:00
}
pub fn load_game(&mut self, rom: impl AsRef<Path>) {
2019-11-17 07:25:03 +01:00
let path = rom.as_ref();
2019-11-16 08:03:30 +01:00
let mut data = None;
let mut v = Vec::new();
2019-11-17 07:25:03 +01:00
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());
}
2019-11-16 08:03:30 +01:00
}
}
self.retro
2019-11-17 07:25:03 +01:00
.load_game(Some(path), data, None)
2019-11-16 08:03:30 +01:00
.unwrap();
if let Some(device) = self.preferred_pad {
for port in 0..self.gamepads.len() as u32 {
self.retro.set_controller_port_device(port, device);
}
}
2019-11-15 06:21:58 +01:00
}
2019-12-23 09:22:35 +01:00
pub fn update_key_state<'a>(&mut self, keyboard_state: &sdl2::keyboard::KeyboardState<'a>){
let keys: Vec<Keycode> = keyboard_state.pressed_scancodes().filter_map(Keycode::from_scancode).collect();
self.pressed_keys = keys;
}
2019-11-16 08:03:30 +01:00
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);
}
}
2019-12-23 09:22:35 +01:00
fn input_state_gamepad(&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 input_state_keyboard(&mut self, port: u32, device: &InputDeviceId, _index: &InputIndex) -> i16 {
2019-12-23 09:22:35 +01:00
if port != 0 { // Keyboard only controls the first port.
return 0;
}
match device {
InputDeviceId::Joypad(button) => {
match keyboard_map(&button) {
Some(x) => return if self.pressed_keys.contains(&x) {1} else {0},
None => 0
}
},
_ => 0
}
}
2019-11-16 08:03:30 +01:00
}
2019-11-17 07:25:03 +01:00
impl Drop for MyEmulator {
fn drop(&mut self) {
retro::wrapper::unset_handler();
}
}
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) {
2019-11-16 05:59:08 +01:00
let rect = Rect::new(0, 0, width, height);
2019-11-16 08:03:30 +01:00
if let Ok(mut tex) =
self.canvas
.texture_creator()
2019-11-16 08:03:30 +01:00
.create_texture_static(self.pixel_format, width, height)
{
if tex.update(rect, data, pitch as usize).is_ok() {
2019-11-17 07:04:15 +01:00
self.canvas.clear();
2019-11-16 08:03:30 +01:00
self.canvas.copy(&tex, None, None).unwrap();
}
2019-11-15 10:15:51 +01:00
}
2019-11-15 06:21:58 +01:00
}
2019-11-16 08:03:30 +01:00
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()
}
2019-11-17 05:27:42 +01:00
fn input_poll(&mut self) {
self.gamepad_subsys.update();
}
fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 {
2019-12-23 09:22:35 +01:00
let gamepad_state = self.input_state_gamepad(port, &device, &index);
if gamepad_state != 0 {
2019-12-23 09:22:35 +01:00
return gamepad_state;
2019-11-17 05:27:42 +01:00
}
2019-12-23 09:22:35 +01:00
return self.input_state_keyboard(port, &device, &index);
2019-11-17 05:27:42 +01:00
}
fn get_system_directory(&mut self) -> Option<PathBuf> {
self.sys_path.clone()
}
fn set_pixel_format(&mut self, pix_fmt: retro::ffi::PixelFormat) -> Option<bool> {
2019-11-15 06:21:58 +01:00
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,
};
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 set_variables(&mut self, variables: &Vec<Variable2>) -> Option<bool> {
for v in variables {
eprintln!("{:?}", v);
}
Some(true)
}
fn get_libretro_path(&mut self) -> Option<PathBuf> {
Some(self.core_path.clone())
}
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 get_save_directory(&mut self) -> Option<PathBuf> {
Some(std::env::temp_dir())
}
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)
2019-11-16 08:03:30 +01:00
}
fn set_subsystem_info(&mut self, subsystem_info: &Vec<SubsystemInfo2>) -> Option<bool> {
println!("subsystem info: {:?}", subsystem_info);
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_input_descriptors(&mut self, descriptors: &Vec<InputDescriptor2>) -> Option<bool> {
for id in descriptors {
println!("{:?}", id);
}
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)
2019-11-16 08:03:30 +01:00
}
2019-11-17 06:34:01 +01:00
fn log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) {
eprint!("[{:?}] {}", level, msg);
}
}
2019-11-16 08:03:30 +01:00
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 {
2021-08-18 01:50:26 +02:00
if let Ok(samples) = self.audio_receiver.recv_timeout(Duration::from_millis(500)) {
out.copy_from_slice(&samples[..out.len()]);
2019-11-16 08:03:30 +01:00
}
2019-11-16 05:59:08 +01:00
}
}
}
pub fn main() {
2019-11-15 06:21:58 +01:00
let opt: Opt = Opt::from_args();
let mut emu = MyEmulator::new(&opt.core, &opt.system);
2019-11-15 06:21:58 +01:00
emu.load_game(&opt.rom);
emu.run_loop();
2019-11-15 06:21:58 +01:00
}
#[derive(StructOpt)]
struct Opt {
/// Core module to use.
#[structopt(short, long, parse(from_os_str))]
core: PathBuf,
/// ROM to load using the core.
#[structopt(short, long, parse(from_os_str))]
rom: PathBuf,
/// System directory, often containing BIOS files
#[structopt(short, long, parse(from_os_str))]
system: Option<PathBuf>,
}
2019-11-16 08:03:30 +01:00
2019-11-17 05:27:42 +01:00
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),
}
}
2019-12-23 09:22:35 +01:00
fn keyboard_map(retro_button: &JoypadButton) -> Option<Keycode> {
match retro_button {
JoypadButton::B => Some(Keycode::K),
JoypadButton::Y => Some(Keycode::J),
JoypadButton::Select => Some(Keycode::Num5),
JoypadButton::Start => Some(Keycode::Num6),
JoypadButton::Up => Some(Keycode::W),
JoypadButton::Down => Some(Keycode::S),
JoypadButton::Left => Some(Keycode::A),
JoypadButton::Right => Some(Keycode::D),
JoypadButton::A => Some(Keycode::L),
JoypadButton::X => Some(Keycode::I),
JoypadButton::L => Some(Keycode::Num1),
JoypadButton::R => Some(Keycode::Num0),
JoypadButton::L2 => None,
JoypadButton::R2 => None,
JoypadButton::L3 => None,
JoypadButton::R3 => None,
}
}
fn axis_map(index: &InputIndex, axis: &AnalogAxis) -> Axis {
2019-11-17 05:27:42 +01:00
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,
2019-11-17 05:27:42 +01:00
}
}