use crate::base::Rect;
use super::cell::Cell;
use super::diff::{cells_equal, FrameDiff, Run};
use super::surface::Surface;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Shift {
pub top: i32,
pub bottom: i32,
pub n: i32,
pub up: bool,
}
#[derive(Copy, Clone, Debug)]
pub struct ScrolledRuns<'a> {
pub(super) shift: Option<Shift>,
pub(super) runs: &'a [Run],
}
impl<'a> ScrolledRuns<'a> {
pub fn plain(runs: &'a [Run]) -> ScrolledRuns<'a> {
ScrolledRuns { shift: None, runs }
}
pub fn shift(&self) -> Option<Shift> {
self.shift
}
pub fn runs(&self) -> &'a [Run] {
self.runs
}
pub fn is_empty(&self) -> bool {
self.shift.is_none() && self.runs.is_empty()
}
}
const MIN_BAND_H: i32 = 8;
const MIN_SAVED_ROWS: i32 = 4;
const MAX_CANDIDATES: usize = 4;
const ERASED: Cell = Cell::EMPTY;
impl FrameDiff {
pub fn compute_scrolled<'a>(
&'a mut self,
prev: &Surface,
next: &Surface,
damage: &[Rect],
) -> ScrolledRuns<'a> {
let size = next.size();
if size.is_empty() || prev.size() != size {
return ScrolledRuns::plain(self.compute(prev, next, damage));
}
let bounds = Rect::from_size(size);
let union = damage
.iter()
.fold(Rect::ZERO, |acc, r| acc.union(r.intersect(bounds)));
if union.is_empty() || union.x > 0 || union.right() < size.w || union.h < MIN_BAND_H {
return ScrolledRuns::plain(self.compute(prev, next, damage));
}
let Some(shift) = self.detect_shift(prev, next, union.y, union.bottom()) else {
return ScrolledRuns::plain(self.compute(prev, next, damage));
};
self.runs.clear();
self.spans.clear();
self.collect_spans(damage, size);
self.spans.sort_unstable();
let mut i = 0;
while i < self.spans.len() {
let (y, x0, mut x1) = self.spans[i];
i += 1;
while i < self.spans.len() {
let (ny, nx0, nx1) = self.spans[i];
if ny != y || nx0 > x1 {
break;
}
x1 = x1.max(nx1);
i += 1;
}
self.scan_shifted(prev, next, &shift, y, x0, x1);
}
ScrolledRuns {
shift: Some(shift),
runs: &self.runs,
}
}
fn scan_shifted(
&mut self,
prev: &Surface,
next: &Surface,
shift: &Shift,
y: i32,
x0: i32,
x1: i32,
) {
let src = shift.source_row(y);
let next_row = next.row(y);
let mut run_start: Option<i32> = None;
for x in x0..x1 {
let b = &next_row[x as usize];
let equal = match src {
RowSource::Row(sy) => {
let a = &prev.row(sy)[x as usize];
cells_equal(prev, next, a, b)
}
RowSource::Erased => cells_equal(prev, next, &ERASED, b),
};
if equal {
if let Some(start) = run_start.take() {
self.push_run(y, start, x);
}
} else if run_start.is_none() {
let start = if b.is_continuation() {
(x - 1).max(0)
} else {
x
};
run_start = Some(start);
}
}
if let Some(start) = run_start {
self.push_run(y, start, x1);
}
}
fn detect_shift(
&mut self,
prev: &Surface,
next: &Surface,
top: i32,
bottom: i32,
) -> Option<Shift> {
let base = top;
self.fp_prev.clear();
self.fp_next.clear();
for y in top..bottom {
self.fp_prev.push(row_fingerprint(prev, y));
self.fp_next.push(row_fingerprint(next, y));
}
let fp = |v: &Vec<u64>, y: i32| v[(y - base) as usize];
let mut top = top;
let mut bottom = bottom;
while top < bottom
&& fp(&self.fp_prev, top) == fp(&self.fp_next, top)
&& rows_equal(prev, next, top, top)
{
top += 1;
}
while bottom > top
&& fp(&self.fp_prev, bottom - 1) == fp(&self.fp_next, bottom - 1)
&& rows_equal(prev, next, bottom - 1, bottom - 1)
{
bottom -= 1;
}
let band_h = bottom - top;
if band_h < MIN_BAND_H {
return None;
}
let mut candidates = [(0i32, false); MAX_CANDIDATES * 2];
let mut n_cand = 0;
for n in 1..band_h - 1 {
if n_cand < MAX_CANDIDATES && fp(&self.fp_next, top) == fp(&self.fp_prev, top + n) {
candidates[n_cand] = (n, true);
n_cand += 1;
}
}
let mut down_found = 0;
for n in 1..band_h - 1 {
if down_found < MAX_CANDIDATES
&& fp(&self.fp_next, bottom - 1) == fp(&self.fp_prev, bottom - 1 - n)
{
candidates[n_cand] = (n, false);
n_cand += 1;
down_found += 1;
}
}
let mut best: Option<(Shift, i32)> = None;
for &(n, up) in &candidates[..n_cand] {
let shift = Shift { top, bottom, n, up };
let mut saved = 0;
for y in top..bottom {
if let RowSource::Row(sy) = shift.source_row(y) {
if sy != y
&& fp(&self.fp_next, y) == fp(&self.fp_prev, sy)
&& rows_equal(prev, next, sy, y)
{
saved += 1;
}
}
}
if saved >= MIN_SAVED_ROWS && best.is_none_or(|(_, s)| saved > s) {
best = Some((shift, saved));
}
}
best.map(|(s, _)| s)
}
}
impl Shift {
fn source_row(&self, y: i32) -> RowSource {
if y < self.top || y >= self.bottom {
return RowSource::Row(y);
}
if self.up {
let sy = y + self.n;
if sy < self.bottom {
RowSource::Row(sy)
} else {
RowSource::Erased
}
} else {
let sy = y - self.n;
if sy >= self.top {
RowSource::Row(sy)
} else {
RowSource::Erased
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum RowSource {
Row(i32),
Erased,
}
fn rows_equal(prev: &Surface, next: &Surface, prev_y: i32, next_y: i32) -> bool {
let pr = prev.row(prev_y);
let nr = next.row(next_y);
pr.len() == nr.len()
&& pr
.iter()
.zip(nr)
.all(|(a, b)| cells_equal(prev, next, a, b))
}
fn row_fingerprint(s: &Surface, y: i32) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x100_0000_01b3;
let mut h = OFFSET;
let mut eat = |bytes: &[u8]| {
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(PRIME);
}
};
for cell in s.row(y) {
eat(s.glyph_str(cell).as_bytes());
eat(&[
cell.fg.r, cell.fg.g, cell.fg.b, cell.fg.a, cell.bg.r, cell.bg.g, cell.bg.b, cell.bg.a,
cell.ul.r, cell.ul.g, cell.ul.b, cell.ul.a,
]);
eat(&cell.attrs.bits().to_le_bytes());
if cell.link != 0 {
eat(s.link_uri(cell.link).unwrap_or("").as_bytes());
}
eat(&[0xFF]);
}
h
}
#[cfg(test)]
#[path = "scroll_tests.rs"]
mod tests;