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:
2026-10-27 22:09:33 +01:00
parent 7b8258b231
commit c8056ddac6
17 changed files with 308 additions and 40 deletions

View File

@@ -47,8 +47,12 @@ fn main() {
libkos::println!("snake: needs the display (see /bin/snake.manifest)"); libkos::println!("snake: needs the display (see /bin/snake.manifest)");
return; return;
} }
// Takes focus; the game polls every tick, so it never waits on the signal. let Ok(alarm) = syscall::create_notification() else {
let _ = libkos::display::subscribe_input(display_ep()); libkos::println!("snake: no timer");
return;
};
// Takes focus; keys are polled every tick, the signal only wakes it.
let _ = libkos::display::subscribe(display_ep(), alarm, types::badge::Badge::new(1 << 1));
let mut snake = Snake::new(); let mut snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS]; let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS];
@@ -67,12 +71,12 @@ fn main() {
flush(); flush();
// Any key leaves. // Any key leaves.
while next_input() == Input::None { while next_input() == Input::None {
wait_tick(); wait_tick(alarm);
} }
return; return;
} }
wait_tick(); wait_tick(alarm);
if handle_input(&mut snake) == Input::Quit { if handle_input(&mut snake) == Input::Quit {
return; return;
} }
@@ -122,10 +126,17 @@ fn handle_input(snake: &mut Snake) -> Input {
} }
} }
fn wait_tick() { /// Sleeps until the next tick on `alarm`, instead of spinning. Keys
let start = syscall::get_time(); /// signal it too (snake has one notification), so it waits again until
let deadline = Ticks::from_raw(start.raw() + TICK_DELAY); /// the tick is really due.
while syscall::get_time().raw() < deadline.raw() { fn wait_tick(alarm: CapSlot) {
syscall::yield_now(); let deadline = syscall::get_time().raw() + TICK_DELAY;
if syscall::set_timer(alarm, Ticks::from_raw(deadline)).is_err() {
return;
}
while syscall::get_time().raw() < deadline {
if syscall::wait(alarm).is_err() {
return;
}
} }
} }

View File

@@ -76,6 +76,14 @@ impl port_framebuffer::Framebuffer for Headless {
} }
fn clear(&mut self, _: types::display::Color) {} fn clear(&mut self, _: types::display::Color) {}
fn flush(&mut self) {} fn flush(&mut self) {}
fn scroll_up(
&mut self,
_: types::display::ScreenCoord,
_: types::display::Dimension,
_: types::display::Dimension,
_: types::display::Color,
) {
}
} }
#[inline(never)] #[inline(never)]

View File

@@ -10,6 +10,7 @@ pub struct MockFramebuffer {
pixels: [(ScreenCoord, ScreenCoord, Color); 4096], pixels: [(ScreenCoord, ScreenCoord, Color); 4096],
pixel_count: usize, pixel_count: usize,
flushed: bool, flushed: bool,
scrolls: usize,
} }
impl MockFramebuffer { impl MockFramebuffer {
@@ -20,6 +21,7 @@ impl MockFramebuffer {
pixels: [(ScreenCoord::new(0), ScreenCoord::new(0), Color::from_raw(0)); 4096], pixels: [(ScreenCoord::new(0), ScreenCoord::new(0), Color::from_raw(0)); 4096],
pixel_count: 0, pixel_count: 0,
flushed: false, flushed: false,
scrolls: 0,
} }
} }
@@ -27,6 +29,11 @@ impl MockFramebuffer {
self.pixel_count self.pixel_count
} }
/// How many times the screen was scrolled.
pub fn scrolls(&self) -> usize {
self.scrolls
}
pub fn was_flushed(&self) -> bool { pub fn was_flushed(&self) -> bool {
self.flushed self.flushed
} }
@@ -62,4 +69,8 @@ impl Framebuffer for MockFramebuffer {
fn flush(&mut self) { fn flush(&mut self) {
self.flushed = true; self.flushed = true;
} }
fn scroll_up(&mut self, _top: ScreenCoord, _height: Dimension, _by: Dimension, _fill: Color) {
self.scrolls += 1;
}
} }

