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:
@@ -9,6 +9,9 @@ pub enum DisplayRequest {
|
||||
Flush,
|
||||
GetDimensions,
|
||||
DrawText(Text),
|
||||
/// Moves the rows from `top`, `height` tall, up by `by`; the rows left
|
||||
/// at the bottom are filled with `fill`.
|
||||
Scroll { top: ScreenCoord, height: Dimension, by: Dimension, fill: Color },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -30,6 +33,10 @@ impl Encode for DisplayRequest {
|
||||
DisplayRequest::Flush => [2, 0, 0, 0],
|
||||
DisplayRequest::GetDimensions => [3, 0, 0, 0],
|
||||
DisplayRequest::DrawText(t) => t.words(),
|
||||
DisplayRequest::Scroll { top, height, by, fill } => {
|
||||
let band = (top.raw() as usize) | ((height.raw() as usize) << 16);
|
||||
[types::display::SCROLL, band, by.raw() as usize, fill.raw() as usize]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +55,12 @@ impl Decode for DisplayRequest {
|
||||
1 => Some(DisplayRequest::Clear { color: Color::from_raw(words[1] as u32) }),
|
||||
2 => Some(DisplayRequest::Flush),
|
||||
3 => Some(DisplayRequest::GetDimensions),
|
||||
types::display::SCROLL => Some(DisplayRequest::Scroll {
|
||||
top: ScreenCoord::new((words[1] & 0xFFFF) as u16),
|
||||
height: Dimension::new(((words[1] >> 16) & 0xFFFF) as u16),
|
||||
by: Dimension::new((words[2] & 0xFFFF) as u16),
|
||||
fill: Color::from_raw(words[3] as u32),
|
||||
}),
|
||||
_ => Text::from_words(words).map(DisplayRequest::DrawText),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use types::bounded::CapSlot;
|
||||
|
||||
use crate::protocol::{DisplayRequest, DisplayResponse};
|
||||
use port_framebuffer::Framebuffer;
|
||||
use types::display::text::Text;
|
||||
use types::display::text::{TEXT_BYTES, Text};
|
||||
use types::display::{Dimension, ScreenCoord};
|
||||
|
||||
use crate::focus::Focus;
|
||||
@@ -38,17 +38,20 @@ impl<F: Framebuffer> DisplayServer<F> {
|
||||
if y0.saturating_add(font::HEIGHT) > fh {
|
||||
return;
|
||||
}
|
||||
for (i, c) in t.chars().iter().enumerate() {
|
||||
let Some(cx) = x0.checked_add(i as u16 * font::WIDTH).filter(|cx| cx + font::WIDTH <= fw) else {
|
||||
return;
|
||||
};
|
||||
for (row, bits) in font::glyph(*c).iter().enumerate() {
|
||||
for col in 0..font::WIDTH {
|
||||
let on = bits & (0x80 >> col) != 0;
|
||||
let (x, y) = (ScreenCoord::new(cx + col), ScreenCoord::new(y0 + row as u16));
|
||||
self.fb.write_pixel(x, y, if on { t.fg } else { t.bg });
|
||||
// Whole cells only.
|
||||
let fit = (fw.saturating_sub(x0) / font::WIDTH) as usize;
|
||||
let chars = &t.chars()[..t.chars().len().min(fit)];
|
||||
// One run per pixel row across every cell, not a call per pixel.
|
||||
let mut line = [t.bg; TEXT_BYTES * font::WIDTH as usize];
|
||||
let line = &mut line[..chars.len() * font::WIDTH as usize];
|
||||
for row in 0..font::HEIGHT {
|
||||
for (cell, c) in line.chunks_mut(font::WIDTH as usize).zip(chars) {
|
||||
let bits = font::glyph(*c)[row as usize];
|
||||
for (col, px) in cell.iter_mut().enumerate() {
|
||||
*px = if bits & (0x80 >> col) != 0 { t.fg } else { t.bg };
|
||||
}
|
||||
}
|
||||
self.fb.write_row(t.x, ScreenCoord::new(y0 + row), line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +115,16 @@ impl<F: Framebuffer> Server for DisplayServer<F> {
|
||||
self.fb.flush();
|
||||
DisplayResponse::Ok
|
||||
}
|
||||
DisplayRequest::Scroll { top, height, by, fill } => {
|
||||
// Clipped like a rectangle: only rows on the screen move.
|
||||
let fh = self.fb.height().raw();
|
||||
if top.raw() < fh {
|
||||
let height = Dimension::new(height.raw().min(fh - top.raw()));
|
||||
let by = Dimension::new(by.raw().min(height.raw()));
|
||||
self.fb.scroll_up(top, height, by, fill);
|
||||
}
|
||||
DisplayResponse::Ok
|
||||
}
|
||||
DisplayRequest::DrawText(t) => {
|
||||
self.draw_text(&t);
|
||||
DisplayResponse::Ok
|
||||
|
||||
@@ -53,3 +53,14 @@ fn dimensions_response_roundtrip() {
|
||||
fn invalid_request_returns_none() {
|
||||
assert!(DisplayRequest::decode([99, 0, 0, 0]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_roundtrip() {
|
||||
let req = DisplayRequest::Scroll {
|
||||
top: ScreenCoord::new(0),
|
||||
height: Dimension::new(800),
|
||||
by: Dimension::new(16),
|
||||
fill: Color::from_raw(0x101018),
|
||||
};
|
||||
assert_eq!(DisplayRequest::decode(req.encode()), Some(req));
|
||||
}
|
||||
|
||||
@@ -167,3 +167,18 @@ fn text_past_the_edge_is_cut_not_wrapped() {
|
||||
server.handle(DisplayRequest::DrawText(low));
|
||||
assert_eq!(server.framebuffer().pixel_count(), 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scroll_off_the_screen_moves_nothing() {
|
||||
let mut s = test_server();
|
||||
let scroll = |top| DisplayRequest::Scroll {
|
||||
top: ScreenCoord::new(top),
|
||||
height: Dimension::new(u16::MAX),
|
||||
by: Dimension::new(u16::MAX),
|
||||
fill: Color::from_raw(0),
|
||||
};
|
||||
s.handle(scroll(u16::MAX));
|
||||
assert_eq!(s.framebuffer().scrolls(), 0);
|
||||
s.handle(scroll(0));
|
||||
assert_eq!(s.framebuffer().scrolls(), 1, "clipped, then done");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user