1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-24 05:42:03 +03:00

termwiz: improve performance of windows console renderer

This reduces flickering updates in the native windows console;
it works by taking a copy of the screen buffer, applying the
Change's to that buffer and then copying back to the console.
This commit is contained in:
Wez Furlong 2020-04-05 19:12:10 -07:00
parent fe89082764
commit 96880a08b4
3 changed files with 340 additions and 139 deletions

View File

@ -207,9 +207,16 @@ impl<T: Terminal> LineEditor<T> {
pub fn read_line(&mut self, host: &mut dyn LineEditorHost) -> anyhow::Result<Option<String>> {
self.terminal.set_raw_mode()?;
let res = self.read_line_impl(host);
self.terminal.set_cooked_mode()?;
self.terminal.render(&[Change::Text("\r\n".to_string())])?;
self.terminal.render(&[Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(
(self.last_render_height as isize) - (self.last_render_cursor_y as isize),
),
}])?;
self.terminal.flush()?;
self.terminal.set_cooked_mode()?;
res
}
@ -573,7 +580,7 @@ impl<T: Terminal> LineEditor<T> {
self.line = line;
}
}
None => {}
None => continue,
}
self.render(host)?;
}

View File

@ -7,20 +7,21 @@ use crate::surface::{Change, Position};
use crate::terminal::windows::ConsoleOutputHandle;
use num;
use std::io::Write;
use winapi::shared::minwindef::WORD;
use winapi::um::wincon::{
BACKGROUND_BLUE, BACKGROUND_GREEN, BACKGROUND_INTENSITY, BACKGROUND_RED,
BACKGROUND_BLUE, BACKGROUND_GREEN, BACKGROUND_INTENSITY, BACKGROUND_RED, CHAR_INFO,
COMMON_LVB_REVERSE_VIDEO, COMMON_LVB_UNDERSCORE, FOREGROUND_BLUE, FOREGROUND_GREEN,
FOREGROUND_INTENSITY, FOREGROUND_RED,
};
pub struct WindowsConsoleRenderer {
current_attr: CellAttributes,
pending_attr: CellAttributes,
}
impl WindowsConsoleRenderer {
pub fn new(_caps: Capabilities) -> Self {
Self {
current_attr: CellAttributes::default(),
pending_attr: CellAttributes::default(),
}
}
}
@ -110,208 +111,338 @@ fn to_attr_word(attr: &CellAttributes) -> u16 {
bg | fg | reverse | underline
}
struct ScreenBuffer {
buf: Vec<CHAR_INFO>,
dirty: bool,
rows: usize,
cols: usize,
cursor_x: usize,
cursor_y: usize,
pending_attr: WORD,
}
impl ScreenBuffer {
fn cursor_idx(&self) -> usize {
let idx = (self.cursor_y * self.cols) + self.cursor_x;
assert!(
idx < self.rows * self.cols,
"idx={}, cursor:({},{}) rows={}, cols={}.",
idx,
self.cursor_x,
self.cursor_y,
self.rows,
self.cols
);
idx
}
fn fill(&mut self, c: char, attr: WORD, x: usize, y: usize, num_elements: usize) -> usize {
let idx = (y * self.cols) + x;
let max = self.rows * self.cols;
let end = (idx + num_elements).min(max);
let c = c as u16;
for cell in &mut self.buf[idx..end] {
cell.Attributes = attr;
unsafe {
*cell.Char.UnicodeChar_mut() = c;
}
}
self.dirty = true;
end
}
fn set_cursor<B: ConsoleOutputHandle + Write>(
&mut self,
x: usize,
y: usize,
out: &mut B,
) -> anyhow::Result<()> {
self.cursor_x = x;
self.cursor_y = y;
if self.cursor_y >= self.rows {
let lines_to_scroll = self.cursor_y.saturating_sub(self.rows) + 1;
self.scroll_up(0, self.rows, lines_to_scroll, out)?;
// Adjust cursor by an extra position to compensate for the scroll
self.cursor_y -= lines_to_scroll + 1;
}
// Make sure we mark dirty after we've scrolled!
self.dirty = true;
assert!(self.cursor_x < self.cols);
assert!(self.cursor_y < self.rows);
Ok(())
}
fn write_text<B: ConsoleOutputHandle + Write>(
&mut self,
t: &str,
attr: WORD,
out: &mut B,
) -> anyhow::Result<()> {
for c in t.chars() {
match c {
'\r' => {
self.cursor_x = 0;
}
'\n' => {
self.cursor_y += 1;
if self.cursor_y >= self.rows {
self.dirty = true;
self.scroll_up(0, self.rows, 1 + self.cursor_y - self.rows, out)?;
self.dirty = true;
self.cursor_y = self.rows - 1;
assert!(self.cursor_y < self.rows);
}
}
c => {
if self.cursor_x == self.cols {
self.cursor_y += 1;
self.cursor_x = 0;
if self.cursor_y >= self.rows {
self.dirty = true;
self.scroll_up(0, self.rows, 1 + self.cursor_y - self.rows, out)?;
self.dirty = true;
self.cursor_y = self.rows - 1;
assert!(self.cursor_y < self.rows);
}
}
let idx = self.cursor_idx();
let mut cell = &mut self.buf[idx];
cell.Attributes = attr;
unsafe {
*cell.Char.UnicodeChar_mut() = c as u16;
}
self.cursor_x += 1;
self.dirty = true;
}
}
}
Ok(())
}
fn flush_screen<B: ConsoleOutputHandle + Write>(&mut self, out: &mut B) -> anyhow::Result<()> {
if self.dirty {
out.flush()?;
out.set_buffer_contents(&self.buf)?;
out.flush()?;
let info = out.get_buffer_info()?;
out.set_cursor_position(
self.cursor_x as i16,
self.cursor_y as i16 + info.srWindow.Top,
)?;
out.flush()?;
out.set_attr(self.pending_attr)?;
out.flush()?;
self.dirty = false;
}
Ok(())
}
fn reread_buffer<B: ConsoleOutputHandle + Write>(&mut self, out: &mut B) -> anyhow::Result<()> {
self.buf = out.get_buffer_contents()?;
self.dirty = false;
Ok(())
}
fn scroll_up<B: ConsoleOutputHandle + Write>(
&mut self,
first_row: usize,
region_size: usize,
scroll_count: usize,
out: &mut B,
) -> anyhow::Result<()> {
if region_size > 0 {
self.flush_screen(out)?;
let info = out.get_buffer_info()?;
out.scroll_region(
info.srWindow.Left,
info.srWindow.Top + first_row as i16,
info.srWindow.Right,
info.srWindow.Top + first_row as i16 + region_size as i16,
0,
-(scroll_count as i16),
self.pending_attr,
)?;
self.reread_buffer(out)?;
}
Ok(())
}
fn scroll_down<B: ConsoleOutputHandle + Write>(
&mut self,
first_row: usize,
region_size: usize,
scroll_count: usize,
out: &mut B,
) -> anyhow::Result<()> {
if region_size > 0 {
self.flush_screen(out)?;
let info = out.get_buffer_info()?;
out.scroll_region(
info.srWindow.Left,
info.srWindow.Top + first_row as i16,
info.srWindow.Right,
info.srWindow.Top + first_row as i16 + region_size as i16,
0,
scroll_count as i16,
self.pending_attr,
)?;
self.reread_buffer(out)?;
}
Ok(())
}
}
impl WindowsConsoleRenderer {
pub fn render_to<B: ConsoleOutputHandle + Write>(
&mut self,
changes: &[Change],
out: &mut B,
) -> anyhow::Result<()> {
out.flush()?;
let info = out.get_buffer_info()?;
let cols = info.dwSize.X as usize;
let rows = info.srWindow.Bottom as usize - info.srWindow.Top as usize;
let mut buffer = ScreenBuffer {
buf: out.get_buffer_contents()?,
cursor_x: info.dwCursorPosition.X as usize,
cursor_y: (info.dwCursorPosition.Y as usize)
.saturating_sub(info.srWindow.Top as usize)
.min(rows - 1),
dirty: false,
rows,
cols,
pending_attr: to_attr_word(&CellAttributes::default()),
};
for change in changes {
match change {
Change::ClearScreen(color) => {
out.flush()?;
self.current_attr = CellAttributes::default()
let attr = CellAttributes::default()
.set_background(color.clone())
.clone();
let info = out.get_buffer_info()?;
// We want to clear only the viewport; we don't want to toss out
// the scrollback.
if info.srWindow.Left != 0 {
// The user has scrolled the viewport horizontally; let's move
// it back to the left for the sake of sanity
out.set_viewport(
0,
info.srWindow.Top,
info.srWindow.Right - info.srWindow.Left,
info.srWindow.Bottom,
)?;
}
// Clear the full width of the buffer (not the viewport size)
let visible_width = info.dwSize.X as u32;
// And clear all of the visible lines from this point down
let visible_height = info.dwSize.Y as u32 - info.srWindow.Top as u32;
let num_spaces = visible_width * visible_height;
out.fill_char(' ', 0, info.srWindow.Top, num_spaces as u32)?;
out.fill_attr(
to_attr_word(&self.current_attr),
0,
info.srWindow.Top,
num_spaces as u32,
)?;
out.set_cursor_position(0, info.srWindow.Top)?;
buffer.fill(' ', to_attr_word(&attr), 0, 0, cols * rows);
buffer.set_cursor(0, 0, out)?;
}
Change::ClearToEndOfLine(color) => {
out.flush()?;
self.current_attr = CellAttributes::default()
let attr = CellAttributes::default()
.set_background(color.clone())
.clone();
let info = out.get_buffer_info()?;
let width =
(info.dwSize.X as u32).saturating_sub(info.dwCursorPosition.X as u32);
out.fill_char(' ', info.dwCursorPosition.X, info.dwCursorPosition.Y, width)?;
out.fill_attr(
to_attr_word(&self.current_attr),
info.dwCursorPosition.X,
info.dwCursorPosition.Y,
width,
)?;
buffer.fill(
' ',
to_attr_word(&attr),
buffer.cursor_x,
buffer.cursor_y,
cols.saturating_sub(buffer.cursor_x),
);
}
Change::ClearToEndOfScreen(color) => {
out.flush()?;
self.current_attr = CellAttributes::default()
let attr = CellAttributes::default()
.set_background(color.clone())
.clone();
let info = out.get_buffer_info()?;
let width =
(info.dwSize.X as u32).saturating_sub(info.dwCursorPosition.X as u32);
out.fill_char(' ', info.dwCursorPosition.X, info.dwCursorPosition.Y, width)?;
out.fill_attr(
to_attr_word(&self.current_attr),
info.dwCursorPosition.X,
info.dwCursorPosition.Y,
width,
)?;
// Clear the full width of the buffer (not the viewport size)
let visible_width = info.dwSize.X as u32;
// And clear all of the visible lines below the cursor
let visible_height =
(info.dwSize.Y as u32).saturating_sub((info.dwCursorPosition.Y as u32) + 1);
let num_spaces = visible_width * visible_height;
out.fill_char(' ', 0, info.dwCursorPosition.Y + 1, num_spaces as u32)?;
out.fill_attr(
to_attr_word(&self.current_attr),
0,
info.dwCursorPosition.Y + 1,
num_spaces as u32,
)?;
buffer.fill(
' ',
to_attr_word(&attr),
buffer.cursor_x,
buffer.cursor_y,
cols * rows,
);
}
Change::Text(text) => {
out.flush()?;
out.set_attr(to_attr_word(&self.current_attr))?;
out.write_all(text.as_bytes())?;
buffer.write_text(&text, to_attr_word(&self.pending_attr), out)?;
}
Change::CursorPosition { x, y } => {
out.flush()?;
let info = out.get_buffer_info()?;
// For horizontal cursor movement, we consider the full width
// of the screen buffer, even if the viewport is smaller
let x = match x {
Position::Absolute(x) => *x as i16,
Position::Relative(delta) => info.dwCursorPosition.X + *delta as i16,
Position::EndRelative(delta) => info.dwSize.X - *delta as i16,
Position::Absolute(x) => *x as usize,
Position::Relative(delta) => {
(buffer.cursor_x as isize).saturating_sub(-*delta) as usize
}
Position::EndRelative(delta) => cols.saturating_sub(*delta),
};
// For vertical cursor movement, we constrain the movement to
// the viewport.
let y = match y {
Position::Absolute(y) => info.srWindow.Top + *y as i16,
Position::Relative(delta) => info.dwCursorPosition.Y + *delta as i16,
Position::EndRelative(delta) => info.srWindow.Bottom - *delta as i16,
Position::Absolute(y) => *y as usize,
Position::Relative(delta) => {
(buffer.cursor_y as isize).saturating_sub(-*delta) as usize
}
Position::EndRelative(delta) => rows.saturating_sub(*delta),
};
out.set_cursor_position(x, y)?;
buffer.set_cursor(x, y, out)?;
}
Change::Attribute(AttributeChange::Intensity(value)) => {
self.current_attr.set_intensity(*value);
self.pending_attr.set_intensity(*value);
}
Change::Attribute(AttributeChange::Italic(value)) => {
self.current_attr.set_italic(*value);
self.pending_attr.set_italic(*value);
}
Change::Attribute(AttributeChange::Reverse(value)) => {
self.current_attr.set_reverse(*value);
self.pending_attr.set_reverse(*value);
}
Change::Attribute(AttributeChange::StrikeThrough(value)) => {
self.current_attr.set_strikethrough(*value);
self.pending_attr.set_strikethrough(*value);
}
Change::Attribute(AttributeChange::Blink(value)) => {
self.current_attr.set_blink(*value);
self.pending_attr.set_blink(*value);
}
Change::Attribute(AttributeChange::Invisible(value)) => {
self.current_attr.set_invisible(*value);
self.pending_attr.set_invisible(*value);
}
Change::Attribute(AttributeChange::Underline(value)) => {
self.current_attr.set_underline(*value);
self.pending_attr.set_underline(*value);
}
Change::Attribute(AttributeChange::Foreground(col)) => {
self.current_attr.set_foreground(*col);
self.pending_attr.set_foreground(*col);
}
Change::Attribute(AttributeChange::Background(col)) => {
self.current_attr.set_background(*col);
self.pending_attr.set_background(*col);
}
Change::Attribute(AttributeChange::Hyperlink(link)) => {
self.current_attr.hyperlink = link.clone();
self.pending_attr.hyperlink = link.clone();
}
Change::AllAttributes(all) => {
self.current_attr = all.clone();
self.pending_attr = all.clone();
}
Change::CursorColor(_color) => {}
Change::CursorShape(_shape) => {}
Change::Image(image) => {
// Images are not supported, so just blank out the cells and
// move the cursor to the right spot
out.flush()?;
let info = out.get_buffer_info()?;
for y in 0..image.height {
out.fill_char(
buffer.fill(
' ',
info.dwCursorPosition.X,
y as i16 + info.dwCursorPosition.Y,
image.width as u32,
)?;
0,
buffer.cursor_x,
y + buffer.cursor_y,
image.width as usize,
);
}
out.set_cursor_position(
info.dwCursorPosition.X + image.width as i16,
info.dwCursorPosition.Y,
)?;
buffer.set_cursor(buffer.cursor_x + image.width, buffer.cursor_y, out)?;
}
Change::ScrollRegionUp {
first_row,
region_size,
scroll_count,
} => {
if *region_size > 0 {
let info = out.get_buffer_info()?;
out.scroll_region(
info.srWindow.Left,
info.srWindow.Top + *first_row as i16,
info.srWindow.Right,
info.srWindow.Top + *first_row as i16 + *region_size as i16,
0,
-(*scroll_count as i16),
to_attr_word(&self.current_attr),
)?;
}
buffer.scroll_up(*first_row, *region_size, *scroll_count, out)?;
}
Change::ScrollRegionDown {
first_row,
region_size,
scroll_count,
} => {
if *region_size > 0 {
let info = out.get_buffer_info()?;
out.scroll_region(
info.srWindow.Left,
info.srWindow.Top + *first_row as i16,
info.srWindow.Right,
info.srWindow.Top + *first_row as i16 + *region_size as i16,
0,
*scroll_count as i16,
to_attr_word(&self.current_attr),
)?;
}
buffer.scroll_down(*first_row, *region_size, *scroll_count, out)?;
}
Change::Title(_text) => {
// Don't actually render this for now.
@ -325,8 +456,8 @@ impl WindowsConsoleRenderer {
}
}
}
out.flush()?;
out.set_attr(to_attr_word(&self.current_attr))?;
buffer.flush_screen(out)?;
Ok(())
}
}

View File

@ -15,11 +15,11 @@ use winapi::um::synchapi::{CreateEventW, SetEvent, WaitForMultipleObjects};
use winapi::um::winbase::{INFINITE, WAIT_FAILED, WAIT_OBJECT_0};
use winapi::um::wincon::{
FillConsoleOutputAttribute, FillConsoleOutputCharacterW, GetConsoleScreenBufferInfo,
ScrollConsoleScreenBufferW, SetConsoleCursorPosition, SetConsoleScreenBufferSize,
SetConsoleTextAttribute, SetConsoleWindowInfo, CHAR_INFO, CONSOLE_SCREEN_BUFFER_INFO, COORD,
DISABLE_NEWLINE_AUTO_RETURN, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_MOUSE_INPUT,
ENABLE_PROCESSED_INPUT, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING,
ENABLE_WINDOW_INPUT, INPUT_RECORD, SMALL_RECT,
ReadConsoleOutputW, ScrollConsoleScreenBufferW, SetConsoleCursorPosition,
SetConsoleScreenBufferSize, SetConsoleTextAttribute, SetConsoleWindowInfo, WriteConsoleOutputW,
CHAR_INFO, CONSOLE_SCREEN_BUFFER_INFO, COORD, DISABLE_NEWLINE_AUTO_RETURN, ENABLE_ECHO_INPUT,
ENABLE_LINE_INPUT, ENABLE_MOUSE_INPUT, ENABLE_PROCESSED_INPUT, ENABLE_VIRTUAL_TERMINAL_INPUT,
ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WINDOW_INPUT, INPUT_RECORD, SMALL_RECT,
};
use crate::caps::Capabilities;
@ -52,6 +52,8 @@ pub trait ConsoleOutputHandle {
fn set_attr(&mut self, attr: u16) -> Result<(), Error>;
fn set_cursor_position(&mut self, x: i16, y: i16) -> Result<(), Error>;
fn get_buffer_info(&mut self) -> Result<CONSOLE_SCREEN_BUFFER_INFO, Error>;
fn get_buffer_contents(&mut self) -> anyhow::Result<Vec<CHAR_INFO>>;
fn set_buffer_contents(&mut self, buffer: &[CHAR_INFO]) -> anyhow::Result<()>;
fn set_viewport(&mut self, left: i16, top: i16, right: i16, bottom: i16) -> Result<(), Error>;
fn scroll_region(
&mut self,
@ -296,6 +298,67 @@ impl ConsoleOutputHandle for OutputHandle {
Ok(())
}
fn get_buffer_contents(&mut self) -> anyhow::Result<Vec<CHAR_INFO>> {
let info = self.get_buffer_info()?;
let cols = info.dwSize.X as usize;
let rows = info.srWindow.Bottom as usize - info.srWindow.Top as usize;
let mut res = vec![
CHAR_INFO {
Attributes: 0,
Char: unsafe { mem::zeroed() }
};
cols * rows
];
let mut read_region = info.srWindow.clone();
unsafe {
if ReadConsoleOutputW(
self.handle.as_raw_handle() as *mut _,
res.as_mut_ptr(),
COORD {
X: cols as i16,
Y: rows as i16,
},
COORD { X: 0, Y: 0 },
&mut read_region,
) == 0
{
bail!("ReadConsoleOutputW failed: {}", IoError::last_os_error());
}
}
Ok(res)
}
fn set_buffer_contents(&mut self, buffer: &[CHAR_INFO]) -> anyhow::Result<()> {
let info = self.get_buffer_info()?;
let cols = info.dwSize.X as usize;
let rows = info.srWindow.Bottom as usize - info.srWindow.Top as usize;
anyhow::ensure!(
rows * cols == buffer.len(),
"buffer size doesn't match screen size"
);
let mut write_region = info.srWindow.clone();
unsafe {
if WriteConsoleOutputW(
self.handle.as_raw_handle() as *mut _,
buffer.as_ptr(),
COORD {
X: cols as i16,
Y: rows as i16,
},
COORD { X: 0, Y: 0 },
&mut write_region,
) == 0
{
bail!("WriteConsoleOutputW failed: {}", IoError::last_os_error());
}
}
Ok(())
}
fn get_buffer_info(&mut self) -> Result<CONSOLE_SCREEN_BUFFER_INFO, Error> {
let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() };
let ok = unsafe {
@ -604,7 +667,7 @@ impl Terminal for WindowsTerminal {
)
};
if result == WAIT_OBJECT_0 + 0 {
pending = 1;
pending = self.input_handle.get_number_of_input_events()?;
} else if result == WAIT_OBJECT_0 + 1 {
return Ok(Some(InputEvent::Wake));
} else if result == WAIT_FAILED {