perf: fast display (row runs, bulk fills, pixel scroll), terminal draws changed cells only + scrolls pixels, print! batched (was IPC per fmt fragment), snake sleeps on timer
This commit is contained in:
@@ -15,12 +15,15 @@ pub struct Grid {
|
||||
pub row: usize,
|
||||
/// Rows to redraw.
|
||||
dirty: Vec<bool>,
|
||||
/// Rows everything moved up by since the screen last caught up: moving
|
||||
/// the pixels is cheaper than drawing every row again.
|
||||
scrolled: usize,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn new(cols: usize, rows: usize) -> Self {
|
||||
let (cols, rows) = (cols.clamp(1, MAX_COLS), rows.clamp(1, MAX_ROWS));
|
||||
Self { cells: vec![b' '; cols * rows], cols, rows, col: 0, row: 0, dirty: vec![true; rows] }
|
||||
Self { cells: vec![b' '; cols * rows], cols, rows, col: 0, row: 0, dirty: vec![true; rows], scrolled: 0 }
|
||||
}
|
||||
|
||||
pub fn row(&self, r: usize) -> &[u8] {
|
||||
@@ -37,6 +40,12 @@ impl Grid {
|
||||
self.dirty[..rows].iter_mut().enumerate().filter_map(|(r, d)| core::mem::take(d).then_some(r))
|
||||
}
|
||||
|
||||
/// How far to move the screen up before drawing the dirty rows, which
|
||||
/// already account for it.
|
||||
pub fn take_scrolled(&mut self) -> usize {
|
||||
core::mem::take(&mut self.scrolled)
|
||||
}
|
||||
|
||||
fn mark(&mut self, r: usize) {
|
||||
self.dirty[r] = true;
|
||||
}
|
||||
@@ -53,7 +62,16 @@ impl Grid {
|
||||
self.cells.copy_within(self.cols.., 0);
|
||||
let last = (self.rows - 1) * self.cols;
|
||||
self.cells[last..].fill(b' ');
|
||||
self.dirty.fill(true);
|
||||
// Rows still to redraw move up with their text.
|
||||
self.dirty.copy_within(1.., 0);
|
||||
self.mark(self.rows - 1);
|
||||
if self.scrolled + 1 >= self.rows {
|
||||
// Scrolled a whole screen: just draw it all.
|
||||
self.touch_all();
|
||||
self.scrolled = 0;
|
||||
} else {
|
||||
self.scrolled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes one byte at the cursor: `\n` moves to the next line, other
|
||||
@@ -90,5 +108,6 @@ impl Grid {
|
||||
self.col = 0;
|
||||
self.row = 0;
|
||||
self.dirty.fill(true);
|
||||
self.scrolled = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use libkos::input::{InputEvent, Key};
|
||||
use libkos::syscall;
|
||||
use types::badge::Badge;
|
||||
use types::bounded::CapSlot;
|
||||
use types::display::{Color, ScreenCoord};
|
||||
use types::display::{Color, Dimension, ScreenCoord};
|
||||
use types::terminal::{KEYS_BIT, TIMER_BIT};
|
||||
|
||||
/// Where init put them (its manifest: own endpoint, then `uses`).
|
||||
@@ -33,6 +33,8 @@ struct Screen {
|
||||
/// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
|
||||
notify: CapSlot,
|
||||
timer: CapSlot,
|
||||
/// The screen's height, all of which scrolls.
|
||||
height: Dimension,
|
||||
}
|
||||
|
||||
/// A key as the line editor wants it, if it is one.
|
||||
@@ -65,6 +67,11 @@ impl server::Console for Screen {
|
||||
display::flush(DISPLAY);
|
||||
}
|
||||
|
||||
fn scroll(&mut self, rows: usize) {
|
||||
let by = Dimension::new(u16::try_from(rows * CELL_HEIGHT).unwrap_or(u16::MAX));
|
||||
display::scroll(DISPLAY, ScreenCoord::new(0), self.height, by, BG);
|
||||
}
|
||||
|
||||
fn flush_later(&mut self) {
|
||||
let at = syscall::get_time().raw() + FLUSH_DELAY;
|
||||
let _ = syscall::set_timer(self.timer, types::time::Ticks::from_raw(at));
|
||||
@@ -116,6 +123,6 @@ pub fn run() -> ! {
|
||||
display::clear(DISPLAY, BG);
|
||||
let cols = w.raw() as usize / CELL_WIDTH;
|
||||
let rows = h.raw() as usize / CELL_HEIGHT;
|
||||
let mut srv = server::TerminalServer::new(Screen { notify, timer }, cols, rows);
|
||||
let mut srv = server::TerminalServer::new(Screen { notify, timer, height: h }, cols, rows);
|
||||
server_framework::serve(&mut srv, EP)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ pub trait Console {
|
||||
/// Shows the cursor at a cell, over what `draw_row` drew there.
|
||||
fn draw_cursor(&mut self, col: usize, row: usize, under: u8);
|
||||
fn flush(&mut self);
|
||||
/// Moves everything drawn up by `rows` text rows, blank below.
|
||||
fn scroll(&mut self, rows: usize);
|
||||
/// Soon calls `TerminalServer::signal`, to flush a burst of output once.
|
||||
fn flush_later(&mut self);
|
||||
/// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace.
|
||||
@@ -37,6 +39,8 @@ pub struct TerminalServer<C: Console> {
|
||||
line_len: usize,
|
||||
line_read: usize,
|
||||
flush_pending: bool,
|
||||
/// How many cells of each row are drawn on the screen.
|
||||
drawn: [usize; crate::grid::MAX_ROWS],
|
||||
typeahead: [u8; TYPEAHEAD],
|
||||
typeahead_len: usize,
|
||||
/// Told about Ctrl+C typed while no line is being read.
|
||||
@@ -61,6 +65,7 @@ impl<C: Console> TerminalServer<C> {
|
||||
line_len: 0,
|
||||
line_read: 0,
|
||||
flush_pending: false,
|
||||
drawn: [0; crate::grid::MAX_ROWS],
|
||||
typeahead: [0; TYPEAHEAD],
|
||||
typeahead_len: 0,
|
||||
on_interrupt: None,
|
||||
@@ -84,16 +89,33 @@ impl<C: Console> TerminalServer<C> {
|
||||
}
|
||||
|
||||
fn redraw(&mut self) {
|
||||
let scrolled = self.grid.take_scrolled();
|
||||
if scrolled > 0 {
|
||||
self.console.scroll(scrolled);
|
||||
let rows = self.grid.rows();
|
||||
self.drawn.copy_within(scrolled.min(rows)..rows, 0);
|
||||
self.drawn[rows - scrolled.min(rows)..rows].fill(0);
|
||||
}
|
||||
let mut dirty = [false; crate::grid::MAX_ROWS];
|
||||
for r in self.grid.take_dirty() {
|
||||
dirty[r] = true;
|
||||
}
|
||||
for (r, _) in dirty.iter().enumerate().filter(|(_, d)| **d) {
|
||||
self.console.draw_row(r, self.grid.row(r));
|
||||
// As far as there is text, or as far as was drawn before (so
|
||||
// what is gone is erased): rarely the whole width.
|
||||
let row = self.grid.row(r);
|
||||
let text = row.iter().rposition(|b| *b != b' ').map_or(0, |i| i + 1);
|
||||
let width = text.max(self.drawn[r]).min(row.len());
|
||||
if width > 0 {
|
||||
self.console.draw_row(r, &row[..width]);
|
||||
}
|
||||
self.drawn[r] = text;
|
||||
}
|
||||
let (col, row) = (self.grid.col, self.grid.row);
|
||||
let under = self.grid.row(row).get(col).copied().unwrap_or(b' ');
|
||||
self.console.draw_cursor(col, row, under);
|
||||
// The cursor cell is drawn too, so it is erased when it moves.
|
||||
self.drawn[row] = self.drawn[row].max(col + 1);
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
@@ -210,7 +232,9 @@ impl<C: Console> Server for TerminalServer<C> {
|
||||
let reply = match words[0] {
|
||||
types::terminal::READ => self.read(),
|
||||
types::terminal::REDRAW => {
|
||||
// Taking focus back clears the screen.
|
||||
self.console.take_focus();
|
||||
self.drawn = [0; crate::grid::MAX_ROWS];
|
||||
self.grid.touch_all();
|
||||
self.redraw();
|
||||
self.console.flush();
|
||||
|
||||
@@ -16,15 +16,23 @@ struct Fake {
|
||||
flushes: usize,
|
||||
timers: usize,
|
||||
focus_taken: usize,
|
||||
scrolled: usize,
|
||||
/// Every row drawn, with how many cells.
|
||||
rows_drawn: Vec<(usize, usize)>,
|
||||
interrupted: Vec<CapSlot>,
|
||||
}
|
||||
|
||||
impl Console for Fake {
|
||||
fn draw_row(&mut self, _row: usize, _text: &[u8]) {}
|
||||
fn draw_row(&mut self, row: usize, text: &[u8]) {
|
||||
self.rows_drawn.push((row, text.len()));
|
||||
}
|
||||
fn draw_cursor(&mut self, _col: usize, _row: usize, _under: u8) {}
|
||||
fn flush(&mut self) {
|
||||
self.flushes += 1;
|
||||
}
|
||||
fn scroll(&mut self, rows: usize) {
|
||||
self.scrolled += rows;
|
||||
}
|
||||
fn flush_later(&mut self) {
|
||||
self.timers += 1;
|
||||
}
|
||||
@@ -143,3 +151,28 @@ fn redraw_takes_focus_back_and_paints_every_row() {
|
||||
assert_eq!(t.console().focus_taken, 1);
|
||||
assert_eq!(&t.grid().row(0)[..2], b"hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_full_screen_scrolls_its_pixels_and_draws_one_row() {
|
||||
let mut t = term(&[]);
|
||||
for _ in 0..12 {
|
||||
t.handle_call(1, text_words(b"line\n"), None);
|
||||
}
|
||||
// 10 rows: two scrolls so far, then one more line.
|
||||
t.console_mut_for_test().rows_drawn.clear();
|
||||
let before = t.console().scrolled;
|
||||
t.handle_call(1, text_words(b"more\n"), None);
|
||||
assert_eq!(t.console().scrolled, before + 1, "the screen moved up a row");
|
||||
let drawn = &t.console().rows_drawn;
|
||||
assert!(drawn.len() <= 2, "only the rows that changed: {drawn:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_are_drawn_only_as_wide_as_their_text_or_what_it_replaces() {
|
||||
let mut t = term(&[]);
|
||||
t.handle_call(1, text_words(b"hello"), None);
|
||||
assert_eq!(t.console().rows_drawn.last(), Some(&(0, 5)));
|
||||
t.handle_call(1, [types::terminal::CLEAR, 0, 0, 0], None);
|
||||
// Cleared: the old five cells and the cursor after them are erased.
|
||||
assert!(t.console().rows_drawn.contains(&(0, 6)), "{:?}", t.console().rows_drawn);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user