View File

@@ -15,6 +15,8 @@ const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000);
const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x23000); const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x23000);
const FB_VA: VirtualAddress = VirtualAddress::from_raw(0x100000); const FB_VA: VirtualAddress = VirtualAddress::from_raw(0x100000);
const FB_BPP: usize = 4; const FB_BPP: usize = 4;
/// Pixels converted and stored per run.
const RUN: usize = 64;
const RESOURCE_ID: u32 = 1; const RESOURCE_ID: u32 = 1;
/// When the device will not say. /// When the device will not say.
const DEFAULT_SIZE: (u16, u16) = (640, 480); const DEFAULT_SIZE: (u16, u16) = (640, 480);
@@ -79,6 +81,12 @@ impl VirtioFramebuffer {
} }
} }
impl VirtioFramebuffer {
fn pixel(&self, x: usize, y: usize) -> ByteOffset {
ByteOffset::new(y * self.stride + x * FB_BPP)
}
}
impl Framebuffer for VirtioFramebuffer { impl Framebuffer for VirtioFramebuffer {
fn width(&self) -> Dimension { fn width(&self) -> Dimension {
self.width self.width
@@ -89,18 +97,33 @@ impl Framebuffer for VirtioFramebuffer {
} }
fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) { fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) {
if x.raw() >= self.width.raw() || y.raw() >= self.height.raw() { self.write_row(x, y, &[color]);
return;
}
let offset = (y.raw() as usize) * self.stride + (x.raw() as usize) * FB_BPP;
self.pixels.write(ByteOffset::new(offset), color.raw());
} }
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) { fn write_row(&mut self, x: ScreenCoord, y: ScreenCoord, pixels: &[Color]) {
for dy in 0..h.raw() { let (w, h) = (self.width.raw() as usize, self.height.raw() as usize);
for dx in 0..w.raw() { let (x, y) = (x.raw() as usize, y.raw() as usize);
self.write_pixel(ScreenCoord::new(x.raw() + dx), ScreenCoord::new(y.raw() + dy), color); if x >= w || y >= h {
return;
}
let mut run = [0u32; RUN];
let mut at = self.pixel(x, y);
for chunk in pixels[..pixels.len().min(w - x)].chunks(RUN) {
for (slot, c) in run.iter_mut().zip(chunk) {
*slot = c.raw();
} }
self.pixels.write_u32s(at, &run[..chunk.len()]);
at = at + chunk.len() * FB_BPP;
}
}
/// Callers clip to the screen (the display server does).
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) {
let (fw, fh) = (self.width.raw() as usize, self.height.raw() as usize);
let (x, y) = (x.raw() as usize, y.raw() as usize);
let w = (w.raw() as usize).min(fw.saturating_sub(x));
for row in y..(y + h.raw() as usize).min(fh) {
self.pixels.fill_u32(self.pixel(x, row), w, color.raw());
} }
} }
@@ -109,6 +132,17 @@ impl Framebuffer for VirtioFramebuffer {
self.pixels.fill_u32(ByteOffset::ZERO, total, color.raw()); self.pixels.fill_u32(ByteOffset::ZERO, total, color.raw());
} }
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let fh = self.height.raw() as usize;
let top = top.raw() as usize;
let bottom = (top + height.raw() as usize).min(fh);
let by = (by.raw() as usize).min(bottom.saturating_sub(top));
let moved = bottom - top - by;
self.pixels.copy_within(self.pixel(0, top + by), self.pixel(0, top), moved * self.stride);
let width = self.width.raw() as usize;
self.pixels.fill_u32(self.pixel(0, bottom - by), by * width, fill.raw());
}
fn flush(&mut self) { fn flush(&mut self) {
let w = self.width.raw() as u32; let w = self.width.raw() as u32;
let h = self.height.raw() as u32; let h = self.height.raw() as u32;

View File

@@ -47,6 +47,13 @@ pub fn draw_text(ep: CapSlot, x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Col
} }
} }
/// Moves the rows from `top`, `height` tall, up by `by`, filling the rows
/// left at the bottom with `fill`.
pub fn scroll(ep: CapSlot, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let band = (top.raw() as usize) | ((height.raw() as usize) << 16);
let _ = syscall::request(ep, [types::display::SCROLL, band, by.raw() as usize, fill.raw() as usize]);
}
/// The display server's character cell, in pixels. /// The display server's character cell, in pixels.
pub const CELL_WIDTH: usize = 8; pub const CELL_WIDTH: usize = 8;
pub const CELL_HEIGHT: usize = 16; pub const CELL_HEIGHT: usize = 16;

