flywheel/actor/
renderer.rs1use super::messages::RenderCommand;
8use crate::buffer::diff::{render_diff, render_full, DiffState};
9use crate::buffer::Buffer;
10use crate::layout::Rect;
11use crossbeam_channel::Receiver;
12use std::io::{self, Stdout, Write};
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15use std::thread::{self, JoinHandle};
16use std::time::{Duration, Instant};
17
18pub struct RendererActor {
20 handle: Option<JoinHandle<()>>,
22 shutdown: Arc<AtomicBool>,
24}
25
26#[derive(Debug, Clone, Default)]
28pub struct RenderStats {
29 pub frames: u64,
31 #[allow(dead_code)]
33 pub cells_changed: u64,
34 pub bytes_written: u64,
36 pub avg_render_us: u64,
38 pub last_render_us: u64,
40}
41
42struct Renderer {
44 current: Buffer,
46 next: Buffer,
48 diff_state: DiffState,
50 output: Vec<u8>,
52 stdout: Stdout,
54 stats: RenderStats,
56 dirty_rects: Vec<Rect>,
58 needs_full_redraw: bool,
60 cursor_x: Option<u16>,
62 cursor_y: u16,
63}
64
65impl Renderer {
66 fn new(width: u16, height: u16) -> Self {
68 let current = Buffer::new(width, height);
69 let next = Buffer::new(width, height);
70
71 Self {
72 current,
73 next,
74 diff_state: DiffState::new(),
75 output: Vec::with_capacity(65536),
76 stdout: io::stdout(),
77 stats: RenderStats::default(),
78 dirty_rects: Vec::new(),
79 needs_full_redraw: true,
80 cursor_x: None,
81 cursor_y: 0,
82 }
83 }
84
85 #[allow(dead_code)]
87 pub const fn buffer_mut(&mut self) -> &mut Buffer {
88 &mut self.next
89 }
90
91 const fn mark_full_dirty(&mut self) {
93 self.needs_full_redraw = true;
94 }
95
96 #[allow(dead_code)]
98 fn mark_dirty(&mut self, rect: Rect) {
99 self.dirty_rects.push(rect);
100 }
101
102 fn render(&mut self) -> io::Result<()> {
104 let start = Instant::now();
105 self.output.clear();
106
107 if self.needs_full_redraw {
108 render_full(&self.next, &mut self.output);
110 self.needs_full_redraw = false;
111 self.diff_state.reset();
112 } else {
113 let _result = render_diff(
115 &self.current,
116 &self.next,
117 &self.dirty_rects,
118 &mut self.output,
119 &mut self.diff_state,
120 );
121 }
122
123 self.dirty_rects.clear();
124
125 if let Some(x) = self.cursor_x {
127 let _ = write!(
129 &mut self.output,
130 "\x1b[{};{}H\x1b[?25h",
131 self.cursor_y + 1,
132 x + 1
133 );
134 } else {
135 self.output.extend_from_slice(b"\x1b[?25l");
137 }
138
139 if !self.output.is_empty() {
141 self.stdout.write_all(&self.output)?;
142 self.stdout.flush()?;
143 }
144
145 self.current.copy_from(&self.next);
147
148 let elapsed = start.elapsed();
150 self.stats.frames += 1;
151 self.stats.bytes_written += self.output.len() as u64;
152 self.stats.last_render_us = u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX);
153
154 if self.stats.avg_render_us == 0 {
156 self.stats.avg_render_us = self.stats.last_render_us;
157 } else {
158 self.stats.avg_render_us =
159 (self.stats.avg_render_us * 15 + self.stats.last_render_us) / 16;
160 }
161
162 Ok(())
163 }
164
165 fn write_raw(&mut self, bytes: &[u8]) -> io::Result<()> {
171 self.stdout.write_all(bytes)?;
172 self.stdout.flush()?;
173 self.stats.bytes_written += bytes.len() as u64;
174
175 self.needs_full_redraw = true;
182 self.diff_state.reset();
183
184 Ok(())
185 }
186
187 fn resize(&mut self, width: u16, height: u16) {
189 self.current.resize(width, height);
190 self.next.resize(width, height);
191 self.mark_full_dirty();
192 }
193
194 const fn set_cursor(&mut self, x: Option<u16>, y: u16) {
196 self.cursor_x = x;
197 self.cursor_y = y;
198 }
199}
200
201impl RendererActor {
202 #[allow(clippy::missing_panics_doc)]
214 pub fn spawn(receiver: Receiver<RenderCommand>, width: u16, height: u16) -> Self {
215 let shutdown = Arc::new(AtomicBool::new(false));
216 let shutdown_clone = shutdown.clone();
217
218 let handle = thread::Builder::new()
219 .name("flywheel-render".to_string())
220 .spawn(move || {
221 if let Err(e) = Self::run_loop(&receiver, &shutdown_clone, width, height) {
222 eprintln!("Render thread error: {e}");
223 }
224 })
225 .expect("Failed to spawn render thread");
226
227 Self {
228 handle: Some(handle),
229 shutdown,
230 }
231 }
232
233 pub fn shutdown(&self) {
235 self.shutdown.store(true, Ordering::Relaxed);
236 }
237
238 pub fn join(mut self) {
240 if let Some(handle) = self.handle.take() {
241 let _ = handle.join();
242 }
243 }
244
245 fn run_loop(
247 receiver: &Receiver<RenderCommand>,
248 shutdown: &Arc<AtomicBool>,
249 width: u16,
250 height: u16,
251 ) -> io::Result<()> {
252 let mut renderer = Renderer::new(width, height);
253
254 loop {
255 if shutdown.load(Ordering::Relaxed) {
257 break;
258 }
259
260 if let Ok(command) = receiver.recv_timeout(Duration::from_millis(16)) {
262 match command {
263 RenderCommand::FullRedraw(buffer) => {
264 renderer.next = *buffer;
265 renderer.mark_full_dirty();
266 renderer.render()?;
267 }
268 RenderCommand::Update(buffer) => {
269 renderer.next = *buffer;
270 renderer.render()?;
271 }
272 RenderCommand::Resize { width, height } => {
273 renderer.resize(width, height);
274 }
275 RenderCommand::SetCursor { x, y } => {
276 renderer.set_cursor(x, y);
277 }
278 RenderCommand::RawOutput { bytes } => {
279 renderer.write_raw(&bytes)?;
280 }
281 RenderCommand::Shutdown => {
282 break;
283 }
284 }
285 } else {
286 }
289 }
290
291 Ok(())
292 }
293}