2019-11-16 08:03:30 +01:00
|
|
|
extern crate crossbeam_channel;
|
2019-11-15 04:16:44 +01:00
|
|
|
extern crate rustro;
|
2019-11-15 06:21:58 +01:00
|
|
|
extern crate sdl2;
|
|
|
|
|
|
|
|
use rustro::retro;
|
2019-11-16 08:03:30 +01:00
|
|
|
use rustro::retro::ffi::{GameGeometry, SystemAvInfo};
|
2019-11-17 05:27:42 +01:00
|
|
|
use rustro::retro::constants::{Input, DeviceIndex, JoypadButton, AnalogAxis};
|
2019-11-11 01:24:29 +01:00
|
|
|
|
2019-11-11 08:21:38 +01:00
|
|
|
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};
|
2019-11-11 01:24:29 +01:00
|
|
|
|
|
|
|
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::{TextureCreator, WindowCanvas};
|
2019-11-15 10:15:51 +01:00
|
|
|
use sdl2::video::WindowContext;
|
2019-11-15 06:21:58 +01:00
|
|
|
|
|
|
|
struct MyEmulator {
|
|
|
|
retro: retro::wrapper::LibretroWrapper,
|
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
|
|
|
texture_creator: TextureCreator<WindowContext>,
|
|
|
|
|
|
|
|
// 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-11-15 06:21:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MyEmulator {
|
2019-11-16 05:59:08 +01:00
|
|
|
pub fn with_core(core: impl AsRef<Path>) -> Pin<Box<Self>> {
|
2019-11-15 06:21:58 +01:00
|
|
|
let lib = libloading::Library::new(core.as_ref()).unwrap();
|
|
|
|
let raw_retro = retro::loading::LibretroApi::from_library(lib).unwrap();
|
|
|
|
let retro = retro::wrapper::LibretroWrapper::from(raw_retro);
|
2019-11-16 08:27:19 +01:00
|
|
|
|
|
|
|
let title = format!(
|
|
|
|
"{} - rust libretro",
|
2019-11-16 08:03:30 +01:00
|
|
|
unsafe { CStr::from_ptr(retro.as_ref().get_system_info().library_name) }
|
|
|
|
.to_string_lossy()
|
|
|
|
);
|
2019-11-15 06:21:58 +01:00
|
|
|
|
2019-11-15 10:15:51 +01:00
|
|
|
let av_info = retro.as_ref().get_system_av_info();
|
2019-11-15 06:21:58 +01:00
|
|
|
let pixel_format = sdl2::pixels::PixelFormatEnum::ABGR1555;
|
|
|
|
|
2019-11-16 08:03:30 +01:00
|
|
|
let sdl_context = sdl2::init().unwrap();
|
|
|
|
|
|
|
|
let (width, height) = (av_info.geometry.base_width, av_info.geometry.base_height);
|
|
|
|
let window = sdl_context
|
|
|
|
.video()
|
|
|
|
.unwrap()
|
2019-11-16 08:27:19 +01:00
|
|
|
.window(title.as_str(), width, height)
|
2019-11-16 08:03:30 +01:00
|
|
|
.opengl()
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let canvas = window.into_canvas().build().unwrap();
|
|
|
|
let texture_creator = canvas.texture_creator();
|
|
|
|
|
|
|
|
let (audio_sender, audio_receiver) = crossbeam_channel::bounded(2);
|
|
|
|
|
|
|
|
let audio = sdl_context.audio().unwrap();
|
2019-11-16 08:27:19 +01:00
|
|
|
let mut 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,
|
|
|
|
};
|
2019-11-16 08:27:19 +01:00
|
|
|
if let Some(0) = desired_spec.freq {
|
2019-11-17 05:27:42 +01:00
|
|
|
desired_spec.freq = Some(32040); // old default, fix for cores that don't report it
|
2019-11-16 08:27:19 +01:00
|
|
|
}
|
2019-11-16 08:03:30 +01:00
|
|
|
let mut audio_spec = None;
|
|
|
|
let audio_device = audio
|
|
|
|
.open_playback(None, &desired_spec, |spec| {
|
2019-11-17 06:34:28 +01:00
|
|
|
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-11-16 08:03:30 +01:00
|
|
|
let emu = MyEmulator {
|
|
|
|
retro,
|
|
|
|
av_info,
|
|
|
|
sdl_context,
|
|
|
|
canvas,
|
|
|
|
pixel_format,
|
|
|
|
texture_creator,
|
|
|
|
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-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-16 05:59:08 +01:00
|
|
|
pin_emu.retro.as_ref().init();
|
|
|
|
pin_emu
|
2019-11-15 06:21:58 +01:00
|
|
|
}
|
2019-11-11 01:24:29 +01:00
|
|
|
|
2019-11-16 08:03:30 +01:00
|
|
|
pub fn run(&mut self) {
|
|
|
|
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,
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The rest of the game loop goes here...
|
|
|
|
self.retro.as_ref().run();
|
|
|
|
self.canvas.present();
|
2019-11-15 10:15:51 +01:00
|
|
|
|
2019-11-16 08:03:30 +01:00
|
|
|
Duration::from_secs_f64(1.0 / self.av_info.timing.fps)
|
|
|
|
.checked_sub(frame_begin.elapsed())
|
|
|
|
.map(std::thread::sleep);
|
|
|
|
}
|
2019-11-15 06:21:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn load_game(&self, rom: impl AsRef<Path>) {
|
2019-11-16 08:03:30 +01:00
|
|
|
let mut data = None;
|
|
|
|
let mut v = Vec::new();
|
|
|
|
if let Ok(mut f) = std::fs::File::open(rom.as_ref()) {
|
|
|
|
if f.read_to_end(&mut v).is_ok() {
|
|
|
|
data = Some(v.as_ref());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.retro
|
|
|
|
.as_ref()
|
|
|
|
.load_game(Some(rom.as_ref()), data, None)
|
|
|
|
.unwrap();
|
2019-11-15 06:21:58 +01:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl retro::wrapper::Handler for MyEmulator {
|
2019-11-11 01:24:29 +01:00
|
|
|
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.texture_creator
|
|
|
|
.create_texture_static(self.pixel_format, width, height)
|
|
|
|
{
|
|
|
|
if tex.update(rect, data, pitch as usize).is_ok() {
|
|
|
|
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: Input, index: DeviceIndex) -> i16 {
|
|
|
|
match self.gamepads.get(port as usize) {
|
|
|
|
Some(gamepad) => {
|
|
|
|
match device {
|
|
|
|
Input::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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Input::Analog(axis) => gamepad.axis(axis_map(index, axis)),
|
|
|
|
_ => 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-15 06:21:58 +01:00
|
|
|
fn set_pixel_format(&mut self, pix_fmt: retro::ffi::PixelFormat) -> bool {
|
|
|
|
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
|
2019-11-11 01:24:29 +01:00
|
|
|
}
|
2019-11-16 08:03:30 +01:00
|
|
|
|
|
|
|
fn set_system_av_info(&mut self, av_info: SystemAvInfo) -> bool {
|
|
|
|
self.av_info = av_info;
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_geometry(&mut self, geom: GameGeometry) -> bool {
|
|
|
|
self.av_info.geometry = geom;
|
|
|
|
true
|
|
|
|
}
|
2019-11-11 01:24:29 +01:00
|
|
|
}
|
|
|
|
|
2019-11-17 05:27:42 +01:00
|
|
|
|
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]) {
|
2019-11-17 06:34:28 +01:00
|
|
|
if self.audio_spec.format == AudioFormat::S16LSB {
|
|
|
|
if let Ok(samples) = self.audio_receiver.recv() {
|
|
|
|
out.copy_from_slice(&samples[..out.len()]);
|
2019-11-16 08:03:30 +01:00
|
|
|
}
|
2019-11-16 05:59:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-15 10:15:51 +01:00
|
|
|
pub fn main() -> failure::Fallible<()> {
|
2019-11-15 06:21:58 +01:00
|
|
|
let opt: Opt = Opt::from_args();
|
|
|
|
let mut emu = MyEmulator::with_core(&opt.core);
|
|
|
|
emu.load_game(&opt.rom);
|
2019-11-16 08:03:30 +01:00
|
|
|
emu.run();
|
2019-11-15 06:21:58 +01:00
|
|
|
|
2019-11-15 10:15:51 +01:00
|
|
|
Ok(())
|
2019-11-15 06:21:58 +01:00
|
|
|
}
|
2019-11-11 01:24:29 +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,
|
|
|
|
}
|
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),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn axis_map(index: DeviceIndex, axis: AnalogAxis) -> Axis {
|
|
|
|
match (index, axis) {
|
|
|
|
(DeviceIndex::Left, AnalogAxis::X) => Axis::LeftX,
|
|
|
|
(DeviceIndex::Left, AnalogAxis::Y) => Axis::LeftY,
|
|
|
|
(DeviceIndex::Right, AnalogAxis::X) => Axis::RightX,
|
|
|
|
(DeviceIndex::Right, AnalogAxis::Y) => Axis::RightY,
|
|
|
|
}
|
|
|
|
}
|