View File

@@ -1,10 +1,32 @@
use core::fmt; use core::fmt;
struct Writer; /// Holds what one `print!` formats, so it goes out in full messages:
/// formatting writes in fragments (padding a character at a time), and a
/// call per fragment is a call (and a redraw, on a terminal) per character.
struct Writer {
buf: [u8; BATCH],
len: usize,
}
/// A few whole text messages' worth.
const BATCH: usize = 5 * types::log::TEXT_BYTES;
impl Writer {
fn flush(&mut self) {
write_bytes(&self.buf[..self.len]);
self.len = 0;
}
}
impl fmt::Write for Writer { impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result { fn write_str(&mut self, s: &str) -> fmt::Result {
write_bytes(s.as_bytes()); for &b in s.as_bytes() {
if self.len == BATCH {
self.flush();
}
self.buf[self.len] = b;
self.len += 1;
}
Ok(()) Ok(())
} }
} }
@@ -26,7 +48,9 @@ pub fn write_bytes(bytes: &[u8]) {
pub fn _print(args: fmt::Arguments) { pub fn _print(args: fmt::Arguments) {
use fmt::Write; use fmt::Write;
Writer.write_fmt(args).ok(); let mut w = Writer { buf: [0; BATCH], len: 0 };
w.write_fmt(args).ok();
w.flush();
} }
#[macro_export] #[macro_export]

View File

@@ -118,15 +118,36 @@ impl Window {
} }
} }
/// `count` u32s from `offset` set to `value`. /// `count` u32s from `offset` set to `value`: one check, then a run of
/// stores (drawing fills thousands at a time).
pub fn fill_u32(&self, offset: ByteOffset, count: usize, value: u32) { pub fn fill_u32(&self, offset: ByteOffset, count: usize, value: u32) {
let bytes = count.checked_mul(4).expect("window fill too large"); let bytes = count.checked_mul(4).expect("window fill too large");
self.at(offset, bytes, 4); let addr = self.at(offset, bytes, 4) as *mut u32;
for i in 0..count { for i in 0..count {
self.write(offset + i * 4, value); // SAFETY: the whole run was checked in range and aligned.
unsafe { core::ptr::write_volatile(addr.add(i), value) }
} }
} }
/// `values` stored from `offset` on, in one checked run.
pub fn write_u32s(&self, offset: ByteOffset, values: &[u32]) {
let addr = self.at(offset, values.len() * 4, 4) as *mut u32;
for (i, v) in values.iter().enumerate() {
// SAFETY: the whole run was checked in range and aligned.
unsafe { core::ptr::write_volatile(addr.add(i), *v) }
}
}
/// Moves `len` bytes from `from` to `to` inside the window; the two may
/// overlap.
pub fn copy_within(&self, from: ByteOffset, to: ByteOffset, len: usize) {
let src = self.at(from, len, 1) as *const u8;
let dst = self.at(to, len, 1) as *mut u8;
// SAFETY: both ranges are inside the mapped window; `copy` allows
// overlap. Plain memory moves, not device registers.
unsafe { core::ptr::copy(src, dst, len) }
}
/// `count` words from `offset` as atomics, for memory shared with /// `count` words from `offset` as atomics, for memory shared with
/// threads or processes that only ever touch it atomically. /// threads or processes that only ever touch it atomically.
pub fn atomics(&self, offset: ByteOffset, count: usize) -> &'static [AtomicUsize] { pub fn atomics(&self, offset: ByteOffset, count: usize) -> &'static [AtomicUsize] {

View File

@@ -10,4 +10,18 @@ pub trait Framebuffer {
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color); fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color);
fn clear(&mut self, color: Color); fn clear(&mut self, color: Color);
fn flush(&mut self); fn flush(&mut self);
/// A run of pixels from (x, y) rightwards, clipped at the screen edge.
fn write_row(&mut self, x: ScreenCoord, y: ScreenCoord, pixels: &[Color]) {
for (i, c) in pixels.iter().enumerate() {
let Some(px) = u16::try_from(i).ok().and_then(|i| x.raw().checked_add(i)) else {
return;
};
self.write_pixel(ScreenCoord::new(px), y, *c);
}
}
/// Moves the band of rows from `top`, `height` tall, up by `by` rows,
/// and fills the `by` rows left at its bottom with `fill`.
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color);
} }

