use std::io::{Stdout, Write}; use std::path::Path; use std::time::Instant; use ferretro_components::base::ControlFlow; use ferretro_components::prelude::*; use termion::color::DetectColors; use termion::raw::{IntoRawMode, RawTerminal}; use termion::screen::AlternateScreen; use termion::terminal_size; use anime_telnet::encoding::{AnsiEncoder, ProcessorPipeline}; use anime_telnet::metadata::ColorMode; use fast_image_resize::FilterType; use image::RgbaImage; use sdl2::pixels::PixelFormatEnum; use sdl2::surface::Surface; pub 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 AnsiEncoder for AnsiVideoComponent { fn needs_width(&self) -> u32 { self.terminal_width - 1 } fn needs_height(&self) -> u32 { (self.terminal_height - 1) * 2 // half-blocks? } fn needs_color(&self) -> ColorMode { self.color_mode } } impl Default for AnsiVideoComponent { fn default() -> Self { 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 { terminal_width: width, terminal_height: height, color_mode, screen, fps: 60.0, framerate_sampling_start: Instant::now(), frame_count: 0, frame_skip: 0, } } } impl Drop for AnsiVideoComponent { fn drop(&mut self) { write!(self.screen, "{}", termion::cursor::Show).unwrap(); } } impl RetroCallbacks for AnsiVideoComponent { fn video_refresh(&mut self, frame: &VideoFrame) { match frame { VideoFrame::XRGB1555 { width, height, .. } | VideoFrame::RGB565 { width, height, .. } | VideoFrame::XRGB8888 { width, height, .. } => { let (bytes, pitch) = frame.data_pitch_as_bytes().unwrap(); let pitch = pitch as u32; let format = match frame.pixel_format().unwrap() { PixelFormat::ARGB1555 => sdl2::pixels::PixelFormatEnum::RGB555, 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 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, termion::color::Fg(termion::color::Black)).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 { 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(()) } }