From c8056ddac63967c39a2d342c8ac28786ca61e467 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 22:09:33 +0100 Subject: [PATCH] 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 --- crates/apps/snake/src/main.rs | 29 +++++++---- crates/bins/display-qemu/src/main.rs | 8 +++ crates/drivers/framebuffer/mock/src/lib.rs | 11 ++++ crates/drivers/framebuffer/virtio/src/lib.rs | 52 +++++++++++++++---- crates/libkos/src/display.rs | 7 +++ crates/libkos/src/io.rs | 30 +++++++++-- crates/libkos/src/window.rs | 27 ++++++++-- crates/ports/framebuffer/src/lib.rs | 14 +++++ crates/servers/display-server/src/protocol.rs | 13 +++++ crates/servers/display-server/src/server.rs | 33 ++++++++---- .../display-server/src/tests/protocol.rs | 11 ++++ .../display-server/src/tests/server.rs | 15 ++++++ crates/servers/terminal-server/src/grid.rs | 23 +++++++- crates/servers/terminal-server/src/lib.rs | 11 +++- crates/servers/terminal-server/src/server.rs | 26 +++++++++- .../terminal-server/src/tests/server.rs | 35 ++++++++++++- crates/types/src/display.rs | 3 ++ 17 files changed, 308 insertions(+), 40 deletions(-) diff --git a/crates/apps/snake/src/main.rs b/crates/apps/snake/src/main.rs index 3058f49..d573faf 100644 --- a/crates/apps/snake/src/main.rs +++ b/crates/apps/snake/src/main.rs @@ -47,8 +47,12 @@ fn main() { libkos::println!("snake: needs the display (see /bin/snake.manifest)"); return; } - // Takes focus; the game polls every tick, so it never waits on the signal. - let _ = libkos::display::subscribe_input(display_ep()); + let Ok(alarm) = syscall::create_notification() else { + 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 commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS]; @@ -67,12 +71,12 @@ fn main() { flush(); // Any key leaves. while next_input() == Input::None { - wait_tick(); + wait_tick(alarm); } return; } - wait_tick(); + wait_tick(alarm); if handle_input(&mut snake) == Input::Quit { return; } @@ -122,10 +126,17 @@ fn handle_input(snake: &mut Snake) -> Input { } } -fn wait_tick() { - let start = syscall::get_time(); - let deadline = Ticks::from_raw(start.raw() + TICK_DELAY); - while syscall::get_time().raw() < deadline.raw() { - syscall::yield_now(); +/// Sleeps until the next tick on `alarm`, instead of spinning. Keys +/// signal it too (snake has one notification), so it waits again until +/// the tick is really due. +fn wait_tick(alarm: CapSlot) { + 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; + } } } diff --git a/crates/bins/display-qemu/src/main.rs b/crates/bins/display-qemu/src/main.rs index 7c14599..4ae2495 100644 --- a/crates/bins/display-qemu/src/main.rs +++ b/crates/bins/display-qemu/src/main.rs @@ -76,6 +76,14 @@ impl port_framebuffer::Framebuffer for Headless { } fn clear(&mut self, _: types::display::Color) {} fn flush(&mut self) {} + fn scroll_up( + &mut self, + _: types::display::ScreenCoord, + _: types::display::Dimension, + _: types::display::Dimension, + _: types::display::Color, + ) { + } } #[inline(never)] diff --git a/crates/drivers/framebuffer/mock/src/lib.rs b/crates/drivers/framebuffer/mock/src/lib.rs index c4ec33d..ec6e4de 100644 --- a/crates/drivers/framebuffer/mock/src/lib.rs +++ b/crates/drivers/framebuffer/mock/src/lib.rs @@ -10,6 +10,7 @@ pub struct MockFramebuffer { pixels: [(ScreenCoord, ScreenCoord, Color); 4096], pixel_count: usize, flushed: bool, + scrolls: usize, } impl MockFramebuffer { @@ -20,6 +21,7 @@ impl MockFramebuffer { pixels: [(ScreenCoord::new(0), ScreenCoord::new(0), Color::from_raw(0)); 4096], pixel_count: 0, flushed: false, + scrolls: 0, } } @@ -27,6 +29,11 @@ impl MockFramebuffer { self.pixel_count } + /// How many times the screen was scrolled. + pub fn scrolls(&self) -> usize { + self.scrolls + } + pub fn was_flushed(&self) -> bool { self.flushed } @@ -62,4 +69,8 @@ impl Framebuffer for MockFramebuffer { fn flush(&mut self) { self.flushed = true; } + + fn scroll_up(&mut self, _top: ScreenCoord, _height: Dimension, _by: Dimension, _fill: Color) { + self.scrolls += 1; + } } diff --git a/crates/drivers/framebuffer/virtio/src/lib.rs b/crates/drivers/framebuffer/virtio/src/lib.rs index 54063a7..4bd50ae 100644 --- a/crates/drivers/framebuffer/virtio/src/lib.rs +++ b/crates/drivers/framebuffer/virtio/src/lib.rs @@ -15,6 +15,8 @@ const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000); const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x23000); const FB_VA: VirtualAddress = VirtualAddress::from_raw(0x100000); const FB_BPP: usize = 4; +/// Pixels converted and stored per run. +const RUN: usize = 64; const RESOURCE_ID: u32 = 1; /// When the device will not say. 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 { fn width(&self) -> Dimension { self.width @@ -89,18 +97,33 @@ impl Framebuffer for VirtioFramebuffer { } fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) { - if x.raw() >= self.width.raw() || y.raw() >= self.height.raw() { - return; - } - let offset = (y.raw() as usize) * self.stride + (x.raw() as usize) * FB_BPP; - self.pixels.write(ByteOffset::new(offset), color.raw()); + self.write_row(x, y, &[color]); } - fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) { - for dy in 0..h.raw() { - for dx in 0..w.raw() { - self.write_pixel(ScreenCoord::new(x.raw() + dx), ScreenCoord::new(y.raw() + dy), color); + fn write_row(&mut self, x: ScreenCoord, y: ScreenCoord, pixels: &[Color]) { + let (w, h) = (self.width.raw() as usize, self.height.raw() as usize); + let (x, y) = (x.raw() as usize, y.raw() as usize); + 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()); } + 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) { let w = self.width.raw() as u32; let h = self.height.raw() as u32; diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 6904161..510c36f 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -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. pub const CELL_WIDTH: usize = 8; pub const CELL_HEIGHT: usize = 16; diff --git a/crates/libkos/src/io.rs b/crates/libkos/src/io.rs index ca9c79e..c5b5061 100644 --- a/crates/libkos/src/io.rs +++ b/crates/libkos/src/io.rs @@ -1,10 +1,32 @@ 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 { 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(()) } } @@ -26,7 +48,9 @@ pub fn write_bytes(bytes: &[u8]) { pub fn _print(args: fmt::Arguments) { 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] diff --git a/crates/libkos/src/window.rs b/crates/libkos/src/window.rs index ab6ee23..d5d909c 100644 --- a/crates/libkos/src/window.rs +++ b/crates/libkos/src/window.rs @@ -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) { 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 { - 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 /// threads or processes that only ever touch it atomically. pub fn atomics(&self, offset: ByteOffset, count: usize) -> &'static [AtomicUsize] { diff --git a/crates/ports/framebuffer/src/lib.rs b/crates/ports/framebuffer/src/lib.rs index ff56ed3..9c55081 100644 --- a/crates/ports/framebuffer/src/lib.rs +++ b/crates/ports/framebuffer/src/lib.rs @@ -10,4 +10,18 @@ pub trait Framebuffer { fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color); fn clear(&mut self, color: Color); 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); } diff --git a/crates/servers/display-server/src/protocol.rs b/crates/servers/display-server/src/protocol.rs index 84ea462..239f09f 100644 --- a/crates/servers/display-server/src/protocol.rs +++ b/crates/servers/display-server/src/protocol.rs @@ -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), } } diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index f12ec24..466b7d9 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -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 DisplayServer { 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 Server for DisplayServer { 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 diff --git a/crates/servers/display-server/src/tests/protocol.rs b/crates/servers/display-server/src/tests/protocol.rs index 1dd4e59..c583582 100644 --- a/crates/servers/display-server/src/tests/protocol.rs +++ b/crates/servers/display-server/src/tests/protocol.rs @@ -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)); +} diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs index 06189ee..804a808 100644 --- a/crates/servers/display-server/src/tests/server.rs +++ b/crates/servers/display-server/src/tests/server.rs @@ -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"); +} diff --git a/crates/servers/terminal-server/src/grid.rs b/crates/servers/terminal-server/src/grid.rs index d3374d9..93b655d 100644 --- a/crates/servers/terminal-server/src/grid.rs +++ b/crates/servers/terminal-server/src/grid.rs @@ -15,12 +15,15 @@ pub struct Grid { pub row: usize, /// Rows to redraw. dirty: Vec, + /// 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; } } diff --git a/crates/servers/terminal-server/src/lib.rs b/crates/servers/terminal-server/src/lib.rs index 83b00f1..481b6c4 100644 --- a/crates/servers/terminal-server/src/lib.rs +++ b/crates/servers/terminal-server/src/lib.rs @@ -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) } diff --git a/crates/servers/terminal-server/src/server.rs b/crates/servers/terminal-server/src/server.rs index 141bac4..3f778af 100644 --- a/crates/servers/terminal-server/src/server.rs +++ b/crates/servers/terminal-server/src/server.rs @@ -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 { 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 TerminalServer { 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 TerminalServer { } 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 Server for TerminalServer { 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(); diff --git a/crates/servers/terminal-server/src/tests/server.rs b/crates/servers/terminal-server/src/tests/server.rs index edea85d..b371048 100644 --- a/crates/servers/terminal-server/src/tests/server.rs +++ b/crates/servers/terminal-server/src/tests/server.rs @@ -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, } 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); +} diff --git a/crates/types/src/display.rs b/crates/types/src/display.rs index 74d0690..b8dce57 100644 --- a/crates/types/src/display.rs +++ b/crates/types/src/display.rs @@ -52,6 +52,9 @@ impl Dimension { /// `DrawText`, shared by the display server and `libkos::display`: up to /// `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]`. +/// Request tag: move a band of rows up (the display server's `Scroll`). +pub const SCROLL: usize = 7; + pub mod text { use super::{Color, ScreenCoord};