add input and frameskip

This commit is contained in:
lifning 2021-11-24 21:38:34 -08:00
parent 9ecdcf74eb
commit 203f575914
3 changed files with 309 additions and 46 deletions

27
Cargo.lock generated
View File

@ -350,7 +350,7 @@ dependencies = [
"image 0.23.14",
"sdl2",
"structopt",
"terminal_size",
"termion",
"tokio",
]
@ -746,6 +746,12 @@ dependencies = [
"syn",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "object"
version = "0.24.0"
@ -907,6 +913,15 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_termios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall",
]
[[package]]
name = "regex"
version = "0.2.11"
@ -1114,13 +1129,15 @@ dependencies = [
]
[[package]]
name = "terminal_size"
version = "0.1.17"
name = "termion"
version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [
"libc",
"winapi",
"numtoa",
"redox_syscall",
"redox_termios",
]
[[package]]

View File

@ -17,7 +17,7 @@ features = ["sync"]
[dependencies]
structopt = "0.3"
terminal_size = "0.1"
termion = "1"
sdl2 = "0.35"
# must match the versions in anime_telnet:

View File

@ -1,19 +1,30 @@
use std::path::PathBuf;
use std::collections::HashMap;
use std::io::{Stdout, Write};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant};
use structopt::StructOpt;
use ferretro_components::base::ControlFlow;
use ferretro_components::prelude::*;
use ferretro_components::provided::stdlib::*;
use termion::color::DetectColors;
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::{IntoRawMode, RawTerminal};
use termion::screen::AlternateScreen;
use termion::terminal_size;
use anime_telnet::encoding::Encoder as AnsiArtEncoder;
use anime_telnet::encoding::ProcessorPipeline;
use anime_telnet::metadata::ColorMode;
use fast_image_resize::FilterType;
use image::RgbaImage;
use sdl2::pixels::PixelFormatEnum;
use sdl2::surface::Surface;
use ferretro_components::base::ControlFlow;
#[derive(StructOpt)]
struct Opt {
@ -28,32 +39,24 @@ struct Opt {
system: Option<PathBuf>,
}
struct RetroFrameEncoder {
struct AnsiVideoComponent {
terminal_width: u32,
terminal_height: u32,
color_mode: ColorMode,
screen: AlternateScreen<RawTerminal<Stdout>>,
fps: f32,
framerate_sampling_start: Instant,
frame_count: usize,
frame_skip: usize,
}
impl Default for RetroFrameEncoder {
fn default() -> Self {
use terminal_size::*;
let (Width(w), Height(h)) = terminal_size()
.unwrap_or((Width(80), Height(24)));
RetroFrameEncoder {
terminal_width: w as u32,
terminal_height: h as u32,
color_mode: ColorMode::EightBit,
}
}
}
impl AnsiArtEncoder for RetroFrameEncoder {
impl AnsiArtEncoder for AnsiVideoComponent {
fn needs_width(&self) -> u32 {
self.terminal_width
self.terminal_width - 1
}
fn needs_height(&self) -> u32 {
self.terminal_height * 2 // half-blocks?
(self.terminal_height - 1) * 2 // half-blocks?
}
fn needs_color(&self) -> ColorMode {
@ -61,23 +64,27 @@ impl AnsiArtEncoder for RetroFrameEncoder {
}
}
struct AnsiVideoComponent {
processor: ProcessorPipeline,
encoder: RetroFrameEncoder,
}
impl Default for AnsiVideoComponent {
fn default() -> Self {
let encoder = RetroFrameEncoder::default();
let processor = ProcessorPipeline {
filter: FilterType::Hamming,
width: encoder.needs_width(),
height: encoder.needs_height(),
color_modes: Some(encoder.needs_color()).into_iter().collect(),
};
let output = std::io::stdout().into_raw_mode().unwrap();
let mut screen = AlternateScreen::from(output);
write!(screen, "{}", termion::cursor::Hide).unwrap();
let (w, h) = terminal_size().unwrap_or((80, 24));
let (width, height) = (w as u32, h as u32);
let colors = screen.available_colors().unwrap_or(16);
let color_mode = if colors > 16 { ColorMode::True } else { ColorMode::EightBit };
AnsiVideoComponent {
processor,
encoder,
terminal_width: width,
terminal_height: height,
color_mode,
screen,
fps: 60.0,
framerate_sampling_start: Instant::now(),
frame_count: 0,
frame_skip: 0,
}
}
}
@ -88,8 +95,6 @@ impl RetroCallbacks for AnsiVideoComponent {
VideoFrame::XRGB1555 { width, height, .. }
| VideoFrame::RGB565 { width, height, .. }
| VideoFrame::XRGB8888 { width, height, .. } => {
// dirty, but must be &mut for SDL API.
// safe as long as we don't leak the Surface we construct here.
let (bytes, pitch) = frame.data_pitch_as_bytes().unwrap();
let pitch = pitch as u32;
let format = match frame.pixel_format().unwrap() {
@ -97,27 +102,269 @@ impl RetroCallbacks for AnsiVideoComponent {
PixelFormat::ARGB8888 => sdl2::pixels::PixelFormatEnum::ARGB8888,
PixelFormat::RGB565 => sdl2::pixels::PixelFormatEnum::RGB565,
};
// dirty, but must be &mut for SDL API.
// safety: we don't actually mutate or leak the Surface we construct here.
let data = unsafe {
core::slice::from_raw_parts_mut(bytes.as_ptr() as *mut u8, bytes.len())
};
// has the screen size changed?
let (w, h) = terminal_size().unwrap_or((80, 24));
let (w, h) = (w as u32, h as u32);
let force_redraw = if self.terminal_width != w || self.terminal_height != h {
self.terminal_width = w as u32;
self.terminal_height = h as u32;
self.frame_skip = 0;
write!(self.screen, "{}", termion::clear::All).unwrap();
true
} else {
false
};
if !force_redraw {
if self.frame_skip != 0 && self.frame_count % self.frame_skip != 0 {
return;
}
}
if let Ok(surf) = Surface::from_data(data, *width, *height, pitch, format) {
let rgba_raw = surf.into_canvas().unwrap().read_pixels(None, PixelFormatEnum::ABGR8888).unwrap();
let rgba_img = RgbaImage::from_raw(*width, *height, rgba_raw).unwrap();
let processed = self.processor.process(&rgba_img).into_iter().next().unwrap().1;
println!("\x1B[0m\x1B[0J{}", self.encoder.encode_frame(&processed));
let processor = ProcessorPipeline {
filter: FilterType::Hamming,
width: self.needs_width(),
height: self.needs_height(),
color_modes: Some(self.needs_color()).into_iter().collect(),
};
let processed = processor.process(&rgba_img).into_iter().next().unwrap().1;
write!(self.screen, "{}", termion::cursor::Goto(1, 1)).unwrap();
for line in self.encode_frame(&processed).lines() {
write!(self.screen, "{}\r\n", line).unwrap();
}
write!(self.screen, "\x1B[0m").unwrap();
self.screen.flush().unwrap();
} else if force_redraw {
// TODO: draw last copy
}
}
_ => {}
}
}
fn get_variable(&mut self, key: &str) -> Option<String> {
match key {
"parallel-n64-gfxplugin" => Some("angrylion".to_string()),
_ => None,
}
}
}
impl RetroComponent for AnsiVideoComponent {}
impl RetroComponent for AnsiVideoComponent {
fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow {
self.frame_count += 1;
if self.frame_skip < 10 && self.frame_count > 10 {
let now = Instant::now();
let period = now.duration_since(self.framerate_sampling_start).as_secs_f32();
let actual_fps = self.frame_count as f32 / period;
if actual_fps < self.fps * 2.0 / 3.0 {
self.frame_skip += 1;
self.frame_count = 0;
self.framerate_sampling_start = now;
}
}
ControlFlow::Continue
}
fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> ferretro_components::base::Result<()> {
self.fps = retro.get_system_av_info().timing.fps as f32;
Ok(())
}
}
struct TermiosInputComponent {
//reader: AsyncReader,
receiver: Receiver<(Instant, Key)>,
want_quit: bool,
preferred_pad: Option<u32>,
button_map: HashMap<Key, InputDeviceId>,
axis_maps: [HashMap<Key, (i16, i16)>; 2],
button_state: HashMap<InputDeviceId, (Instant, i16)>,
axis_state: [(Instant, [i16; 2]); 2],
}
impl Default for TermiosInputComponent {
fn default() -> Self {
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for k in std::io::stdin().keys().map(|kr| kr.unwrap()) {
sender.send((Instant::now(), k)).unwrap();
if k == Key::Esc {
break
}
}
});
TermiosInputComponent {
//reader: termion::async_stdin(),
receiver,
want_quit: false,
preferred_pad: None,
button_map: [
(Key::Up, InputDeviceId::Joypad(JoypadButton::Up)),
(Key::Down, InputDeviceId::Joypad(JoypadButton::Down)),
(Key::Left, InputDeviceId::Joypad(JoypadButton::Left)),
(Key::Right, InputDeviceId::Joypad(JoypadButton::Right)),
(Key::Char('x'), InputDeviceId::Joypad(JoypadButton::A)),
(Key::Char('z'), InputDeviceId::Joypad(JoypadButton::B)),
(Key::Char('s'), InputDeviceId::Joypad(JoypadButton::X)),
(Key::Char('a'), InputDeviceId::Joypad(JoypadButton::Y)),
(Key::Ctrl('x'), InputDeviceId::Joypad(JoypadButton::A)),
(Key::Ctrl('z'), InputDeviceId::Joypad(JoypadButton::B)),
(Key::Ctrl('s'), InputDeviceId::Joypad(JoypadButton::X)),
(Key::Ctrl('a'), InputDeviceId::Joypad(JoypadButton::Y)),
(Key::Alt('x'), InputDeviceId::Joypad(JoypadButton::A)),
(Key::Alt('z'), InputDeviceId::Joypad(JoypadButton::B)),
(Key::Alt('s'), InputDeviceId::Joypad(JoypadButton::X)),
(Key::Alt('a'), InputDeviceId::Joypad(JoypadButton::Y)),
(Key::Char('q'), InputDeviceId::Joypad(JoypadButton::L)),
(Key::Char('w'), InputDeviceId::Joypad(JoypadButton::R)),
(Key::Ctrl('q'), InputDeviceId::Joypad(JoypadButton::L2)),
(Key::Ctrl('w'), InputDeviceId::Joypad(JoypadButton::R2)),
(Key::Alt('q'), InputDeviceId::Joypad(JoypadButton::L3)),
(Key::Alt('w'), InputDeviceId::Joypad(JoypadButton::R3)),
(Key::Char('\n'), InputDeviceId::Joypad(JoypadButton::Start)),
(Key::Backspace, InputDeviceId::Joypad(JoypadButton::Select)),
(Key::Char('\t'), InputDeviceId::Joypad(JoypadButton::Select)),
].into_iter().collect(),
axis_maps: [
[
(Key::Char('1'), (i16::MIN, i16::MAX)),
(Key::Char('2'), (0, i16::MAX)),
(Key::Char('3'), (i16::MAX, i16::MAX)),
(Key::Char('4'), (i16::MIN, 0)),
(Key::Char('5'), (0, 0)),
(Key::Char('6'), (i16::MAX, 0)),
(Key::Char('7'), (i16::MIN + 1, i16::MIN + 1)), // ???
(Key::Char('8'), (0, i16::MIN)),
(Key::Char('9'), (i16::MAX, i16::MIN)),
].into_iter().collect(),
[
(Key::Char('r'), (0, i16::MIN)),
(Key::Char('d'), (0, i16::MAX)),
(Key::Char('e'), (i16::MIN, 0)),
(Key::Char('f'), (i16::MAX, 0)),
].into_iter().collect(),
],
button_state: Default::default(),
axis_state: [(Instant::now(), [0, 0]); 2],
}
}
}
impl RetroCallbacks for TermiosInputComponent {
fn input_poll(&mut self) {
while let Ok((now, k)) = self.receiver.try_recv() {
if k == Key::Esc {
self.want_quit = true;
}
if let Some(mapping) = self.button_map.get(&k) {
self.button_state.insert(mapping.to_owned(), (now, 1));
}
for (axis_map, axis_state) in self.axis_maps.iter().zip(self.axis_state.iter_mut()) {
let (mut sum_x, mut sum_y) = (0, 0);
let mut count = 0;
if let Some((x, y)) = axis_map.get(&k) {
sum_x += *x as i32;
sum_y += *y as i32;
count += 1;
}
if count != 0 {
let average_x = (sum_x / count) as i16;
let average_y = (sum_y / count) as i16;
*axis_state = (now, [average_x, average_y]);
}
}
}
}
fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 {
if port != 0 {
return 0;
}
if let Some((inst, val)) = self.button_state.get(&device) {
// TODO: consult kbdrate.c (and X11?) for durations (can't detect key-up/held, only repeat)
if Instant::now().duration_since(*inst) < Duration::from_millis(300) {
return *val;
}
}
match device {
InputDeviceId::Analog(axis_id) => {
let (inst, axes) = self.axis_state[index as u32 as usize];
let since = Instant::now().duration_since(inst);
let ratio = if since < Duration::from_millis(100) {
1.0
} else if since < Duration::from_millis(300) {
(0.3 - since.as_secs_f32()) * 5.0
} else {
0.0
};
(axes[axis_id as u32 as usize] as f32 * ratio) as i16
}
// TODO: mouse?
_ => 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: &[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 TermiosInputComponent {
fn post_run(&mut self, _: &mut LibretroWrapper) -> ControlFlow {
if self.want_quit {
ControlFlow::Break
} else {
ControlFlow::Continue
}
}
fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> ferretro_components::base::Result<()> {
if let Some(device) = self.preferred_pad {
retro.set_controller_port_device(0, device);
}
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let opt: Opt = Opt::from_args();
let mut emu = RetroComponentBase::new(&opt.core);
emu.register_component(AnsiVideoComponent::default())?;
emu.register_component(TermiosInputComponent::default())?;
emu.register_component(StatefulInputComponent::default())?;
emu.register_component(PathBufComponent {
sys_path: opt.system.clone(),
@ -128,7 +375,6 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut vars_comp = VariableStoreComponent::default();
vars_comp.insert("mgba_skip_bios", "ON");
vars_comp.insert("mgba_sgb_borders", "OFF");
vars_comp.insert("parallel-n64-gfxplugin", "angrylion");
emu.register_component(vars_comp)?;
emu.register_component(SleepFramerateLimitComponent::default())?;