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:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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] {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user