View File

@@ -9,6 +9,9 @@ pub enum DisplayRequest {
Flush, Flush,
GetDimensions, GetDimensions,
DrawText(Text), 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)] #[derive(Debug, PartialEq)]
@@ -30,6 +33,10 @@ impl Encode for DisplayRequest {
DisplayRequest::Flush => [2, 0, 0, 0], DisplayRequest::Flush => [2, 0, 0, 0],
DisplayRequest::GetDimensions => [3, 0, 0, 0], DisplayRequest::GetDimensions => [3, 0, 0, 0],
DisplayRequest::DrawText(t) => t.words(), 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) }), 1 => Some(DisplayRequest::Clear { color: Color::from_raw(words[1] as u32) }),
2 => Some(DisplayRequest::Flush), 2 => Some(DisplayRequest::Flush),
3 => Some(DisplayRequest::GetDimensions), 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), _ => Text::from_words(words).map(DisplayRequest::DrawText),
} }
} }

View File

@@ -3,7 +3,7 @@ use types::bounded::CapSlot;
use crate::protocol::{DisplayRequest, DisplayResponse}; use crate::protocol::{DisplayRequest, DisplayResponse};
use port_framebuffer::Framebuffer; use port_framebuffer::Framebuffer;
use types::display::text::Text; use types::display::text::{TEXT_BYTES, Text};
use types::display::{Dimension, ScreenCoord}; use types::display::{Dimension, ScreenCoord};
use crate::focus::Focus; use crate::focus::Focus;
@@ -38,17 +38,20 @@ impl<F: Framebuffer> DisplayServer<F> {
if y0.saturating_add(font::HEIGHT) > fh { if y0.saturating_add(font::HEIGHT) > fh {
return; return;
} }
for (i, c) in t.chars().iter().enumerate() { // Whole cells only.
let Some(cx) = x0.checked_add(i as u16 * font::WIDTH).filter(|cx| cx + font::WIDTH <= fw) else { let fit = (fw.saturating_sub(x0) / font::WIDTH) as usize;
return; let chars = &t.chars()[..t.chars().len().min(fit)];
}; // One run per pixel row across every cell, not a call per pixel.
for (row, bits) in font::glyph(*c).iter().enumerate() { let mut line = [t.bg; TEXT_BYTES * font::WIDTH as usize];
for col in 0..font::WIDTH { let line = &mut line[..chars.len() * font::WIDTH as usize];
let on = bits & (0x80 >> col) != 0; for row in 0..font::HEIGHT {
let (x, y) = (ScreenCoord::new(cx + col), ScreenCoord::new(y0 + row as u16)); for (cell, c) in line.chunks_mut(font::WIDTH as usize).zip(chars) {
self.fb.write_pixel(x, y, if on { t.fg } else { t.bg }); 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(); self.fb.flush();
DisplayResponse::Ok 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) => { DisplayRequest::DrawText(t) => {
self.draw_text(&t); self.draw_text(&t);
DisplayResponse::Ok DisplayResponse::Ok

View File

@@ -53,3 +53,14 @@ fn dimensions_response_roundtrip() {
fn invalid_request_returns_none() { fn invalid_request_returns_none() {
assert!(DisplayRequest::decode([99, 0, 0, 0]).is_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));
}

View File

@@ -167,3 +167,18 @@ fn text_past_the_edge_is_cut_not_wrapped() {
server.handle(DisplayRequest::DrawText(low)); server.handle(DisplayRequest::DrawText(low));
assert_eq!(server.framebuffer().pixel_count(), 256); 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");
}

View File

@@ -15,12 +15,15 @@ pub struct Grid {
pub row: usize, pub row: usize,
/// Rows to redraw. /// Rows to redraw.
dirty: Vec<bool>, 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 { impl Grid {
pub fn new(cols: usize, rows: usize) -> Self { pub fn new(cols: usize, rows: usize) -> Self {
let (cols, rows) = (cols.clamp(1, MAX_COLS), rows.clamp(1, MAX_ROWS)); 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] { 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)) 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) { fn mark(&mut self, r: usize) {
self.dirty[r] = true; self.dirty[r] = true;
} }
@@ -53,7 +62,16 @@ impl Grid {
self.cells.copy_within(self.cols.., 0); self.cells.copy_within(self.cols.., 0);
let last = (self.rows - 1) * self.cols; let last = (self.rows - 1) * self.cols;
self.cells[last..].fill(b' '); 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 /// Writes one byte at the cursor: `\n` moves to the next line, other
@@ -90,5 +108,6 @@ impl Grid {
self.col = 0; self.col = 0;
self.row = 0; self.row = 0;
self.dirty.fill(true); self.dirty.fill(true);
self.scrolled = 0;
} }
} }

View File

@@ -15,7 +15,7 @@ use libkos::input::{InputEvent, Key};
use libkos::syscall; use libkos::syscall;
use types::badge::Badge; use types::badge::Badge;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::display::{Color, ScreenCoord}; use types::display::{Color, Dimension, ScreenCoord};
use types::terminal::{KEYS_BIT, TIMER_BIT}; use types::terminal::{KEYS_BIT, TIMER_BIT};
/// Where init put them (its manifest: own endpoint, then `uses`). /// Where init put them (its manifest: own endpoint, then `uses`).
@@ -33,6 +33,8 @@ struct Screen {
/// `KEYS_BIT`, the flush timer with `TIMER_BIT`. /// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
notify: CapSlot, notify: CapSlot,
timer: 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. /// A key as the line editor wants it, if it is one.
@@ -65,6 +67,11 @@ impl server::Console for Screen {
display::flush(DISPLAY); 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) { fn flush_later(&mut self) {
let at = syscall::get_time().raw() + FLUSH_DELAY; let at = syscall::get_time().raw() + FLUSH_DELAY;
let _ = syscall::set_timer(self.timer, types::time::Ticks::from_raw(at)); let _ = syscall::set_timer(self.timer, types::time::Ticks::from_raw(at));
@@ -116,6 +123,6 @@ pub fn run() -> ! {
display::clear(DISPLAY, BG); display::clear(DISPLAY, BG);
let cols = w.raw() as usize / CELL_WIDTH; let cols = w.raw() as usize / CELL_WIDTH;
let rows = h.raw() as usize / CELL_HEIGHT; 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) server_framework::serve(&mut srv, EP)
} }

View File

@@ -13,6 +13,8 @@ pub trait Console {
/// Shows the cursor at a cell, over what `draw_row` drew there. /// Shows the cursor at a cell, over what `draw_row` drew there.
fn draw_cursor(&mut self, col: usize, row: usize, under: u8); fn draw_cursor(&mut self, col: usize, row: usize, under: u8);
fn flush(&mut self); 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. /// Soon calls `TerminalServer::signal`, to flush a burst of output once.
fn flush_later(&mut self); fn flush_later(&mut self);
/// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace. /// 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_len: usize,
line_read: usize, line_read: usize,
flush_pending: bool, flush_pending: bool,
/// How many cells of each row are drawn on the screen.
drawn: [usize; crate::grid::MAX_ROWS],
typeahead: [u8; TYPEAHEAD], typeahead: [u8; TYPEAHEAD],
typeahead_len: usize, typeahead_len: usize,
/// Told about Ctrl+C typed while no line is being read. /// Told about Ctrl+C typed while no line is being read.
@@ -61,6 +65,7 @@ impl<C: Console> TerminalServer<C> {
line_len: 0, line_len: 0,
line_read: 0, line_read: 0,
flush_pending: false, flush_pending: false,
drawn: [0; crate::grid::MAX_ROWS],
typeahead: [0; TYPEAHEAD], typeahead: [0; TYPEAHEAD],
typeahead_len: 0, typeahead_len: 0,
on_interrupt: None, on_interrupt: None,
@@ -84,16 +89,33 @@ impl<C: Console> TerminalServer<C> {
} }
fn redraw(&mut self) { 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]; let mut dirty = [false; crate::grid::MAX_ROWS];
for r in self.grid.take_dirty() { for r in self.grid.take_dirty() {
dirty[r] = true; dirty[r] = true;
} }
for (r, _) in dirty.iter().enumerate().filter(|(_, d)| **d) { 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 (col, row) = (self.grid.col, self.grid.row);
let under = self.grid.row(row).get(col).copied().unwrap_or(b' '); let under = self.grid.row(row).get(col).copied().unwrap_or(b' ');
self.console.draw_cursor(col, row, under); 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]) { fn write(&mut self, bytes: &[u8]) {
@@ -210,7 +232,9 @@ impl<C: Console> Server for TerminalServer<C> {
let reply = match words[0] { let reply = match words[0] {
types::terminal::READ => self.read(), types::terminal::READ => self.read(),
types::terminal::REDRAW => { types::terminal::REDRAW => {
// Taking focus back clears the screen.
self.console.take_focus(); self.console.take_focus();
self.drawn = [0; crate::grid::MAX_ROWS];
self.grid.touch_all(); self.grid.touch_all();
self.redraw(); self.redraw();
self.console.flush(); self.console.flush();

View File

@@ -16,15 +16,23 @@ struct Fake {
flushes: usize, flushes: usize,
timers: usize, timers: usize,
focus_taken: usize, focus_taken: usize,
scrolled: usize,
/// Every row drawn, with how many cells.
rows_drawn: Vec<(usize, usize)>,
interrupted: Vec<CapSlot>, interrupted: Vec<CapSlot>,
} }
impl Console for Fake { 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 draw_cursor(&mut self, _col: usize, _row: usize, _under: u8) {}
fn flush(&mut self) { fn flush(&mut self) {
self.flushes += 1; self.flushes += 1;
} }
fn scroll(&mut self, rows: usize) {
self.scrolled += rows;
}
fn flush_later(&mut self) { fn flush_later(&mut self) {
self.timers += 1; 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.console().focus_taken, 1);
assert_eq!(&t.grid().row(0)[..2], b"hi"); 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);
}

View File

@@ -52,6 +52,9 @@ impl Dimension {
/// `DrawText`, shared by the display server and `libkos::display`: up to /// `DrawText`, shared by the display server and `libkos::display`: up to
/// `TEXT_BYTES` characters at a pixel position, each cell `fg` on `bg`. /// `TEXT_BYTES` characters at a pixel position, each cell `fg` on `bg`.
/// Words: `[TEXT | len << 8 | fg << 16, x | y << 16 | bg << 32, bytes 0..8, 8..16]`. /// Words: `[TEXT | len << 8 | fg << 16, x | y << 16 | bg << 32, bytes 0..8, 8..16]`.
/// Request tag: move a band of rows up (the display server's `Scroll`).
pub const SCROLL: usize = 7;
pub mod text { pub mod text {
use super::{Color, ScreenCoord}; use super::{Color, ScreenCoord};