use super::messages::RenderCommand;
use crate::buffer::diff::{render_diff, render_full, DiffState};
use crate::buffer::Buffer;
use crate::layout::Rect;
use crossbeam_channel::Receiver;
use std::io::{self, Stdout, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
pub struct RendererActor {
handle: Option<JoinHandle<()>>,
shutdown: Arc<AtomicBool>,
}
#[derive(Debug, Clone, Default)]
pub struct RenderStats {
pub frames: u64,
#[allow(dead_code)]
pub cells_changed: u64,
pub bytes_written: u64,
pub avg_render_us: u64,
pub last_render_us: u64,
}
struct Renderer {
current: Buffer,
next: Buffer,
diff_state: DiffState,
output: Vec<u8>,
stdout: Stdout,
stats: RenderStats,
dirty_rects: Vec<Rect>,
needs_full_redraw: bool,
cursor_x: Option<u16>,
cursor_y: u16,
}
impl Renderer {
fn new(width: u16, height: u16) -> Self {
let current = Buffer::new(width, height);
let next = Buffer::new(width, height);
Self {
current,
next,
diff_state: DiffState::new(),
output: Vec::with_capacity(65536),
stdout: io::stdout(),
stats: RenderStats::default(),
dirty_rects: Vec::new(),
needs_full_redraw: true,
cursor_x: None,
cursor_y: 0,
}
}
#[allow(dead_code)]
pub const fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.next
}
const fn mark_full_dirty(&mut self) {
self.needs_full_redraw = true;
}
#[allow(dead_code)]
fn mark_dirty(&mut self, rect: Rect) {
self.dirty_rects.push(rect);
}
fn render(&mut self) -> io::Result<()> {
let start = Instant::now();
self.output.clear();
if self.needs_full_redraw {
render_full(&self.next, &mut self.output);
self.needs_full_redraw = false;
self.diff_state.reset();
} else {
let _result = render_diff(
&self.current,
&self.next,
&self.dirty_rects,
&mut self.output,
&mut self.diff_state,
);
}
self.dirty_rects.clear();
if let Some(x) = self.cursor_x {
let _ = write!(
&mut self.output,
"\x1b[{};{}H\x1b[?25h",
self.cursor_y + 1,
x + 1
);
} else {
self.output.extend_from_slice(b"\x1b[?25l");
}
if !self.output.is_empty() {
self.stdout.write_all(&self.output)?;
self.stdout.flush()?;
}
self.current.copy_from(&self.next);
let elapsed = start.elapsed();
self.stats.frames += 1;
self.stats.bytes_written += self.output.len() as u64;
self.stats.last_render_us = u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX);
if self.stats.avg_render_us == 0 {
self.stats.avg_render_us = self.stats.last_render_us;
} else {
self.stats.avg_render_us =
(self.stats.avg_render_us * 15 + self.stats.last_render_us) / 16;
}
Ok(())
}
fn write_raw(&mut self, bytes: &[u8]) -> io::Result<()> {
self.stdout.write_all(bytes)?;
self.stdout.flush()?;
self.stats.bytes_written += bytes.len() as u64;
self.needs_full_redraw = true;
self.diff_state.reset();
Ok(())
}
fn resize(&mut self, width: u16, height: u16) {
self.current.resize(width, height);
self.next.resize(width, height);
self.mark_full_dirty();
}
const fn set_cursor(&mut self, x: Option<u16>, y: u16) {
self.cursor_x = x;
self.cursor_y = y;
}
}
impl RendererActor {
#[allow(clippy::missing_panics_doc)]
pub fn spawn(receiver: Receiver<RenderCommand>, width: u16, height: u16) -> Self {
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
let handle = thread::Builder::new()
.name("flywheel-render".to_string())
.spawn(move || {
if let Err(e) = Self::run_loop(&receiver, &shutdown_clone, width, height) {
eprintln!("Render thread error: {e}");
}
})
.expect("Failed to spawn render thread");
Self {
handle: Some(handle),
shutdown,
}
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Relaxed);
}
pub fn join(mut self) {
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
fn run_loop(
receiver: &Receiver<RenderCommand>,
shutdown: &Arc<AtomicBool>,
width: u16,
height: u16,
) -> io::Result<()> {
let mut renderer = Renderer::new(width, height);
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
if let Ok(command) = receiver.recv_timeout(Duration::from_millis(16)) {
match command {
RenderCommand::FullRedraw(buffer) => {
renderer.next = *buffer;
renderer.mark_full_dirty();
renderer.render()?;
}
RenderCommand::Update(buffer) => {
renderer.next = *buffer;
renderer.render()?;
}
RenderCommand::Resize { width, height } => {
renderer.resize(width, height);
}
RenderCommand::SetCursor { x, y } => {
renderer.set_cursor(x, y);
}
RenderCommand::RawOutput { bytes } => {
renderer.write_raw(&bytes)?;
}
RenderCommand::Shutdown => {
break;
}
}
} else {
}
}
Ok(())
}
}