170 lines
6.0 KiB
Rust
170 lines
6.0 KiB
Rust
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<RawTerminal<Stdout>>,
|
|
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<String> {
|
|
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(())
|
|
}
|
|
}
|