-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: optimize ANSI escape code generation and rendering logic #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| use std::fmt::Write as FmtWrite; | ||
| use std::io::{self, Write}; | ||
|
|
||
| use crate::{Attr, Color}; | ||
|
|
@@ -254,13 +255,16 @@ impl Framebuffer { | |
| /// * `x_offset`: The x-coordinate offset to start combining. | ||
| /// * `y_offset`: The y-coordinate offset to start combining. | ||
| pub fn combine(&mut self, other: &Framebuffer, x_offset: usize, y_offset: usize) { | ||
| for y in 0..other.height { | ||
| for x in 0..other.width { | ||
| if x + x_offset < self.width && y + y_offset < self.height { | ||
| let idx = (y + y_offset) * self.width + (x + x_offset); | ||
| self.buffer[idx] = other.buffer[y * other.width + x]; | ||
| } | ||
| } | ||
| let max_y = other.height.min(self.height.saturating_sub(y_offset)); | ||
| let max_x = other.width.min(self.width.saturating_sub(x_offset)); | ||
| if max_x == 0 || max_y == 0 { | ||
| return; | ||
| } | ||
| for y in 0..max_y { | ||
| let dst_start = (y + y_offset) * self.width + x_offset; | ||
| let src_start = y * other.width; | ||
| self.buffer[dst_start..dst_start + max_x] | ||
| .copy_from_slice(&other.buffer[src_start..src_start + max_x]); | ||
|
Comment on lines
+258
to
+267
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -295,50 +299,44 @@ impl Framebuffer { | |
| let mut prev_attrs = Attr::NORMAL; | ||
| let mut prev_fg: Color = Color::default(); | ||
| let mut prev_bg: Color = Color::default(); | ||
| let mut has_changes = false; | ||
|
|
||
| stdout_lock.write_all("\x1B[0m".as_bytes())?; // Reset all attributes | ||
| stdout_lock.flush()?; | ||
| let mut chunk = String::with_capacity(CHUNK_SIZE); | ||
|
|
||
| // Collect all changed cells first | ||
| let mut changes = Vec::new(); | ||
| for y in 0..self.height { | ||
| for x in 0..self.width { | ||
| let idx = y * self.width + x; | ||
| let front = &self.buffer[idx]; | ||
| let back = &back_fb.buffer[idx]; | ||
|
|
||
| if front != back { | ||
| changes.push((x, y, idx, back)); | ||
| let back = back_fb.buffer[idx]; // Cell is Copy; no heap allocation | ||
|
|
||
| if self.buffer[idx] != back { | ||
| if !has_changes { | ||
| chunk.push_str("\x1B[0m"); // Reset all attributes before first change | ||
| has_changes = true; | ||
| } | ||
| write!(chunk, "\x1B[{};{}H", y + 1, x + 1).unwrap(); // Move to the target coordinates | ||
| if prev_attrs != back.attrs { | ||
| prev_attrs = back.attrs; | ||
| back.attrs.write_ansi(&mut chunk); | ||
| } | ||
| if prev_fg != back.fg { | ||
| prev_fg = back.fg; | ||
| back.fg.write_ansi(true, &mut chunk); | ||
| } | ||
| if prev_bg != back.bg { | ||
| prev_bg = back.bg; | ||
| back.bg.write_ansi(false, &mut chunk); | ||
| } | ||
| chunk.push(back.ch); // Add the character | ||
| self.buffer[idx] = back; // Copy the Cell to the front buffer | ||
|
|
||
| if chunk.len() >= CHUNK_SIZE { | ||
| stdout_lock.write_all(chunk.as_bytes())?; | ||
| stdout_lock.flush()?; | ||
| chunk.clear(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
306
to
339
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rendering logic has been significantly optimized by removing the intermediate This can be achieved by tracking whether the previously processed cell in the same row was also updated. Here's a conceptual example of how this could be implemented: for y in 0..self.height {
let mut last_cell_was_dirty = false;
for x in 0..self.width {
let idx = y * self.width + x;
let back = back_fb.buffer[idx];
if self.buffer[idx] != back {
if !last_cell_was_dirty {
// This is the first cell in a sequence of dirty cells, move cursor.
write!(chunk, "\x1B[{};{}H", y + 1, x + 1).unwrap();
}
// ... (rest of the logic to set attributes and write character) ...
self.buffer[idx] = back;
last_cell_was_dirty = true;
} else {
last_cell_was_dirty = false;
}
}
}This change would reduce the number of ANSI escape sequences sent to the terminal, especially for large contiguous updates, making rendering even more efficient. |
||
|
|
||
| let mut chunk = String::with_capacity(CHUNK_SIZE); | ||
|
|
||
| // Draw the output for each changed cell | ||
| for (x, y, idx, back) in changes { | ||
| chunk.push_str(&format!("\x1B[{};{}H", y + 1, x + 1)); // Move to the target coordinates | ||
| if prev_attrs != back.attrs { | ||
| prev_attrs = back.attrs; | ||
| chunk.push_str(&back.attrs.to_ansi()); | ||
| } | ||
| if prev_fg != back.fg { | ||
| prev_fg = back.fg; | ||
| chunk.push_str(&back.fg.to_ansi(true)); | ||
| } | ||
| if prev_bg != back.bg { | ||
| prev_bg = back.bg; | ||
| chunk.push_str(&back.bg.to_ansi(false)); | ||
| } | ||
| chunk.push(back.ch); // Add the character | ||
| self.buffer[idx] = *back; // Copy the Cell to the front buffer | ||
|
|
||
| if chunk.len() >= CHUNK_SIZE { | ||
| stdout_lock.write_all(chunk.as_bytes())?; | ||
| stdout_lock.flush()?; | ||
| chunk.clear(); | ||
| } | ||
| } | ||
| if !chunk.is_empty() { | ||
| stdout_lock.write_all(chunk.as_bytes())?; | ||
| stdout_lock.flush()?; | ||
|
|
@@ -505,4 +503,29 @@ mod tests { | |
| assert_eq!(fb1.buffer[7].ch, '─'); | ||
| assert_eq!(fb1.buffer[8].ch, '╯'); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_fb_combine_out_of_bounds() { | ||
| let mut fb1 = Framebuffer::new(3, 2); | ||
| let mut fb2 = Framebuffer::new(2, 2); | ||
| fb2.set_char( | ||
| 0, | ||
| 0, | ||
| 'X', | ||
| Attr::default(), | ||
| Color::default(), | ||
| Color::default(), | ||
| ); | ||
|
|
||
| // x_offset fully outside: must not panic | ||
| fb1.combine(&fb2, 7, 0); | ||
| // y_offset fully outside: must not panic | ||
| fb1.combine(&fb2, 0, 5); | ||
| // both offsets outside | ||
| fb1.combine(&fb2, 7, 5); | ||
| // fb1 should be unchanged (all spaces) | ||
| for cell in &fb1.buffer { | ||
| assert_eq!(cell.ch, ' '); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -49,7 +49,6 @@ impl RenderThread { | |||||||||||||||
| if elapsed_since_frame < rendering_rate { | ||||||||||||||||
| thread::sleep(rendering_rate - elapsed_since_frame); | ||||||||||||||||
| } | ||||||||||||||||
| last_frame_time = time::Instant::now(); | ||||||||||||||||
|
|
||||||||||||||||
| // Rendering process | ||||||||||||||||
| match back_fb.try_lock() { | ||||||||||||||||
|
|
@@ -58,12 +57,16 @@ impl RenderThread { | |||||||||||||||
| if front_locked.refresh(&back_locked).is_err() { | ||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
| // Update frame time only after a successful render so that | ||||||||||||||||
| // a failed lock attempt does not consume the frame budget. | ||||||||||||||||
| last_frame_time = time::Instant::now(); | ||||||||||||||||
| frame_count += 1; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| Err(TryLockError::WouldBlock) => { | ||||||||||||||||
| // Skip if the back buffer is locked | ||||||||||||||||
| thread::sleep(time::Duration::from_millis(1)); | ||||||||||||||||
| // back_fb is locked by draw(); retry immediately without | ||||||||||||||||
| // resetting the frame timer so the next iteration does not | ||||||||||||||||
| // sleep a full rendering_rate before retrying. | ||||||||||||||||
|
Comment on lines
+67
to
+69
|
||||||||||||||||
| // back_fb is locked by draw(); retry immediately without | |
| // resetting the frame timer so the next iteration does not | |
| // sleep a full rendering_rate before retrying. | |
| // back_fb is locked by draw(); yield to avoid busy-waiting, | |
| // but do not reset the frame timer so the next iteration does | |
| // not sleep a full rendering_rate before retrying. | |
| thread::yield_now(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RenderThread now busy-waits when the back_fb lock is held by another thread. The thread::sleep(time::Duration::from_millis(1)) was removed in the TryLockError::WouldBlock case, causing the thread to spin at 100% CPU usage until the lock becomes available. This uncontrolled resource consumption can lead to a denial of service, especially in multi-tenant environments or on battery-powered devices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use std::fmt::Write as FmtWrite;appears to be unused in this file (thewrite!macro doesn’t require the trait to be imported, and the alias isn’t referenced). If your CI denies warnings, this will fail builds; consider removing it or using it explicitly.