Skip to content

Commit

Permalink
Use buffer age to perform partial rendering
Browse files Browse the repository at this point in the history
This commit makes uses of buffer age extension and recent
alacritty_terminal damage tracking to perform partial redraws on
platforms supporting `GLX_EXT_buffer_age` and `EGL_EXT_buffer_age`.

The alacritty_terminal damage tracking was enhanced in a way that it
reports damage only up to `occ`, so partial redraws during scrolling
are possible.

Fixes alacritty#5843.
  • Loading branch information
kchibisov committed Feb 7, 2022
1 parent 998250f commit 30c4f02
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 158 deletions.
18 changes: 6 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Expand Up @@ -9,3 +9,6 @@ members = [
lto = true
debug = 1
incremental = false

[patch.crates-io]
glutin = { git = "https://github.com/kchibisov/glutin", branch = "buffer-age" }
6 changes: 1 addition & 5 deletions alacritty/res/text.f.glsl
Expand Up @@ -13,12 +13,8 @@ uniform sampler2D mask;

void main() {
if (backgroundPass != 0) {
if (bg.a == 0.0) {
discard;
}

alphaMask = vec4(1.0);
color = vec4(bg.rgb, bg.a);
color = vec4(bg.rgb * bg.a, bg.a);
} else if ((int(fg.a) & COLORED) != 0) {
// Color glyphs, like emojis.
vec4 glyphColor = texture(mask, TexCoords);
Expand Down
45 changes: 35 additions & 10 deletions alacritty/src/display/content.rs
Expand Up @@ -15,6 +15,7 @@ use alacritty_terminal::term::{RenderableContent as TerminalContent, Term, TermM

use crate::config::UiConfig;
use crate::display::color::{List, DIM_FACTOR};
use crate::display::damage::TerminalDamageHistory;
use crate::display::hint::HintState;
use crate::display::{self, Display, MAX_SEARCH_LINES};
use crate::event::SearchState;
Expand All @@ -27,6 +28,7 @@ pub const MIN_CURSOR_CONTRAST: f64 = 1.5;
/// This provides the terminal cursor and an iterator over all non-empty cells.
pub struct RenderableContent<'a> {
terminal_content: TerminalContent<'a>,
terminal_damage_history: &'a TerminalDamageHistory,
cursor: RenderableCursor,
cursor_shape: CursorShape,
cursor_point: Point<usize>,
Expand All @@ -35,18 +37,20 @@ pub struct RenderableContent<'a> {
config: &'a UiConfig,
colors: &'a List,
focused_match: Option<&'a Match>,
skip_empty_cells: bool,
}

impl<'a> RenderableContent<'a> {
pub fn new<T: EventListener>(
config: &'a UiConfig,
display: &'a mut Display,
term: &'a Term<T>,
selection: Option<SelectionRange>,
search_state: &'a SearchState,
) -> Self {
let terminal_content = term.renderable_content(selection);
let search = search_state.dfas().map(|dfas| Regex::new(term, dfas));
let focused_match = search_state.focused_match();
let terminal_content = term.renderable_content();

// Find terminal cursor shape.
let cursor_shape = if terminal_content.cursor.shape == CursorShape::Hidden
Expand All @@ -72,10 +76,17 @@ impl<'a> RenderableContent<'a> {
None
};

let terminal_damage_history = display.window.terminal_damage_history();

// If we'll clear terminal content anyway we can skip empty cells. Otherwise process them.
let skip_empty_cells = terminal_damage_history.should_clear();

Self {
colors: &display.colors,
cursor: RenderableCursor::new_hidden(),
terminal_content,
terminal_damage_history,
skip_empty_cells,
focused_match,
cursor_shape,
cursor_point,
Expand Down Expand Up @@ -103,10 +114,6 @@ impl<'a> RenderableContent<'a> {
self.terminal_content.colors[color].unwrap_or(self.colors[color])
}

pub fn selection_range(&self) -> Option<SelectionRange> {
self.terminal_content.selection
}

/// Assemble the information required to render the terminal cursor.
fn renderable_cursor(&mut self, cell: &RenderableCell) -> RenderableCursor {
// Cursor colors.
Expand Down Expand Up @@ -152,8 +159,17 @@ impl<'a> Iterator for RenderableContent<'a> {
/// (eg. invert fg and bg colors).
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
let mut cell = loop {
let cell = self.terminal_content.display_iter.next()?;
let point =
display::point_to_viewport(self.terminal_content.display_offset, cell.point)
.unwrap();

// Don't process undamaged points.
if !self.terminal_damage_history.is_damaged(point) {
continue;
}

let mut cell = RenderableCell::new(self, cell);

if self.cursor_point == cell.point {
Expand All @@ -168,12 +184,21 @@ impl<'a> Iterator for RenderableContent<'a> {
cell.bg_alpha = 1.;
}

return Some(cell);
} else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
// Skip empty cells and wide char spacers.
return Some(cell);
break cell;
// Wide char spacers are likely fine...
} else if !self.skip_empty_cells
|| (!cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER))
{
break cell;
}
};

// Restore alpha for background cells.
if cell.bg_alpha == 0. {
cell.bg_alpha = self.config.window_opacity();
}

Some(cell)
}
}

Expand Down
162 changes: 155 additions & 7 deletions alacritty/src/display/damage.rs
@@ -1,18 +1,27 @@
//! Damage tracking and shaping routines.

use std::cmp;
use std::collections::VecDeque;
use std::iter::Peekable;

use glutin::Rect;

use alacritty_terminal::index::Point;
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::term::{LineDamageBounds, SizeInfo, TermDamageIterator};

/// Maximum percent of area growth from merging damaged rects.
const MAX_GROWTH: f32 = 0.3;

/// Iterator which converts `alacritty_terminal` damage information into renderer damaged rects.
pub struct RenderDamageIterator<'a> {
damaged_lines: Peekable<TermDamageIterator<'a>>,
size_info: SizeInfo<u32>,
}

impl<'a> RenderDamageIterator<'a> {
pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: SizeInfo<u32>) -> Self {
pub fn new(damaged_lines: &'a [LineDamageBounds], size_info: SizeInfo<u32>) -> Self {
let damaged_lines = TermDamageIterator::new(damaged_lines);
Self { damaged_lines: damaged_lines.peekable(), size_info }
}

Expand Down Expand Up @@ -46,23 +55,54 @@ impl<'a> Iterator for RenderDamageIterator<'a> {
let line = self.damaged_lines.next()?;
let mut total_damage_rect = self.overdamage(self.rect_for_line(line));

// Merge rectangles which overlap with each other.
// We don't want to merge `total_damage_rect` with lines that a much longer/shorter than
// it, since we'd end up damaging in suboptimal way.
let max_width_growth = self.size_info.width() / 3;

// Merge rectangles which overlap with each other, unless they don't grow by
// `max_width_growth` at once.
while let Some(line) = self.damaged_lines.peek().copied() {
let next_rect = self.overdamage(self.rect_for_line(line));
if !rects_overlap(total_damage_rect, next_rect) {
if !rects_overlap(&total_damage_rect, &next_rect) {
break;
}

let merged_rect = merge_rects(total_damage_rect, next_rect);

// If the width growth is higher than `max_width_growth` don't merge rects and the
// area growth is larger than `MAX_GROWTH` to avoid overdamaging.
if width_growth(&total_damage_rect, &next_rect) > max_width_growth
&& area_growth(&total_damage_rect, &merged_rect) > MAX_GROWTH
{
break;
}

total_damage_rect = merge_rects(total_damage_rect, next_rect);
total_damage_rect = merged_rect;
let _ = self.damaged_lines.next();
}

Some(total_damage_rect)
}
}

/// The growth of width of intersected `[glutin::Rect]`.
fn width_growth(lhs: &Rect, rhs: &Rect) -> u32 {
let lhs_x_end = (lhs.x + lhs.width) as i32;
let rhs_x_end = (rhs.x + rhs.width) as i32;

// The amount damage rect width growth.
(lhs.x as i32 - rhs.x as i32).abs() as u32 + (lhs_x_end - rhs_x_end).abs() as u32
}

/// Area occupied by `[glutin::Rect]`.
fn area_growth(lhs: &Rect, rhs: &Rect) -> f32 {
let lhs_area = lhs.width * lhs.height;
let rhs_area = rhs.width * rhs.height;
1. - cmp::min(rhs_area, lhs_area) as f32 / cmp::max(rhs_area, lhs_area) as f32
}

/// Check if two given [`glutin::Rect`] overlap.
fn rects_overlap(lhs: Rect, rhs: Rect) -> bool {
pub fn rects_overlap(lhs: &Rect, rhs: &Rect) -> bool {
!(
// `lhs` is left of `rhs`.
lhs.x + lhs.width < rhs.x
Expand All @@ -76,11 +116,119 @@ fn rects_overlap(lhs: Rect, rhs: Rect) -> bool {
}

/// Merge two [`glutin::Rect`] by producing the smallest rectangle that contains both.
#[inline]
fn merge_rects(lhs: Rect, rhs: Rect) -> Rect {
pub fn merge_rects(lhs: Rect, rhs: Rect) -> Rect {
let left_x = cmp::min(lhs.x, rhs.x);
let right_x = cmp::max(lhs.x + lhs.width, rhs.x + rhs.width);
let y_top = cmp::max(lhs.y + lhs.height, rhs.y + rhs.height);
let y_bottom = cmp::min(lhs.y, rhs.y);
Rect { x: left_x, y: y_bottom, width: right_x - left_x, height: y_top - y_bottom }
}

/// Maximum number of terminal damage states we maintain.
const HISTORY_DAMAGE: usize = 4;

/// Track damage information from `HISTORY_DAMAGE` amount of frames from `alacritty_terminal`.
#[derive(Debug)]
pub struct TerminalDamageHistory {
using_frames: usize,
history: VecDeque<Vec<LineDamageBounds>>,
should_clear: VecDeque<bool>,
}

impl TerminalDamageHistory {
pub fn new() -> Self {
let should_clear = VecDeque::with_capacity(HISTORY_DAMAGE);
Self { using_frames: 1, history: VecDeque::with_capacity(HISTORY_DAMAGE), should_clear }
}

/// Saves damages information as the information for the current frame updating the previously
/// saved damage state.
#[inline]
pub fn save_damage(&mut self, damage: &[LineDamageBounds], should_clear: bool) {
// We don't need to track frames older than `HISTORY_DAMAGE`.
if self.history.len() == HISTORY_DAMAGE {
let _ = self.history.pop_back();
let _ = self.should_clear.pop_back();
}

self.history.push_front(damage.to_owned());
self.should_clear.push_front(should_clear);
}

/// Return current size of the history.
#[inline]
pub fn history_size(&self) -> usize {
self.history.len()
}

/// Checks whether the given `point` is damaged checking `num_frames` amount of frames.
#[inline]
pub fn is_damaged(&self, point: Point<usize>) -> bool {
if self.using_frames > self.history_size() {
return true;
}

// Check previous frames.
for index in 0..self.using_frames {
let line = self.history[index][point.line];

// Enlarge bounds to account for wide chars.
if point.column >= line.left.saturating_sub(1) && point.column <= line.right + 1 {
return true;
}
}

false
}

/// Damage information for the lastest frame added to the history.
#[inline]
pub fn latest_damage(&self) -> &[LineDamageBounds] {
&self.history[0]
}

/// Damage all the lines in the current frame.
#[inline]
pub fn damage_all(&mut self, size_info: &SizeInfo) {
let last_column = size_info.columns() - 1;
for line in self.history[0].iter_mut() {
line.left = 0;
line.right = last_column;
}
}

/// Damage line in the current frame.
#[inline]
pub fn damage_line(&mut self, line: usize, left: usize, right: usize) {
self.history[0][line].left = cmp::min(self.history[0][line].left, left);
self.history[0][line].right = cmp::max(self.history[0][line].right, right);
}

/// Invalidate all previously saved damage.
#[inline]
pub fn invalidate(&mut self) {
self.using_frames = 1;
self.should_clear.clear();
self.history.clear();
}

/// Use that `num_frames` amount of frames when checking whether the points are damaged.
pub fn use_frames(&mut self, num_frames: usize) {
self.using_frames = num_frames;
}

/// Check if the window will be cleared.
pub fn should_clear(&self) -> bool {
if self.using_frames > self.history_size() {
return true;
}

for index in 0..self.using_frames {
if self.should_clear[index] {
return true;
}
}

false
}
}

0 comments on commit 30c4f02

Please sign in to comment.