initial commit

This commit is contained in:
Viv Lim 2021-08-02 21:50:52 -07:00
commit ff94e1048e
4 changed files with 3207 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/data

2428
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "ferretro-dev-gui"
version = "0.1.0"
edition = "2018"
authors = ["viv <vvnl+git@protonmail.com>"]
[build-dependencies]
cc = "^1"
[dependencies]
crossbeam-channel = "^0.4"
structopt = "^0.3"
ferretro = { git = "ssh://git@vvn.space:2222/cinnabon/rustro.git", branch = "viv/ffmpeg2"}
failure = "^0.1"
libloading = "^0.5"
mini_gl_fb = { git = "https://github.com/vivlim/mini_gl_fb" } # bumped glutin version to 0.27.0 to match the glium i have
conrod_glium = { git = "https://github.com/vivlim/conrod" } # bumped version of glium to 0.30.1 to use winit 0.24 which fixes https://github.com/rust-windowing/winit/issues/1782
conrod_core = { git = "https://github.com/vivlim/conrod" }
glium = "0.30.1"
gilrs = "0.8.1"

754
src/main.rs Normal file
View File

@ -0,0 +1,754 @@
extern crate crossbeam_channel;
extern crate ferretro;
extern crate mini_gl_fb;
extern crate conrod_glium;
extern crate conrod_core;
extern crate failure;
use conrod_core::{Colorable, Positionable, Widget, widget, widget_ids};
use glium::Surface;
use glium::backend::Facade;
use mini_gl_fb::glutin::dpi::LogicalSize;
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::event::{Event, WindowEvent, VirtualKeyCode, KeyboardInput, ElementState};
use mini_gl_fb::{GlutinBreakout, config, get_fancy};
use mini_gl_fb::glutin::window::{Window, WindowId};
use mini_gl_fb::glutin::event_loop::ControlFlow;
use mini_gl_fb::glutin::platform::run_return::EventLoopExtRunReturn;
use failure::{Fallible};
use ferretro::retro;
use ferretro::retro::ffi::{GameGeometry, SystemInfo, SystemAvInfo};
use ferretro::retro::constants::{InputIndex, JoypadButton, AnalogAxis, DeviceType};
use ferretro::retro::wrapped_types::{ControllerDescription2, InputDescriptor2, InputDeviceId, SubsystemInfo2, Variable2};
use ferretro::retro::wrapper::LibretroWrapper;
use std::borrow::Borrow;
use std::cell::RefCell;
use std::convert::TryInto;
use std::ffi::CStr;
use std::io::{Read, Write};
use std::ops::Add;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::rc::Rc;
use std::time::{Duration, Instant};
use structopt::StructOpt;
use gilrs::{Button, GamepadId, Gilrs, Axis};
struct MyEmulator {
pub retro: retro::wrapper::LibretroWrapper,
pub frame: u64,
core_path: PathBuf,
sys_path: Option<PathBuf>,
game_path: Option<PathBuf>,
preferred_pad: Option<u32>,
sys_info: SystemInfo,
av_info: SystemAvInfo,
gamepads: Gilrs,
title: String,
video_buffer: Vec<u8>,
}
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();
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 title = format!(
"{} - rust libretro",
unsafe { CStr::from_ptr(sys_info.library_name) }.to_string_lossy()
);
let mut av_info = retro.get_system_av_info();
// 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 bpp: usize = 4;
let mut video_buffer = vec![0u8 ; av_info.geometry.base_width as usize * av_info.geometry.base_height as usize * bpp];
let mut gamepads = Gilrs::new().unwrap();
let emu = MyEmulator {
retro,
frame: 0,
core_path,
sys_path: sys_path.as_ref().map(|p| p.as_ref().to_path_buf()),
game_path: None,
preferred_pad: None,
av_info,
sys_info,
gamepads,
title,
video_buffer,
};
let mut pin_emu = Box::pin(emu);
retro::wrapper::set_handler(pin_emu.as_mut());
pin_emu.retro.init();
pin_emu
}
pub fn run_frame(&mut self){
self.frame += 1;
self.retro.run();
// update fb
}
/*
pub fn run(&mut self) {
self.audio_device.resume();
let mut event_pump = self.sdl_context.event_pump().unwrap();
'running: loop {
unsafe {
gl::ClearColor(0.3, 0.6, 0.3, 1.0);
gl::Clear(gl::COLOR_BUFFER_BIT);
}
let frame_begin = Instant::now();
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => break 'running,
| Event::KeyDown {
keycode: Some(Keycode::F3),
..
} => self.serialize().unwrap(),
| Event::KeyDown {
keycode: Some(Keycode::F4),
..
} => self.unserialize_newest().unwrap(),
_ => {
self.geodiff.on_input(event)
}
}
}
// The rest of the game loop goes here...
self.retro.run();
self.geodiff.on_frame();
self.window.gl_swap_window();
// 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(frame_begin.elapsed())
.map(std::thread::sleep);
}
}
*/
pub fn load_game(&mut self, rom: impl AsRef<Path>) {
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();
if let Some(device) = self.preferred_pad {
for port in 0..1 as u32 { // TODO: don't hardcode single controller
self.retro.set_controller_port_device(port, device);
}
}
// Store the path so we can use it to build save state filenames.
self.game_path = Some(path.clone().into());
}
fn send_audio_samples(&mut self) {
// not implemented
}
pub fn serialize(&self) -> Fallible<()> {
let data = self.retro.serialize()?;
if let Some(game_path) = self.game_path.borrow() {
let filename = build_savestate_filename(game_path);
match std::fs::File::create(filename.clone()) {
Ok(mut f) => {
f.write_all(&data)?;
println!("State written to {:?}.", filename.into_os_string().into_string());
Ok(())
},
Err(e) => Err(e.into())
}
} else {
Err(failure::err_msg("Game filename is not set, cannot determine serialized target filename"))
}
}
pub fn unserialize_newest(&mut self) -> Fallible<()> {
if let Some(game_path) = self.game_path.borrow()
{
return match get_newest_savestate(game_path) {
Some(path) => self.unserialize(path),
None => Err(failure::err_msg("No existing state found"))
};
}
Err(failure::err_msg("Game filename is not set, cannot determine serialized target filename"))
}
pub fn unserialize(&mut self, state: impl AsRef<Path>) -> 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 MyEmulator {
fn drop(&mut self) {
retro::wrapper::unset_handler();
}
}
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 buffer_len = self.video_buffer.len();
let data_len = data.len();
let expected_len = (width * height * pitch) as usize;
if buffer_len != expected_len {
//println!("expected length {} doesn't match destination buffer: {}", expected_len, buffer_len);
}
if data_len != expected_len {
//println!("expected length {} doesn't match data size: {}", expected_len, data_len);
}
if buffer_len == data_len {
self.video_buffer.copy_from_slice(data);
}
else if buffer_len > data_len {
self.video_buffer.as_mut_slice()[0..data_len].copy_from_slice(data);
}
else if buffer_len < data_len {
self.video_buffer.copy_from_slice(&data[0..buffer_len]);
}
/*
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) {
// not implemented
self.send_audio_samples()
}
fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize {
// not implemented
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.gamepads().nth(port.try_into().unwrap()){
Some(gamepad) => {
match device {
InputDeviceId::Joypad(button) => {
match button_map(&button) {
Some(gilrs_button) => gamepad.1.is_pressed(gilrs_button) as i16,
None => 0,
}
}
InputDeviceId::Analog(axis) => {
let gilrs_axis = axis_map(index, axis);
match gamepad.1.axis_data(gilrs_axis) {
Some(data) => data.value() as i16,
None => 0
}
},
_ => 0,
}
}
None => 0,
}
}
fn get_can_dupe(&mut self) -> Option<bool> { Some(true) }
fn get_system_directory(&mut self) -> Option<PathBuf> {
self.sys_path.clone()
}
fn set_pixel_format(&mut self, pix_fmt: retro::ffi::PixelFormat) -> bool {
// not implemented
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>) -> bool {
for v in variables {
eprintln!("{:?}", v);
}
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) -> bool {
self.set_geometry(av_info.geometry.clone());
self.av_info = av_info;
true
}
fn set_subsystem_info(&mut self, subsystem_info: Vec<SubsystemInfo2>) -> bool {
println!("subsystem info: {:?}", subsystem_info);
true
}
fn set_controller_info(&mut self, controller_info: Vec<ControllerDescription2>) -> 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;
}
}
true
}
fn set_input_descriptors(&mut self, descriptors: Vec<InputDescriptor2>) -> bool {
for id in descriptors {
println!("{:?}", id);
}
true
}
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;
*/
true
}
fn log_print(&mut self, level: retro::ffi::LogLevel, msg: &str) {
eprint!("[{:?}] {}", level, msg);
}
}
pub fn main() -> failure::Fallible<()> {
let opt: Opt = Opt::from_args();
let mut event_loop = EventLoop::new();
let mut multi_window = MultiWindow::new();
let mut emu = Rc::new(RefCell::new(MyEmulator::new(&opt.core, &opt.system)));
let mut emu_win = EmuWindow::new(&event_loop, &emu);
{
let mut emu = emu.borrow_mut();
emu.load_game(&opt.rom);
}
multi_window.add(emu_win);
multi_window.add(ConrodWindow::new(&event_loop, &emu));
//let geodiff = GeodiffUi::new(fb.glutin_breakout().context, config.window_size.width, config.window_size.height);
multi_window.run(&mut event_loop);
Ok(())
}
#[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>,
}
fn button_map(retro_button: &JoypadButton) -> Option<Button> {
match retro_button {
JoypadButton::B => Some(Button::South),
JoypadButton::Y => Some(Button::West),
JoypadButton::Select => Some(Button::Select),
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::East),
JoypadButton::X => Some(Button::North),
JoypadButton::L => Some(Button::LeftTrigger),
JoypadButton::R => Some(Button::RightTrigger),
JoypadButton::L2 => Some(Button::LeftTrigger2),
JoypadButton::R2 => Some(Button::RightTrigger2),
JoypadButton::L3 => Some(Button::LeftThumb),
JoypadButton::R3 => Some(Button::RightThumb),
}
}
fn axis_map(index: InputIndex, axis: AnalogAxis) -> Axis {
match (index, axis) {
(InputIndex::Left, AnalogAxis::X) => Axis::LeftStickX,
(InputIndex::Left, AnalogAxis::Y) => Axis::LeftStickY,
(InputIndex::Right, AnalogAxis::X) => Axis::RightStickX,
(InputIndex::Right, AnalogAxis::Y) => Axis::RightStickY,
}
}
fn build_savestate_filename(game_name: &PathBuf) -> PathBuf {
let mut index = 0;
let builder = |index: usize| game_name.as_path().with_extension(format!("state{}", index));
let mut candidate = builder(index);
while candidate.exists() {
index += 1;
candidate = builder(index);
}
candidate.into()
}
fn get_newest_savestate(game_name: &PathBuf) -> Option<PathBuf> {
let mut index = 0;
let builder = |index: usize| game_name.as_path().with_extension(format!("state{}", index));
let mut candidate = builder(index);
if !candidate.exists() {
return None;
}
while candidate.exists() {
index += 1;
candidate = builder(index);
}
// the current index is an unused index, the previous one exists.
Some(builder(index - 1).into())
}
/// A window being tracked by a `MultiWindow`. All tracked windows will be forwarded all events
/// received on the `MultiWindow`'s event loop.
trait TrackedWindow {
/// Handles one event from the event loop. Returns true if the window needs to be kept alive,
/// otherwise it will be closed. Window events should be checked to ensure that their ID is one
/// that the TrackedWindow is interested in.
fn handle_event(&mut self, event: &Event<()>) -> bool;
}
/// Manages multiple `TrackedWindow`s by forwarding events to them.
struct MultiWindow {
windows: Vec<Option<Box<dyn TrackedWindow>>>,
}
impl MultiWindow {
/// Creates a new `MultiWindow`.
pub fn new() -> Self {
MultiWindow {
windows: vec![],
}
}
/// Adds a new `TrackedWindow` to the `MultiWindow`.
pub fn add(&mut self, window: Box<dyn TrackedWindow>) {
self.windows.push(Some(window))
}
/// Runs the event loop until all `TrackedWindow`s are closed.
pub fn run(&mut self, event_loop: &mut EventLoop<()>) {
if !self.windows.is_empty() {
event_loop.run_return(|event, _, flow| {
*flow = ControlFlow::Poll;
for option in &mut self.windows {
if let Some(window) = option.as_mut() {
if !window.handle_event(&event) {
option.take();
}
}
}
self.windows.retain(Option::is_some);
if self.windows.is_empty() {
*flow = ControlFlow::Exit;
}
});
}
}
}
struct EmuWindow {
pub emu: Rc<RefCell<Pin<Box<MyEmulator>>>>,
pub breakout: GlutinBreakout,
next_frame_time: Instant,
frame_duration: Duration,
}
impl EmuWindow {
pub fn new(event_loop: &EventLoop<()>, emu: &Rc<RefCell<Pin<Box<MyEmulator>>>>) -> Box<Self> {
let emu_inner = emu.borrow_mut();
let config = config! {
window_title: emu_inner.title.clone(),
window_size: LogicalSize::new(emu_inner.av_info.geometry.base_width.into(), emu_inner.av_info.geometry.base_height.into())
};
let breakout = get_fancy(config, &event_loop).glutin_breakout();
Box::from(EmuWindow {
emu: Rc::clone(emu),
breakout,
next_frame_time: Instant::now(),
frame_duration: Duration::from_secs_f64(1.0/60.0),
})
}
fn window(&self) -> &Window {
self.breakout.context.window()
}
pub fn matches_id(&self, id: WindowId) -> bool {
id == self.window().id()
}
/// Updates the window's buffer. Should only be done inside of RedrawRequested events; outside
/// of them, use `request_redraw` instead.
fn redraw(&mut self) {
{
let mut emu = self.emu.borrow_mut();
self.breakout.fb.update_buffer(emu.video_buffer.as_slice());
}
self.breakout.context.swap_buffers().unwrap();
}
/// Requests a redraw event for this window.
fn request_redraw(&self) {
self.window().request_redraw();
}
}
impl TrackedWindow for EmuWindow {
fn handle_event(&mut self, event: &Event<()>) -> bool {
match *event {
Event::WindowEvent {
window_id: id,
event: WindowEvent::CloseRequested,
..
} if self.matches_id(id) => {
return false; // closed
}
Event::WindowEvent {
window_id: id,
event: WindowEvent::KeyboardInput {
input: KeyboardInput {
virtual_keycode: Some(VirtualKeyCode::Escape),
state: ElementState::Pressed,
..
},
..
},
..
} if self.matches_id(id) => {
return false; // close on esc
}
Event::RedrawRequested(id) if self.matches_id(id) => {
unsafe { self.breakout.make_current().unwrap(); }
self.redraw();
}
_ => {
}
}
let now = Instant::now();
if now > self.next_frame_time {
self.next_frame_time = now.add(self.frame_duration);
{
let mut emu = self.emu.borrow_mut();
emu.run_frame();
}
self.request_redraw();
}
true
}
}
widget_ids!(struct Ids { text, circle });
struct ConrodWindow {
ui: conrod_core::Ui,
renderer: conrod_glium::Renderer,
display: glium::Display,
image_map: conrod_core::image::Map<glium::texture::Texture2d>,
window_id: WindowId,
ids: Ids,
pub emu: Rc<RefCell<Pin<Box<MyEmulator>>>>,
}
impl ConrodWindow {
pub fn new(event_loop: &EventLoop<()>, emu: &Rc<RefCell<Pin<Box<MyEmulator>>>>) -> Box<Self> {
let config = config! {
window_title: "rustro debugger".to_string(),
window_size: LogicalSize::new(1024.0, 768.0)
};
let mut ui = conrod_core::UiBuilder::new([config.window_size.width, config.window_size.height]).build();
let ids = Ids::new(ui.widget_id_generator());
let breakout = get_fancy(config, &event_loop).glutin_breakout();
let image_map = conrod_core::image::Map::<glium::texture::Texture2d>::new();
let window_id = breakout.context.window().id();
let display = glium::Display::from_gl_window(breakout.context).unwrap();
//let display = glium::Display::new(breakout.context.window(), breakout.context, event_loop).unwrap();
let mut renderer = conrod_glium::Renderer::new(&display).unwrap();
Box::from(ConrodWindow {
ui,
renderer,
display,
image_map,
window_id,
ids,
emu: Rc::clone(emu),
})
}
pub fn matches_id(&self, id: WindowId) -> bool {
id == self.window_id
}
/// Updates the window's buffer. Should only be done inside of RedrawRequested events; outside
/// of them, use `request_redraw` instead.
fn redraw(&mut self) {
//if let Some(primitives) = ui.draw_if_changed() {
let primitives = self.ui.draw();
self.renderer.fill(&self.display, primitives, &self.image_map);
let mut target = self.display.draw();
target.clear_color(0.0, 1.0, 0.0, 1.0);
self.renderer.draw(&self.display, &mut target, &self.image_map).unwrap();
target.finish().unwrap();
//}
}
/// Requests a redraw event for this window.
fn request_redraw(&self) {
self.display.gl_window().window().request_redraw()
}
}
impl TrackedWindow for ConrodWindow {
fn handle_event(&mut self, event: &Event<()>) -> bool {
match *event {
Event::WindowEvent {
window_id: id,
event: WindowEvent::CloseRequested,
..
} if self.matches_id(id) => {
return false; // closed
}
Event::WindowEvent {
window_id: id,
event: WindowEvent::KeyboardInput {
input: KeyboardInput {
virtual_keycode: Some(VirtualKeyCode::Escape),
state: ElementState::Pressed,
..
},
..
},
..
} if self.matches_id(id) => {
return false; // close on esc
}
Event::MainEventsCleared => {
let mut emu = self.emu.borrow_mut();
let mut ui = self.ui.set_widgets();
widget::Text::new(format!("frame {}", emu.frame).as_str())
.middle_of(ui.window)
.color(conrod_core::color::WHITE)
.font_size(32)
.set(self.ids.text, &mut ui);
widget::Circle::fill(32.0)
.x_y(emu.frame as f64, 64.0)
.color(conrod_core::color::BLUE)
.set(self.ids.circle, &mut ui);
self.display.gl_window().window().request_redraw();
}
Event::RedrawRequested(id) if self.matches_id(id) => {
self.redraw();
}
_ => {
}
}
true
}
}