From 203f575914be9a58cf81cd7636d2cd531659d7f4 Mon Sep 17 00:00:00 2001 From: lifning <> Date: Wed, 24 Nov 2021 21:38:34 -0800 Subject: [PATCH] add input and frameskip --- Cargo.lock | 27 ++++- Cargo.toml | 2 +- src/main.rs | 326 +++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 309 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01bbd68..effd156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 53f7adb..c011d3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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: diff --git a/src/main.rs b/src/main.rs index e90f6a9..46f7914 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, } -struct RetroFrameEncoder { +struct AnsiVideoComponent { terminal_width: u32, terminal_height: u32, color_mode: ColorMode, + screen: AlternateScreen>, + 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 { + 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, + button_map: HashMap, + axis_maps: [HashMap; 2], + button_state: HashMap, + 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 { + 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 { + 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 { + 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> { 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> { 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())?;