feat(display): windows: full-screen, F1 cycles, OS bar (names, front, user); canvas-only (display-owned buffers, 4 max, reused); launchers OPEN (pm per app, terminal own), window caps minted by display; FORGET on launcher death via init courier; raster crate (font, chunked text); F-keys in input; terminal+snake on canvas; REDRAW gone

This commit is contained in:
2026-10-28 02:48:39 +01:00
parent d04dcfffa1
commit 864f039bc7
45 changed files with 1382 additions and 824 deletions

16
Cargo.lock generated
View File

@@ -207,6 +207,7 @@ dependencies = [
"driver-framebuffer-mock",
"libkos",
"port-framebuffer",
"raster",
"server-framework",
"types",
]
@@ -425,6 +426,7 @@ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
name = "libkos"
version = "0.1.0"
dependencies = [
"port-framebuffer",
"types",
]
@@ -526,6 +528,15 @@ version = "0.1.0"
name = "print-server"
version = "0.1.0"
[[package]]
name = "raster"
version = "0.1.0"
dependencies = [
"driver-framebuffer-mock",
"port-framebuffer",
"types",
]
[[package]]
name = "server-framework"
version = "0.1.0"
@@ -572,9 +583,8 @@ dependencies = [
name = "snake"
version = "0.1.0"
dependencies = [
"display-server",
"libkos",
"server-framework",
"port-framebuffer",
"types",
]
@@ -627,6 +637,8 @@ name = "terminal-server"
version = "0.1.0"
dependencies = [
"libkos",
"port-framebuffer",
"raster",
"server-framework",
"types",
]

View File

@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/adapter-riscv",
"crates/raster",
"crates/apps/flaky",
"crates/apps/hello",
"crates/apps/shell",
@@ -127,6 +128,7 @@ libkos = { path = "crates/libkos" }
# ports
port-framebuffer = { path = "crates/ports/framebuffer" }
raster = { path = "crates/raster" }
port-block-device = { path = "crates/ports/block-device" }
port-input-device = { path = "crates/ports/input-device" }

View File

@@ -145,7 +145,7 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
// A Ctrl+C typed since the last program is not for this one.
let _ = syscall::poll(sh.done);
match sh.apps.launch(args, Some(TERMINAL), folder) {
Ok((process, granted)) => {
Ok((process, _granted)) => {
if syscall::watch_death(process, sh.done).is_ok() {
while syscall::wait(sh.done).is_ok_and(|bits| {
if bits & INTERRUPTED != 0 {
@@ -157,10 +157,6 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
let _ = syscall::kill(process);
}
syscall::drop_cap(process);
// It drew over the terminal and took the keyboard.
if Capabilities::from_bits(granted).contains(Capability::Display) {
libkos::terminal::redraw(TERMINAL);
}
}
Err(r) => refused(args[0], r),
}

View File

@@ -12,5 +12,4 @@ bench = false
types = { workspace = true }
libkos = { workspace = true }
display-server = { workspace = true }
server-framework = { workspace = true }
port-framebuffer = { workspace = true }

View File

@@ -4,57 +4,40 @@
extern crate libkos;
use libkos::display::Canvas;
use libkos::syscall;
use port_framebuffer::Framebuffer;
use snake::game::{Direction, GameState, Snake};
use snake::renderer::{self, RenderCommand};
use types::bounded::CapSlot;
use types::display::{Color, Dimension, ScreenCoord};
use types::time::Ticks;
use libkos::syscall;
use server_framework::Encode;
use display_server::protocol::DisplayRequest;
/// Steps a second; QEMU virt's timebase is 10 MHz.
const STEPS_PER_SECOND: u64 = 8;
const TICK_DELAY: u64 = 10_000_000 / STEPS_PER_SECOND;
const MAX_RENDER_CMDS: usize = 140;
/// The display server, which the shell hands over because snake's manifest
/// asks for it. Input comes through it too, while snake has focus.
fn display_ep() -> CapSlot {
libkos::start::display().unwrap_or(CapSlot::const_new(0))
}
fn display_send(req: &DisplayRequest) {
let _ = syscall::request(display_ep(), req.encode());
}
fn draw_rect(x: u16, y: u16, w: u16, h: u16, color: u32) {
display_send(&DisplayRequest::DrawRect {
x: ScreenCoord::new(x),
y: ScreenCoord::new(y),
w: Dimension::new(w),
h: Dimension::new(h),
color: Color::from_raw(color),
});
}
fn flush() {
display_send(&DisplayRequest::Flush);
fn draw_rect(canvas: &mut Canvas, x: u16, y: u16, w: u16, h: u16, color: u32) {
canvas.fill_rect(ScreenCoord::new(x), ScreenCoord::new(y), Dimension::new(w), Dimension::new(h), Color::from_raw(color));
}
libkos::entry!(main);
fn main() {
if libkos::start::display().is_none() {
libkos::println!("snake: needs the display (see /bin/snake.manifest)");
// Its window, which the permission manager opened because snake's
// manifest asks for the display. Input comes through it too.
let Some(mut canvas) = libkos::start::display().and_then(Canvas::attach) else {
libkos::println!("snake: needs the display (see its manifest)");
return;
}
};
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 window = canvas.window();
let _ = libkos::display::subscribe(window, alarm, types::badge::Badge::new(1 << 1));
let mut clock = Clock::new(alarm);
let mut snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS];
@@ -62,24 +45,24 @@ fn main() {
loop {
let count = renderer::render(&snake, &mut commands);
for cmd in &commands[..count] {
draw_rect(cmd.x, cmd.y, cmd.w, cmd.h, cmd.color);
draw_rect(&mut canvas, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color);
}
flush();
canvas.flush();
if snake.state() == GameState::GameOver {
let gw = renderer::grid_pixel_width();
let gh = renderer::grid_pixel_height();
draw_rect(gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
flush();
draw_rect(&mut canvas, gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
canvas.flush();
// Any key leaves.
while next_input() == Input::None {
while next_input(window) == Input::None {
clock.wait_tick();
}
return;
}
clock.wait_tick();
if handle_input(&mut snake) == Input::Quit {
if handle_input(window, &mut snake) == Input::Quit {
return;
}
snake.tick();
@@ -95,8 +78,8 @@ enum Input {
}
/// The next key, if one is waiting: q or Escape quits.
fn next_input() -> Input {
let Ok([tag, kind, ch, _]) = syscall::request(display_ep(), [types::display::input::POLL, 0, 0, 0]) else {
fn next_input(window: CapSlot) -> Input {
let Ok([tag, kind, ch, _]) = syscall::request(window, [types::display::window::POLL, 0, 0, 0]) else {
return Input::None;
};
if tag == 0 {
@@ -117,9 +100,9 @@ fn next_input() -> Input {
}
/// Drains the pending keys, applying each turn; reports a quit.
fn handle_input(snake: &mut Snake) -> Input {
fn handle_input(window: CapSlot, snake: &mut Snake) -> Input {
loop {
match next_input() {
match next_input(window) {
Input::None => return Input::None,
Input::Quit => return Input::Quit,
Input::Turn(d) => snake.set_direction(d),

View File

@@ -37,18 +37,12 @@ fn subscribe_to_input() -> Option<CapSlot> {
sent.ok().map(|_| INPUT)
}
fn server<F: port_framebuffer::Framebuffer>(fb: F, input: Option<CapSlot>) -> display_server::server::DisplayServer<F> {
match input {
Some(input) => display_server::server::DisplayServer::with_input(fb, input),
None => display_server::server::DisplayServer::new(fb),
}
fn server<F: port_framebuffer::Framebuffer>(fb: F, input: Option<CapSlot>) -> display_server::server::DisplayServer<F, display_server::host::KernelHost> {
display_server::server::DisplayServer::new(fb, display_server::host::KernelHost::new(EP, input))
}
#[inline(never)]
fn run_virtio(mut fb: driver_framebuffer_virtio::VirtioFramebuffer, input: Option<CapSlot>) -> ! {
use port_framebuffer::Framebuffer;
fb.clear(types::display::Color::rgb(30, 30, 60));
fb.flush();
fn run_virtio(fb: driver_framebuffer_virtio::VirtioFramebuffer, input: Option<CapSlot>) -> ! {
let mut srv = server(fb, input);
server_framework::serve(&mut srv, EP)
}

View File

@@ -74,3 +74,67 @@ impl Framebuffer for MockFramebuffer {
self.scrolls += 1;
}
}
/// A screen of `W` by `H` pixels that keeps what is drawn, for tests that
/// look at the picture rather than the calls.
pub struct GridFramebuffer<const W: usize, const H: usize> {
pixels: [[Color; W]; H],
pub flushes: usize,
}
impl<const W: usize, const H: usize> GridFramebuffer<W, H> {
pub fn new() -> Self {
Self { pixels: [[Color::from_raw(0); W]; H], flushes: 0 }
}
/// The pixel at (x, y), `None` off the screen.
pub fn pixel(&self, x: usize, y: usize) -> Option<Color> {
self.pixels.get(y).and_then(|row| row.get(x)).copied()
}
}
impl<const W: usize, const H: usize> Default for GridFramebuffer<W, H> {
fn default() -> Self {
Self::new()
}
}
impl<const W: usize, const H: usize> Framebuffer for GridFramebuffer<W, H> {
fn width(&self) -> Dimension {
Dimension::new(W as u16)
}
fn height(&self) -> Dimension {
Dimension::new(H as u16)
}
fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) {
if let Some(px) = self.pixels.get_mut(y.raw() as usize).and_then(|r| r.get_mut(x.raw() as usize)) {
*px = color;
}
}
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) {
for row in y.raw()..y.raw().saturating_add(h.raw()) {
for col in x.raw()..x.raw().saturating_add(w.raw()) {
self.write_pixel(ScreenCoord::new(col), ScreenCoord::new(row), color);
}
}
}
fn clear(&mut self, color: Color) {
self.pixels = [[color; W]; H];
}
fn flush(&mut self) {
self.flushes += 1;
}
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let (top, height, by) = (top.raw() as usize, height.raw() as usize, by.raw() as usize);
let end = (top + height).min(H);
for y in top..end {
self.pixels[y] = if y + by < end { self.pixels[y + by] } else { [fill; W] };
}
}
}

View File

@@ -37,6 +37,8 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
105 => Some(Key::Left),
106 => Some(Key::Right),
43 => Some(Key::Char(if shift { b'|' } else { b'\\' })),
59..=68 => Some(Key::Function((code - 58) as u8)),
87 | 88 => Some(Key::Function((code - 76) as u8)),
_ => None,
};
named.or_else(|| {
@@ -52,6 +54,13 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
mod tests {
use super::*;
#[test]
fn function_keys_are_numbered() {
assert_eq!(key(59, false), Some(Key::Function(1)));
assert_eq!(key(68, false), Some(Key::Function(10)));
assert_eq!(key(88, true), Some(Key::Function(12)));
}
#[test]
fn ctrl_letters_are_control_characters() {
assert_eq!(with_ctrl(Key::Char(b'c')), Key::Char(3));

View File

@@ -63,6 +63,9 @@ pub(crate) struct Launched {
/// to take back when it dies.
pub(crate) grants: [Option<CapSlot>; 2],
pub(crate) spawn: Option<Badge>,
/// The badge on its copy of the display, if it has one: the windows it
/// opened close when it goes.
pub(crate) display_badge: Option<usize>,
}
impl<'a> Launcher<'a> {
@@ -101,7 +104,7 @@ impl<'a> Launcher<'a> {
(self.storage, self.scratch)
}
fn endpoint(&self, name: &str) -> Option<CapSlot> {
pub(crate) fn endpoint(&self, name: &str) -> Option<CapSlot> {
self.served.iter().flatten().find(|(n, _)| *n == name).map(|(_, ep)| *ep)
}
@@ -117,8 +120,10 @@ impl<'a> Launcher<'a> {
Some(ep) => ep,
None => syscall::create_endpoint().ok_or("no endpoint")?,
};
// Read to receive on it; init keeps the original.
let server = perms(&[Permission::Read, Permission::Execute]);
// Read to receive on it; Delegate to mint badged copies of
// its own (storage's grants, the display's windows); init
// keeps the original.
let server = perms(&[Permission::Read, Permission::Execute, Permission::Delegate]);
syscall::delegate(ep, child, server).ok_or("cannot give its endpoint")?;
Some(ep)
}
@@ -127,10 +132,14 @@ impl<'a> Launcher<'a> {
// A client calls (Execute) and may send capabilities through it and
// pass it on (Delegate); never Read, so it cannot pose as the server
// and take other clients' requests.
let mut display_badge = None;
for name in p.uses {
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
let badge = Badge::new(self.next_badge);
self.next_badge += 1;
if *name == "display" {
display_badge = Some(badge.raw());
}
let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
.map_err(|_| "cannot mint an endpoint")?;
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
@@ -192,7 +201,7 @@ impl<'a> Launcher<'a> {
note = ", console handed over";
}
}
Ok(Launched { note, process: child, ep, grants, spawn })
Ok(Launched { note, process: child, ep, grants, spawn, display_badge })
}
/// The path on disk `scope` names: inside the user's folder if it is a

View File

@@ -103,12 +103,16 @@ pub const SERVERS: &[Program] = &[
uses: &["input"],
// Its clients subscribe and take focus.
restarts_clients: true,
// The framebuffer alone is ~1000 pages at 1280x800.
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
// Its windows' state (input queues among it) is KiBs.
stack_pages: 8,
// The framebuffer is ~1000 pages at 1280x800, and so is each of
// the (at most 4) windows' pixel buffers, which it owns.
..server("display", b"bin/display", Limits { pages: 5200, regions: 8, ..SMALL })
},
// Text on the display, lines from the keyboard (it takes input focus).
// Its clients wait on lines and Ctrl+C.
Program { uses: &["display"], restarts_clients: true, ..server("terminal", b"bin/terminal", SMALL) },
// Its clients wait on lines and Ctrl+C. It draws its own text now,
// beside a screenful of grid.
Program { uses: &["display"], restarts_clients: true, stack_pages: 8, ..server("terminal", b"bin/terminal", SMALL) },
server("session", b"bin/session", SMALL),
];

View File

@@ -2,7 +2,8 @@
//! server, so a blocking call to it could wait on itself. Text goes into a
//! ring; a thread of init's own takes it to the log and waits there when
//! the log is down, instead of init. Until that thread runs (early boot),
//! init prints to the console directly.
//! init prints to the console directly. The same thread carries the few
//! messages init sends other servers it restarts (`post`).
use core::fmt;
use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
@@ -20,6 +21,31 @@ static LOST: AtomicUsize = AtomicUsize::new(0);
/// The printer's notification, once it runs.
static WAKE: AtomicUsize = AtomicUsize::new(usize::MAX);
/// Messages waiting: an endpoint slot and four words each.
const POSTS: usize = 16;
static POSTED: [[AtomicUsize; 5]; POSTS] = [const { [const { AtomicUsize::new(0) }; 5] }; POSTS];
static POST_HEAD: AtomicUsize = AtomicUsize::new(0);
static POST_TAIL: AtomicUsize = AtomicUsize::new(0);
/// Has `words` called to `ep` by the printer thread, so init never waits
/// on it; dropped (and said so) if too many are waiting.
pub(crate) fn post(ep: CapSlot, words: [usize; 4]) {
let tail = POST_TAIL.load(Ordering::Relaxed);
if tail - POST_HEAD.load(Ordering::Acquire) == POSTS || wake().is_none() {
crate::outln!("init: a message to a server was dropped");
return;
}
let slot = &POSTED[tail % POSTS];
slot[0].store(ep.value() as usize, Ordering::Relaxed);
for (w, v) in slot[1..].iter().zip(words) {
w.store(v, Ordering::Relaxed);
}
POST_TAIL.store(tail + 1, Ordering::Release);
if let Some(wake) = wake() {
let _ = syscall::signal(wake);
}
}
fn wake() -> Option<CapSlot> {
u8::try_from(WAKE.load(Ordering::Acquire)).ok().and_then(CapSlot::new)
}
@@ -87,6 +113,17 @@ fn printer(_: &'static AtomicUsize) -> ! {
if lost > 0 {
libkos::println!("init: {} bytes of output lost while the log was down", lost);
}
while POST_HEAD.load(Ordering::Relaxed) != POST_TAIL.load(Ordering::Acquire) {
let head = POST_HEAD.load(Ordering::Relaxed);
let slot = &POSTED[head % POSTS];
let words = core::array::from_fn(|i| slot[i + 1].load(Ordering::Relaxed));
let ep = u8::try_from(slot[0].load(Ordering::Relaxed)).ok().and_then(CapSlot::new);
POST_HEAD.store(head + 1, Ordering::Release);
if let Some(ep) = ep {
// Waits while that server is down; init goes on.
let _ = syscall::request(ep, words);
}
}
}
}

View File

@@ -108,6 +108,9 @@ pub(crate) fn sign_in(l: &mut Launcher) -> Result<(), &'static str> {
crate::factory::prepare(fs, folder, scratch)?;
let name = core::str::from_utf8(&h.name[..h.name_len]).unwrap_or("someone");
crate::outln!("init: {} is in", name);
if let Some(display) = l.endpoint("display") {
crate::output::post(display, libkos::display::set_user_words(name.as_bytes()));
}
l.user = Some(User { folder, guest });
start_session(l);
Ok(())
@@ -129,6 +132,9 @@ fn start_session(l: &mut Launcher) {
pub(crate) fn sign_out(l: &mut Launcher) {
l.spawner.clear();
l.stop_session();
if let Some(display) = l.endpoint("display") {
crate::output::post(display, libkos::display::set_user_words(b""));
}
let user = l.user.take();
let (fs, _) = l.storage();
let locked = fs.lock().is_ok();

View File

@@ -83,14 +83,23 @@ pub(crate) struct Supervised {
ep: Option<CapSlot>,
/// Killed for hanging: the watchdog's ping is queued on its endpoint.
hung: bool,
/// The badge on its copy of the display: its windows close with it.
display_badge: Option<usize>,
}
/// What a program held through init, to take back when it goes.
struct Held {
grants: [Option<CapSlot>; 2],
spawn: Option<Badge>,
display_badge: Option<usize>,
}
impl Supervised {
/// What this run held through init, the first time only: by the time
/// its death is heard, a slot it freed may hold someone else's
/// capability.
fn take_held(&mut self) -> Option<([Option<CapSlot>; 2], Option<Badge>)> {
let held = (self.grants, self.spawn);
fn take_held(&mut self) -> Option<Held> {
let held = Held { grants: self.grants, spawn: self.spawn, display_badge: self.display_badge };
self.grants = [None; 2];
self.spawn = None;
core::mem::take(&mut self.holding).then_some(held)
@@ -128,6 +137,7 @@ impl Launcher<'_> {
holding: true,
ep,
hung: false,
display_badge: started.display_badge,
});
self.ping(index);
Ok(note)
@@ -168,8 +178,12 @@ impl Launcher<'_> {
/// Takes back what supervised program `index` held through init.
fn let_go(&mut self, index: usize) {
// Never a call to a server: it may be the one that is down.
let Some((grants, spawn)) = self.supervised[index].as_mut().and_then(Supervised::take_held) else { return };
self.release(grants, spawn);
let Some(held) = self.supervised[index].as_mut().and_then(Supervised::take_held) else { return };
self.release(held.grants, held.spawn);
// The windows it opened (the terminal's, its apps') close.
if let (Some(badge), Some(display)) = (held.display_badge, self.endpoint("display")) {
crate::output::post(display, libkos::display::forget_words(badge));
}
}
/// Has the watchdog ping program `index`, if it is to be pinged.
@@ -306,6 +320,7 @@ impl Launcher<'_> {
spawn: started.spawn,
holding: true,
ep: started.ep,
display_badge: started.display_badge,
..s
});
self.ping(index);

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
types = { workspace = true }
port-framebuffer = { workspace = true }

View File

@@ -1,86 +1,188 @@
//! The window protocol's client (`types::display::window`). A launcher
//! opens a window for an app and passes it on; the app maps the window's
//! pixel buffer as a `Canvas`, draws into it (`raster` draws text) and
//! presents what changed; the display shows it while the window is in
//! front.
use crate::syscall;
use crate::window::Window;
use types::address::{ByteOffset, VirtualAddress};
use types::bounded::CapSlot;
use types::display::window::{self as w, PIXEL_BYTES};
use types::display::{Color, Dimension, ScreenCoord};
use types::mapping::PagePerms;
/// A failed call reads as an error reply (tag `usize::MAX`).
fn ipc(ep: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> (usize, usize, usize, usize) {
let r = syscall::request(ep, [w0, w1, w2, w3]).unwrap_or([usize::MAX, 0, 0, 0]);
(r[0], r[1], r[2], r[3])
/// Where an app's canvas is mapped.
const CANVAS_VA: VirtualAddress = VirtualAddress::from_raw(0x0100_0000);
/// A window just opened: its capability, to pass on, and its size.
pub struct Opened {
pub window: CapSlot,
pub width: Dimension,
pub height: Dimension,
}
pub fn draw_rect(
ep: CapSlot,
x: ScreenCoord,
y: ScreenCoord,
w: Dimension,
h: Dimension,
color: Color,
) {
let xy = (x.raw() as usize) | ((y.raw() as usize) << 16);
let wh = (w.raw() as usize) | ((h.raw() as usize) << 16);
ipc(ep, 0, xy, wh, color.raw() as usize);
}
pub fn clear(ep: CapSlot, color: Color) {
ipc(ep, 1, color.raw() as usize, 0, 0);
}
pub fn flush(ep: CapSlot) {
ipc(ep, 2, 0, 0, 0);
}
pub fn get_dimensions(ep: CapSlot) -> (Dimension, Dimension) {
let (tag, w, h, _) = ipc(ep, 3, 0, 0, 0);
if tag == 1 {
(Dimension::new(w as u16), Dimension::new(h as u16))
} else {
(Dimension::new(0), Dimension::new(0))
/// A launcher opens a window named `name` (what the OS bar shows).
pub fn open(display: CapSlot, name: &[u8]) -> Option<Opened> {
let n = w::pack_name(name);
match syscall::call(display, [w::OPEN, n[0], n[1], n[2]], None).ok()? {
syscall::Received::Message { words: [w::OK, width, height, _], cap: Some(window), .. } => {
Some(Opened { window, width: Dimension::new(width as u16), height: Dimension::new(height as u16) })
}
_ => None,
}
}
/// Draws `s` (up to `TEXT_BYTES` per call, more in several) from `(x, y)`.
pub fn draw_text(ep: CapSlot, x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Color, s: &[u8]) {
use types::display::text::{TEXT_BYTES, Text};
for (i, chunk) in s.chunks(TEXT_BYTES).enumerate() {
let x = ScreenCoord::new(x.raw().saturating_add((i * TEXT_BYTES * CELL_WIDTH) as u16));
let _ = syscall::request(ep, Text::new(x, y, fg, bg, chunk).words());
}
/// Closes `window`: every copy of it, and its buffer, stop working.
pub fn close(window: CapSlot) {
let _ = syscall::request(window, [w::CLOSE, 0, 0, 0]);
}
/// 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;
/// Takes input focus: returns a notification signalled whenever events
/// arrive; read them with `poll_input`.
pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
use types::badge::Badge;
let notify = syscall::create_notification().ok()?;
subscribe(ep, notify, Badge::NONE).then_some(notify)
}
/// Takes input focus, to be told through `notify` with `badge`'s bits.
pub fn subscribe(ep: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
/// Takes input for `window`: `notify` is signalled with `badge`'s bits
/// when some arrives while it is in front; read it with `poll_input`.
pub fn subscribe(window: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
use types::permission::{Permission, PermissionSet};
// Signal only (Delegate just to send it; the transfer strips it).
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let Ok(copy) = syscall::mint(notify, badge, signal) else {
return false;
};
let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
let sent = syscall::call(window, [w::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
syscall::drop_cap(copy);
sent.is_ok_and(|r| r.words()[0] == 0)
sent.is_ok_and(|r| r.words()[0] == w::OK)
}
/// The next input event, if this process has focus and one is waiting.
pub fn poll_input(ep: CapSlot) -> Option<crate::input::InputEvent> {
let words = syscall::request(ep, [types::display::input::POLL, 0, 0, 0]).ok()?;
/// `window`'s next input event, if one is waiting.
pub fn poll_input(window: CapSlot) -> Option<crate::input::InputEvent> {
let words = syscall::request(window, [w::POLL, 0, 0, 0]).ok()?;
crate::input::decode(words)
}
/// A changed area, as corners.
#[derive(Clone, Copy)]
struct Damage {
x0: u16,
y0: u16,
x1: u16,
y1: u16,
}
/// A window's pixels, mapped here. Draw with the `Framebuffer` methods (and
/// `raster`); `flush` presents everything drawn since the last one.
pub struct Canvas {
window: CapSlot,
pixels: Window,
width: Dimension,
height: Dimension,
damage: Option<Damage>,
}
impl Canvas {
/// Maps `window`'s buffer.
pub fn attach(window: CapSlot) -> Option<Self> {
let (words, buffer) = match syscall::call(window, [w::BUFFER, 0, 0, 0], None).ok()? {
syscall::Received::Message { words, cap: Some(buffer), .. } if words[0] == w::OK => (words, buffer),
_ => return None,
};
let pixels = crate::window::map(buffer, CANVAS_VA, PagePerms::ReadWrite).ok()?;
let (width, height) = (Dimension::new(words[1] as u16), Dimension::new(words[2] as u16));
// The display says how big; the mapping says how much is there.
(width.raw() as usize * height.raw() as usize * PIXEL_BYTES <= pixels.len())
.then_some(Self { window, pixels, width, height, damage: None })
}
pub fn window(&self) -> CapSlot {
self.window
}
fn offset(&self, x: u16, y: u16) -> ByteOffset {
ByteOffset::new((y as usize * self.width.raw() as usize + x as usize) * PIXEL_BYTES)
}
/// Marks `x..x+w`, `y..y+h` (already clipped) as changed.
fn touch(&mut self, x: u16, y: u16, w: u16, h: u16) {
let (x1, y1) = (x + w, y + h);
self.damage = Some(match self.damage {
None => Damage { x0: x, y0: y, x1, y1 },
Some(d) => Damage { x0: d.x0.min(x), y0: d.y0.min(y), x1: d.x1.max(x1), y1: d.y1.max(y1) },
});
}
/// The part of a rectangle inside the canvas, if any.
fn clip(&self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension) -> Option<(u16, u16, u16, u16)> {
let (cw, ch) = (self.width.raw(), self.height.raw());
let (x, y) = (x.raw(), y.raw());
(x < cw && y < ch).then(|| (x, y, w.raw().min(cw - x), h.raw().min(ch - y)))
}
}
impl port_framebuffer::Framebuffer for Canvas {
fn width(&self) -> Dimension {
self.width
}
fn height(&self) -> Dimension {
self.height
}
fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) {
if let Some((x, y, _, _)) = self.clip(x, y, Dimension::new(1), Dimension::new(1)) {
self.pixels.write(self.offset(x, y), color.raw());
self.touch(x, y, 1, 1);
}
}
fn write_row(&mut self, x: ScreenCoord, y: ScreenCoord, row: &[Color]) {
let Some((x, y, w, _)) = self.clip(x, y, Dimension::new(row.len().min(u16::MAX as usize) as u16), Dimension::new(1)) else { return };
let mut run = [0u32; 64];
for (i, chunk) in row[..w as usize].chunks(run.len()).enumerate() {
for (slot, c) in run.iter_mut().zip(chunk) {
*slot = c.raw();
}
self.pixels.write_u32s(self.offset(x + (i * 64) as u16, y), &run[..chunk.len()]);
}
self.touch(x, y, w, 1);
}
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) {
let Some((x, y, w, h)) = self.clip(x, y, w, h) else { return };
for row in y..y + h {
self.pixels.fill_u32(self.offset(x, row), w as usize, color.raw());
}
self.touch(x, y, w, h);
}
fn clear(&mut self, color: Color) {
let (w, h) = (self.width, self.height);
self.fill_rect(ScreenCoord::new(0), ScreenCoord::new(0), w, h, color);
}
/// Presents what changed since the last flush.
fn flush(&mut self) {
let Some(d) = self.damage.take() else { return };
let rect = w::pack_rect(ScreenCoord::new(d.x0), ScreenCoord::new(d.y0), Dimension::new(d.x1 - d.x0), Dimension::new(d.y1 - d.y0));
let _ = syscall::request(self.window, [w::PRESENT, rect[0], rect[1], 0]);
}
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let Some((_, top, _, height)) = self.clip(ScreenCoord::new(0), top, self.width, height) else { return };
let by = by.raw().min(height);
let row = self.width.raw() as usize * PIXEL_BYTES;
let kept = (height - by) as usize * row;
self.pixels.copy_within(self.offset(0, top + by), self.offset(0, top), kept);
let width = self.width;
self.fill_rect(ScreenCoord::new(0), ScreenCoord::new(top + height - by), width, Dimension::new(by), fill);
self.touch(0, top, width.raw(), height);
}
}
/// From init only: who is signed in, shown in the OS bar.
pub fn set_user_words(name: &[u8]) -> [usize; 4] {
let n = w::pack_name(name);
[w::SET_USER, n[0], n[1], n[2]]
}
/// From init only: the launcher behind `badge` is gone, and its windows.
pub fn forget_words(badge: usize) -> [usize; 4] {
[w::FORGET, badge, 0, 0]
}

View File

@@ -13,6 +13,8 @@ pub enum Key {
Backspace,
Tab,
Char(u8),
/// F1 is 1.
Function(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -33,6 +35,7 @@ fn decode_key(kind: usize, extra: usize) -> Option<Key> {
7 => Some(Key::Backspace),
8 => Some(Key::Tab),
9 => Some(Key::Char(extra as u8)),
types::input::event::FUNCTION_KEY => Some(Key::Function(extra as u8)),
_ => None,
}
}

View File

@@ -37,12 +37,6 @@ pub fn clear(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::CLEAR, 0, 0, 0]);
}
/// Repaints the terminal and gives it input focus again, after a program
/// that drew on the screen.
pub fn redraw(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::REDRAW, 0, 0, 0]);
}
/// Has the terminal signal `notify` with `badge`'s bits when Ctrl+C is
/// typed while no line is being read. Only the first caller is heard.
pub fn on_interrupt(terminal: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {

View File

@@ -13,6 +13,8 @@ pub enum Key {
Backspace,
Tab,
Char(u8),
/// F1 is 1.
Function(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

11
crates/raster/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "raster"
version = "0.1.0"
edition = "2024"
[dependencies]
types = { workspace = true }
port-framebuffer = { workspace = true }
[dev-dependencies]
driver-framebuffer-mock = { workspace = true }

View File

@@ -1,4 +1,4 @@
//! The display server's one font: Spleen 8x16 (BSD-2-Clause, Frederic
//! The one font: Spleen 8x16 (BSD-2-Clause, Frederic
//! Cambus; see LICENSE-spleen), printable ASCII only. Generated from
//! spleen-8x16.bdf; each glyph is 16 rows, bit 7 the leftmost pixel.

70
crates/raster/src/lib.rs Normal file
View File

@@ -0,0 +1,70 @@
//! Drawing on anything pixel-addressed (a `Framebuffer`): the screen, for
//! the display server, or a window's pixel buffer, for an app.
#![no_std]
#![forbid(unsafe_code)]
pub mod font;
use port_framebuffer::Framebuffer;
use types::display::{Color, ScreenCoord};
/// Characters drawn per row of pixels at a time: a small buffer, so a
/// caller's stack need not hold a whole line.
const CHUNK: usize = 32;
/// `text` from (x, y), each character cell whole, `fg` on `bg`, so text
/// overwrites what was there; cells past the right or bottom edge are left
/// out.
pub fn draw_text(fb: &mut impl Framebuffer, x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Color, text: &[u8]) {
let (fw, fh) = (fb.width().raw(), fb.height().raw());
let (x0, y0) = (x.raw(), y.raw());
if y0.saturating_add(font::HEIGHT) > fh {
return;
}
let fit = (fw.saturating_sub(x0) / font::WIDTH) as usize;
let chars = &text[..text.len().min(fit)];
// One run per pixel row across a chunk of cells, not a call per pixel.
let mut line = [bg; CHUNK * font::WIDTH as usize];
for (i, chunk) in chars.chunks(CHUNK).enumerate() {
let at = ScreenCoord::new(x0 + (i * CHUNK) as u16 * font::WIDTH);
let line = &mut line[..chunk.len() * font::WIDTH as usize];
for row in 0..font::HEIGHT {
for (cell, c) in line.chunks_mut(font::WIDTH as usize).zip(chunk) {
let bits = font::glyph(*c)[row as usize];
for (col, px) in cell.iter_mut().enumerate() {
*px = if bits & (0x80 >> col) != 0 { fg } else { bg };
}
}
fb.write_row(at, ScreenCoord::new(y0 + row), line);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use driver_framebuffer_mock::GridFramebuffer;
#[test]
fn text_is_drawn_cell_by_cell_and_clipped_at_the_edge() {
let mut fb = GridFramebuffer::<20, 20>::new();
let (fg, bg) = (Color::rgb(255, 255, 255), Color::rgb(1, 1, 1));
draw_text(&mut fb, ScreenCoord::new(0), ScreenCoord::new(0), fg, bg, b"!!!");
// '!' has its stem in columns 3 and 4 from row 2.
assert_eq!(fb.pixel(3, 2), Some(fg));
assert_eq!(fb.pixel(0, 2), Some(bg));
assert_eq!(fb.pixel(11, 2), Some(fg), "the second cell");
assert_eq!(fb.pixel(19, 2), Some(Color::from_raw(0)), "a third cell would not fit whole");
}
#[test]
fn a_long_line_is_drawn_in_chunks_without_a_gap() {
let mut fb = GridFramebuffer::<{ 8 * 70 }, 16>::new();
let (fg, bg) = (Color::rgb(255, 255, 255), Color::rgb(1, 1, 1));
draw_text(&mut fb, ScreenCoord::new(0), ScreenCoord::new(0), fg, bg, &[b'!'; 70]);
// The stem of the 33rd and the 70th '!'.
assert_eq!(fb.pixel(32 * 8 + 3, 2), Some(fg));
assert_eq!(fb.pixel(69 * 8 + 3, 2), Some(fg));
}
}

View File

@@ -8,6 +8,7 @@ types = { workspace = true }
server-framework = { workspace = true }
libkos = { workspace = true }
port-framebuffer = { workspace = true }
raster = { workspace = true }
[dev-dependencies]
driver-framebuffer-mock = { workspace = true }

View File

@@ -1,56 +0,0 @@
//! Who gets input: the display server is the input server's only client
//! and passes events on to the app in focus, the one that subscribed last.
//! Apps out of focus get nothing.
/// Events received and not yet polled by the app in focus.
const QUEUED: usize = 32;
pub struct Focus<N: Copy> {
/// The app in focus: its badge and the notification it gave to be told.
holder: Option<(usize, N)>,
events: [[usize; 4]; QUEUED],
head: usize,
len: usize,
}
impl<N: Copy> Focus<N> {
pub const fn new() -> Self {
Self { holder: None, events: [[0; 4]; QUEUED], head: 0, len: 0 }
}
/// `badge` takes focus; what the last holder had not read is dropped.
pub fn subscribe(&mut self, badge: usize, notify: N) {
self.holder = Some((badge, notify));
self.len = 0;
}
/// Queues an event (as the input server sends it) for the holder;
/// returns whom to tell. With no holder the event goes nowhere.
pub fn push(&mut self, event: [usize; 4]) -> Option<N> {
let (_, notify) = self.holder?;
if self.len == QUEUED {
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
}
self.events[(self.head + self.len) % QUEUED] = event;
self.len += 1;
Some(notify)
}
/// The next event for `badge`, if it is in focus and one is queued.
pub fn poll(&mut self, badge: usize) -> Option<[usize; 4]> {
if self.holder.is_none_or(|(b, _)| b != badge) || self.len == 0 {
return None;
}
let event = self.events[self.head];
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
Some(event)
}
}
impl<N: Copy> Default for Focus<N> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,99 @@
//! `Host` on the kernel: window buffers are regions of the display's own,
//! made once per slot and reused (a region lives as long as its owner), so
//! the display's memory has a fixed ceiling whatever apps do.
use libkos::syscall;
use libkos::window::Window;
use types::address::{ByteOffset, VirtualAddress};
use types::badge::Badge;
use types::bounded::{CapSlot, PageCount};
use types::display::Color;
use types::display::window::PIXEL_BYTES;
use types::mapping::PagePerms;
use types::permission::{Permission, PermissionSet};
use crate::server::Host;
use crate::windows::MAX_WINDOWS;
/// Where slot `i`'s buffer is mapped in the display: 4 MiB apart, room for
/// a 1280x800 window.
const BUFFERS_VA: usize = 0x0080_0000;
const BUFFER_SPAN: usize = 0x0040_0000;
const PAGE: usize = 4096;
pub struct KernelHost {
/// The display's own endpoint, which window capabilities are minted
/// from.
ep: CapSlot,
input: Option<CapSlot>,
buffers: [Option<(CapSlot, Window)>; MAX_WINDOWS],
}
impl KernelHost {
pub fn new(ep: CapSlot, input: Option<CapSlot>) -> Self {
Self { ep, input, buffers: [None; MAX_WINDOWS] }
}
}
impl Host for KernelHost {
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot> {
let slot = self.buffers.get_mut(index)?;
if slot.is_none() {
let bytes = (pixels * PIXEL_BYTES).min(BUFFER_SPAN);
let pages = PageCount::new(u16::try_from(bytes.div_ceil(PAGE)).ok()?)?;
let region = syscall::create_region(pages).ok()?;
let va = VirtualAddress::from_raw(BUFFERS_VA + index * BUFFER_SPAN);
match libkos::window::map(region, va, PagePerms::ReadWrite) {
Ok(window) => *slot = Some((region, window)),
Err(_) => {
syscall::drop_cap(region);
return None;
}
}
}
slot.as_ref().map(|(region, _)| *region)
}
fn blank(&mut self, index: usize) {
if let Some((_, window)) = self.buffers.get(index).and_then(Option::as_ref) {
window.fill_u32(ByteOffset::ZERO, window.len() / PIXEL_BYTES, 0);
}
}
fn read(&self, index: usize, from: usize, out: &mut [Color]) {
let Some((_, window)) = self.buffers.get(index).and_then(Option::as_ref) else { return };
for (i, px) in out.iter_mut().enumerate() {
let at = (from + i) * PIXEL_BYTES;
if at + PIXEL_BYTES > window.len() {
return;
}
*px = Color::from_raw(window.read::<u32>(ByteOffset::new(at)));
}
}
fn reclaim(&mut self, index: usize) {
if let Some((region, _)) = self.buffers.get(index).and_then(Option::as_ref) {
// Every copy handed out goes, and with it every mapping of one.
let _ = syscall::revoke(*region);
}
}
fn mint(&mut self, badge: usize) -> Option<CapSlot> {
let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
syscall::mint(self.ep, Badge::new(badge), perms).ok()
}
fn revoke(&mut self, cap: CapSlot) {
let _ = syscall::revoke(cap);
syscall::drop_cap(cap);
}
fn signal(&mut self, notify: CapSlot) {
let _ = syscall::signal(notify);
}
fn input(&mut self) -> Option<[usize; 4]> {
let event = syscall::request(self.input?, [types::input::POLL_EVENT, 0, 0, 0]).ok()?;
(event[0] != 0).then_some(event)
}
}

View File

@@ -1,10 +1,9 @@
#![no_std]
#![forbid(unsafe_code)]
pub mod focus;
pub mod font;
pub mod protocol;
pub mod host;
pub mod server;
pub mod windows;
#[cfg(test)]
#[path = "tests/mod.rs"]

View File

@@ -1,93 +0,0 @@
use server_framework::{Decode, Encode};
use types::display::text::Text;
use types::display::{Color, Dimension, ScreenCoord};
#[derive(Debug, PartialEq)]
pub enum DisplayRequest {
DrawRect { x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color },
Clear { color: Color },
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)]
pub enum DisplayResponse {
Ok,
Dimensions { width: Dimension, height: Dimension },
Error,
}
impl Encode for DisplayRequest {
fn encode(&self) -> [usize; 4] {
match self {
DisplayRequest::DrawRect { x, y, w, h, color } => {
let xy = (x.raw() as usize) | ((y.raw() as usize) << 16);
let wh = (w.raw() as usize) | ((h.raw() as usize) << 16);
[0, xy, wh, color.raw() as usize]
}
DisplayRequest::Clear { color } => [1, color.raw() as usize, 0, 0],
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]
}
}
}
}
impl Decode for DisplayRequest {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => {
let x = ScreenCoord::new((words[1] & 0xFFFF) as u16);
let y = ScreenCoord::new(((words[1] >> 16) & 0xFFFF) as u16);
let w = Dimension::new((words[2] & 0xFFFF) as u16);
let h = Dimension::new(((words[2] >> 16) & 0xFFFF) as u16);
let color = Color::from_raw(words[3] as u32);
Some(DisplayRequest::DrawRect { x, y, w, h, color })
}
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),
}
}
}
impl Encode for DisplayResponse {
fn encode(&self) -> [usize; 4] {
match self {
DisplayResponse::Ok => [0, 0, 0, 0],
DisplayResponse::Dimensions { width, height } => {
[1, width.raw() as usize, height.raw() as usize, 0]
}
DisplayResponse::Error => [2, 0, 0, 0],
}
}
}
impl Decode for DisplayResponse {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(DisplayResponse::Ok),
1 => Some(DisplayResponse::Dimensions {
width: Dimension::new(words[1] as u16),
height: Dimension::new(words[2] as u16),
}),
2 => Some(DisplayResponse::Error),
_ => None,
}
}
}

View File

@@ -1,138 +1,285 @@
//! The display server (see the vision's Display System note): it alone
//! holds the screen. It draws the OS bar along the top (the windows, which
//! one is in front, who is signed in) and shows the window in front below
//! it, copied from that window's pixel buffer as its app presents changes.
//! Apps draw only into their own buffer; the bar is the display's, so no
//! app can pose as another. The kernel side (buffers, capabilities, the
//! input server) is behind `Host`.
use libkos::syscall::Give;
use port_framebuffer::Framebuffer;
use raster::font;
use server_framework::{Decode, Encode, MALFORMED, Server};
use types::bounded::CapSlot;
use types::display::window::{self as w, NAME_BYTES};
use types::display::{Color, Dimension, ScreenCoord};
use crate::protocol::{DisplayRequest, DisplayResponse};
use port_framebuffer::Framebuffer;
use types::display::text::{TEXT_BYTES, Text};
use types::display::{Dimension, ScreenCoord};
use crate::windows::{MAX_WINDOWS, Windows};
use crate::focus::Focus;
use crate::font;
/// The OS bar: one row of text.
pub const BAR_HEIGHT: u16 = font::HEIGHT;
const BAR_BG: Color = Color::rgb(24, 24, 40);
const BAR_FG: Color = Color::rgb(170, 170, 190);
const FRONT_BG: Color = Color::rgb(90, 90, 150);
const FRONT_FG: Color = Color::rgb(255, 255, 255);
/// Pixels copied per row at a time.
const RUN: usize = 256;
pub struct DisplayServer<F: Framebuffer> {
fb: F,
/// The input server, which signals this server's bound notification.
input: Option<CapSlot>,
focus: Focus<CapSlot>,
/// What the display needs from the kernel.
pub trait Host {
/// Window slot `index`'s pixel buffer of `pixels` pixels, made the
/// first time; the capability its window's holder maps.
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot>;
/// Blanks slot `index`'s buffer, so a new window shows nothing of the
/// last one's.
fn blank(&mut self, index: usize);
/// Pixels of slot `index`'s buffer from pixel `from` on, into `out`.
fn read(&self, index: usize, from: usize, out: &mut [Color]);
/// Takes back every copy of slot `index`'s buffer handed out.
fn reclaim(&mut self, index: usize);
/// A copy of the display's endpoint carrying `badge`.
fn mint(&mut self, badge: usize) -> Option<CapSlot>;
/// Deletes `cap` and every copy made from it.
fn revoke(&mut self, cap: CapSlot);
fn signal(&mut self, notify: CapSlot);
/// The input server's next event, if one is waiting.
fn input(&mut self) -> Option<[usize; 4]>;
}
impl<F: Framebuffer> DisplayServer<F> {
pub fn new(fb: F) -> Self {
Self { fb, input: None, focus: Focus::new() }
}
/// Nothing goes through `handle`: every request needs its badge.
pub struct Never;
/// A server that passes `input`'s events on to the app in focus.
pub fn with_input(fb: F, input: CapSlot) -> Self {
Self { input: Some(input), ..Self::new(fb) }
impl Decode for Never {
fn decode(_: [usize; 4]) -> Option<Self> {
None
}
}
impl Encode for Never {
fn encode(&self) -> [usize; 4] {
[MALFORMED, 0, 0, 0]
}
}
pub struct DisplayServer<F: Framebuffer, H: Host> {
fb: F,
host: H,
windows: Windows<CapSlot>,
/// Each open window's capability, as minted.
caps: [Option<CapSlot>; MAX_WINDOWS],
user: [u8; NAME_BYTES],
user_len: usize,
}
impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
pub fn new(fb: F, host: H) -> Self {
let mut s = Self { fb, host, windows: Windows::new(), caps: [None; MAX_WINDOWS], user: [0; NAME_BYTES], user_len: 0 };
s.repaint();
s
}
pub fn framebuffer(&self) -> &F {
&self.fb
}
/// Draws each cell whole, background too, so text overwrites what was
/// there; cells past the screen edge are skipped.
fn draw_text(&mut self, t: &Text) {
let (fw, fh) = (self.fb.width().raw(), self.fb.height().raw());
let (x0, y0) = (t.x.raw(), t.y.raw());
if y0.saturating_add(font::HEIGHT) > fh {
pub fn host(&self) -> &H {
&self.host
}
#[cfg(test)]
pub fn host_mut_for_test(&mut self) -> &mut H {
&mut self.host
}
/// A window's size: the screen below the bar.
fn window_size(&self) -> (Dimension, Dimension) {
(self.fb.width(), Dimension::new(self.fb.height().raw().saturating_sub(BAR_HEIGHT)))
}
fn pixels(&self) -> usize {
let (width, height) = self.window_size();
width.raw() as usize * height.raw() as usize
}
fn open(&mut self, opener: usize, name: &[u8]) -> ([usize; 4], Option<Give>) {
let Some((index, badge)) = self.windows.open(opener, name) else {
return ([MALFORMED, 0, 0, 0], None);
};
let pixels = self.pixels();
let made = self.host.buffer(index, pixels).is_some();
let cap = made.then(|| self.host.mint(badge)).flatten();
let Some(cap) = cap else {
self.windows.close(index);
return ([MALFORMED, 0, 0, 0], None);
};
self.host.blank(index);
self.caps[index] = Some(cap);
self.repaint();
let (width, height) = self.window_size();
// The launcher passes it on to the app.
([w::OK, width.raw() as usize, height.raw() as usize, 0], Some(Give::keeping_delegate(cap)))
}
fn close(&mut self, index: usize) {
if let Some(cap) = self.caps.get_mut(index).and_then(Option::take) {
self.host.revoke(cap);
}
self.host.reclaim(index);
self.windows.close(index);
self.repaint();
}
/// Copies a rectangle of window `index` to the screen, if it is in
/// front; clipped to the window.
fn present(&mut self, index: usize, x: ScreenCoord, y: ScreenCoord, width: Dimension, height: Dimension) {
if self.windows.front() != Some(index) {
return;
}
// 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 };
}
let (ww, wh) = (self.window_size().0.raw(), self.window_size().1.raw());
let (x0, y0) = (x.raw().min(ww), y.raw().min(wh));
let (x1, y1) = (x0.saturating_add(width.raw()).min(ww), y0.saturating_add(height.raw()).min(wh));
let mut run = [Color::from_raw(0); RUN];
for row in y0..y1 {
let mut col = x0;
while col < x1 {
let n = usize::from(x1 - col).min(RUN);
self.host.read(index, row as usize * ww as usize + col as usize, &mut run[..n]);
self.fb.write_row(ScreenCoord::new(col), ScreenCoord::new(row + BAR_HEIGHT), &run[..n]);
col += n as u16;
}
}
self.fb.flush();
}
/// The bar, and the window in front (or nothing) below it.
fn repaint(&mut self) {
self.draw_bar();
let (width, height) = self.window_size();
match self.windows.front() {
Some(index) => self.present(index, ScreenCoord::new(0), ScreenCoord::new(0), width, height),
None => {
self.fb.fill_rect(ScreenCoord::new(0), ScreenCoord::new(BAR_HEIGHT), width, height, Color::rgb(30, 30, 60));
self.fb.flush();
}
self.fb.write_row(t.x, ScreenCoord::new(y0 + row), line);
}
}
}
impl<F: Framebuffer> Server for DisplayServer<F> {
type Request = DisplayRequest;
type Response = DisplayResponse;
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
use types::display::input::{POLL, SUBSCRIBE};
match (words[0], cap) {
(SUBSCRIBE, Some(notify)) => {
self.focus.subscribe(badge, notify);
(DisplayResponse::Ok.encode(), None)
/// Each window's name, the front one highlighted; who is signed in at
/// the right.
fn draw_bar(&mut self) {
let width = self.fb.width();
self.fb.fill_rect(ScreenCoord::new(0), ScreenCoord::new(0), width, Dimension::new(BAR_HEIGHT), BAR_BG);
let mut x = 0u16;
let mut label = [b' '; NAME_BYTES + 2];
let entries: [Option<([u8; NAME_BYTES], usize, bool)>; MAX_WINDOWS] = {
let mut out = [None; MAX_WINDOWS];
for (slot, (_, name, front)) in out.iter_mut().zip(self.windows.list()) {
let mut n = [0u8; NAME_BYTES];
n[..name.len()].copy_from_slice(name);
*slot = Some((n, name.len(), front));
}
(SUBSCRIBE, None) => (DisplayResponse::Error.encode(), None),
(POLL, _) => (self.focus.poll(badge).unwrap_or([0; 4]), None),
_ => match DisplayRequest::decode(words) {
Some(request) => (self.handle(request).encode(), None),
out
};
for (name, len, front) in entries.into_iter().flatten() {
label[1..=len].copy_from_slice(&name[..len]);
label[len + 1] = b' ';
let text = &label[..len + 2];
let (fg, bg) = if front { (FRONT_FG, FRONT_BG) } else { (BAR_FG, BAR_BG) };
raster::draw_text(&mut self.fb, ScreenCoord::new(x), ScreenCoord::new(0), fg, bg, text);
x = x.saturating_add(text.len() as u16 * font::WIDTH + font::WIDTH);
}
let user = &self.user[..self.user_len];
let at = width.raw().saturating_sub((user.len() as u16 + 1) * font::WIDTH);
raster::draw_text(&mut self.fb, ScreenCoord::new(at), ScreenCoord::new(0), BAR_FG, BAR_BG, user);
}
/// A request through window `index`'s capability.
fn window_call(&mut self, index: usize, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
match (words[0], cap) {
(w::BUFFER, _) => match self.host.buffer(index, self.pixels()) {
Some(buffer) => {
let (width, height) = self.window_size();
([w::OK, width.raw() as usize, height.raw() as usize, 0], Some(Give::from(buffer)))
}
None => ([MALFORMED, 0, 0, 0], None),
},
}
}
/// The input server has events: take them all, tell the app in focus.
fn signal(&mut self, _bits: usize) {
let Some(input) = self.input else {
return;
};
let mut told = None;
while let Ok(event) = libkos::syscall::request(input, [types::input::POLL_EVENT, 0, 0, 0]) {
if event[0] == 0 {
break;
(w::PRESENT, _) => {
let (x, y, width, height) = w::unpack_rect([words[1], words[2]]);
self.present(index, x, y, width, height);
([w::OK, 0, 0, 0], None)
}
told = self.focus.push(event).or(told);
}
if let Some(notify) = told {
let _ = libkos::syscall::signal(notify);
}
}
fn handle(&mut self, request: DisplayRequest) -> DisplayResponse {
match request {
DisplayRequest::DrawRect { x, y, w, h, color } => {
// Clients choose every coordinate; clip to the screen so a
// huge or off-screen rectangle costs nothing and coordinates
// never overflow in the driver.
let (fw, fh) = (self.fb.width().raw(), self.fb.height().raw());
if x.raw() < fw && y.raw() < fh {
let w = Dimension::new(w.raw().min(fw - x.raw()));
let h = Dimension::new(h.raw().min(fh - y.raw()));
self.fb.fill_rect(x, y, w, h, color);
}
DisplayResponse::Ok
(w::SUBSCRIBE, Some(notify)) if self.windows.subscribe(badge, notify) => ([w::OK, 0, 0, 0], None),
(w::POLL, _) => (self.windows.poll(badge).unwrap_or([0; 4]), None),
(w::CLOSE, _) => {
self.close(index);
([w::OK, 0, 0, 0], None)
}
DisplayRequest::Clear { color } => {
self.fb.clear(color);
DisplayResponse::Ok
}
DisplayRequest::Flush => {
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
}
DisplayRequest::GetDimensions => DisplayResponse::Dimensions {
width: self.fb.width(),
height: self.fb.height(),
},
_ => ([MALFORMED, 0, 0, 0], None),
}
}
}
fn is_switch_key(event: [usize; 4]) -> bool {
use types::input::event::{FUNCTION_KEY, KEY_DOWN, KEY_UP};
matches!(event[0], KEY_DOWN | KEY_UP) && event[1] == FUNCTION_KEY && event[2] == w::SWITCH_KEY
}
impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
type Request = Never;
type Response = Never;
fn handle(&mut self, request: Never) -> Never {
request
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
if badge & w::BADGE != 0 {
return match self.windows.slot(badge) {
Some(index) => self.window_call(index, badge, words, cap),
// Closed: its capability is on its way out.
None => ([MALFORMED, 0, 0, 0], None),
};
}
match words[0] {
w::OPEN => {
let (name, len) = w::unpack_name([words[1], words[2], words[3]]);
self.open(badge, &name[..len])
}
w::SET_USER if badge == 0 => {
let (name, len) = w::unpack_name([words[1], words[2], words[3]]);
(self.user, self.user_len) = (name, len);
self.repaint();
([w::OK, 0, 0, 0], None)
}
w::FORGET if badge == 0 => {
let mut gone = [None; MAX_WINDOWS];
for (slot, index) in gone.iter_mut().zip(self.windows.opened_by(words[1])) {
*slot = Some(index);
}
for index in gone.into_iter().flatten() {
self.close(index);
}
([w::OK, 0, 0, 0], None)
}
_ => ([MALFORMED, 0, 0, 0], None),
}
}
/// The input server has events: the switch key is the display's, the
/// rest go to the window in front.
fn signal(&mut self, _bits: usize) {
let mut told = None;
while let Some(event) = self.host.input() {
if is_switch_key(event) {
if event[0] == types::input::event::KEY_DOWN && self.windows.cycle().is_some() {
self.repaint();
}
continue;
}
told = self.windows.push(event).or(told);
}
if let Some(notify) = told {
self.host.signal(notify);
}
}
}

View File

@@ -1,36 +0,0 @@
use crate::focus::Focus;
const KEY: [usize; 4] = [1, 9, b'a' as usize, 0];
#[test]
fn only_the_app_in_focus_gets_events() {
let mut f: Focus<u8> = Focus::new();
assert_eq!(f.push(KEY), None, "nobody to give it to");
f.subscribe(1, 10);
assert_eq!(f.push(KEY), Some(10));
assert_eq!(f.poll(2), None);
assert_eq!(f.poll(1), Some(KEY));
assert_eq!(f.poll(1), None);
}
#[test]
fn a_new_subscriber_takes_focus_and_the_old_queue_is_dropped() {
let mut f: Focus<u8> = Focus::new();
f.subscribe(1, 10);
f.push(KEY);
f.subscribe(2, 20);
assert_eq!(f.poll(1), None);
assert_eq!(f.poll(2), None);
assert_eq!(f.push(KEY), Some(20));
assert_eq!(f.poll(2), Some(KEY));
}
#[test]
fn a_full_queue_drops_the_oldest() {
let mut f: Focus<u8> = Focus::new();
f.subscribe(1, 10);
for i in 0..40 {
f.push([1, 9, i, 0]);
}
assert_eq!(f.poll(1), Some([1, 9, 8, 0]));
}

View File

@@ -1,3 +1,2 @@
mod focus;
mod protocol;
mod server;
mod windows;

View File

@@ -1,66 +0,0 @@
use crate::protocol::{DisplayRequest, DisplayResponse};
use server_framework::{Decode, Encode};
use types::display::{Color, Dimension, ScreenCoord};
#[test]
fn draw_rect_roundtrip() {
let req = DisplayRequest::DrawRect {
x: ScreenCoord::new(10),
y: ScreenCoord::new(20),
w: Dimension::new(100),
h: Dimension::new(50),
color: Color::from_raw(0xFF0000),
};
let words = req.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn clear_roundtrip() {
let req = DisplayRequest::Clear { color: Color::from_raw(0x00FF00) };
let words = req.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn flush_roundtrip() {
let words = DisplayRequest::Flush.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, DisplayRequest::Flush);
}
#[test]
fn get_dimensions_roundtrip() {
let words = DisplayRequest::GetDimensions.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, DisplayRequest::GetDimensions);
}
#[test]
fn dimensions_response_roundtrip() {
let resp = DisplayResponse::Dimensions {
width: Dimension::new(320),
height: Dimension::new(240),
};
let words = resp.encode();
let decoded = DisplayResponse::decode(words).unwrap();
assert_eq!(decoded, resp);
}
#[test]
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));
}

View File

@@ -1,184 +1,189 @@
use crate::protocol::{DisplayRequest, DisplayResponse};
use crate::server::DisplayServer;
use driver_framebuffer_mock::MockFramebuffer;
use server_framework::Server;
use types::display::{Color, Dimension, ScreenCoord};
extern crate std;
fn test_server() -> DisplayServer<MockFramebuffer> {
DisplayServer::new(MockFramebuffer::new(
Dimension::new(320),
Dimension::new(240),
))
use crate::server::{BAR_HEIGHT, DisplayServer, Host};
use driver_framebuffer_mock::GridFramebuffer;
use server_framework::{MALFORMED, Server};
use std::vec;
use std::vec::Vec;
use types::bounded::CapSlot;
use types::display::Color;
use types::display::window::{self as w, BADGE};
const W: usize = 32;
const H: usize = 16 + 8;
/// Buffers in memory; a capability is its slot number plus 40.
#[derive(Default)]
struct FakeHost {
buffers: Vec<Option<Vec<Color>>>,
minted: Vec<usize>,
revoked: Vec<CapSlot>,
reclaimed: Vec<usize>,
signalled: Vec<CapSlot>,
events: Vec<[usize; 4]>,
}
#[test]
fn draw_rect_writes_pixels() {
let mut server = test_server();
let resp = server.handle(DisplayRequest::DrawRect {
x: ScreenCoord::new(0),
y: ScreenCoord::new(0),
w: Dimension::new(4),
h: Dimension::new(3),
color: Color::from_raw(0xFF),
});
assert_eq!(resp, DisplayResponse::Ok);
assert_eq!(server.framebuffer().pixel_count(), 12);
}
#[test]
fn clear_resets_pixels() {
let mut server = test_server();
server.handle(DisplayRequest::DrawRect {
x: ScreenCoord::new(0),
y: ScreenCoord::new(0),
w: Dimension::new(10),
h: Dimension::new(10),
color: Color::from_raw(0xFF),
});
server.handle(DisplayRequest::Clear {
color: Color::from_raw(0),
});
assert_eq!(server.framebuffer().pixel_count(), 0);
}
#[test]
fn flush_sets_flag() {
let mut server = test_server();
assert!(!server.framebuffer().was_flushed());
server.handle(DisplayRequest::Flush);
assert!(server.framebuffer().was_flushed());
}
#[test]
fn get_dimensions_returns_size() {
let mut server = test_server();
let resp = server.handle(DisplayRequest::GetDimensions);
assert_eq!(
resp,
DisplayResponse::Dimensions {
width: Dimension::new(320),
height: Dimension::new(240),
impl Host for FakeHost {
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot> {
if self.buffers.len() <= index {
self.buffers.resize(index + 1, None);
}
);
}
#[test]
fn draw_rect_correct_pixel_positions() {
let mut server = test_server();
server.handle(DisplayRequest::DrawRect {
x: ScreenCoord::new(5),
y: ScreenCoord::new(10),
w: Dimension::new(2),
h: Dimension::new(2),
color: Color::from_raw(0xAB),
});
let fb = server.framebuffer();
assert_eq!(fb.pixel_count(), 4);
assert_eq!(
fb.pixel_at(0),
(
ScreenCoord::new(5),
ScreenCoord::new(10),
Color::from_raw(0xAB)
)
);
assert_eq!(
fb.pixel_at(1),
(
ScreenCoord::new(6),
ScreenCoord::new(10),
Color::from_raw(0xAB)
)
);
assert_eq!(
fb.pixel_at(2),
(
ScreenCoord::new(5),
ScreenCoord::new(11),
Color::from_raw(0xAB)
)
);
assert_eq!(
fb.pixel_at(3),
(
ScreenCoord::new(6),
ScreenCoord::new(11),
Color::from_raw(0xAB)
)
);
}
#[test]
fn a_huge_rect_is_clipped_to_the_screen() {
let mut server = test_server();
let resp = server.handle(DisplayRequest::DrawRect {
x: ScreenCoord::new(310),
y: ScreenCoord::new(230),
w: Dimension::new(u16::MAX),
h: Dimension::new(u16::MAX),
color: Color::from_raw(0xFF),
});
assert_eq!(resp, DisplayResponse::Ok);
assert_eq!(server.framebuffer().pixel_count(), 10 * 10);
let (x, y, _) = server.framebuffer().pixel_at(99);
assert_eq!((x.raw(), y.raw()), (319, 239));
}
#[test]
fn an_off_screen_rect_draws_nothing() {
let mut server = test_server();
for (x, y) in [(320, 0), (0, 240), (u16::MAX, u16::MAX)] {
server.handle(DisplayRequest::DrawRect {
x: ScreenCoord::new(x),
y: ScreenCoord::new(y),
w: Dimension::new(u16::MAX),
h: Dimension::new(u16::MAX),
color: Color::from_raw(0xFF),
});
self.buffers[index].get_or_insert_with(|| vec![Color::from_raw(0); pixels]);
CapSlot::new(40 + index as u8)
}
assert_eq!(server.framebuffer().pixel_count(), 0);
fn blank(&mut self, index: usize) {
if let Some(b) = self.buffers[index].as_mut() {
b.fill(Color::from_raw(0));
}
}
fn read(&self, index: usize, from: usize, out: &mut [Color]) {
let b = self.buffers[index].as_ref().unwrap();
out.copy_from_slice(&b[from..from + out.len()]);
}
fn reclaim(&mut self, index: usize) {
self.reclaimed.push(index);
}
fn mint(&mut self, badge: usize) -> Option<CapSlot> {
self.minted.push(badge);
CapSlot::new(20 + self.minted.len() as u8)
}
fn revoke(&mut self, cap: CapSlot) {
self.revoked.push(cap);
}
fn signal(&mut self, notify: CapSlot) {
self.signalled.push(notify);
}
fn input(&mut self) -> Option<[usize; 4]> {
(!self.events.is_empty()).then(|| self.events.remove(0))
}
}
type Srv = DisplayServer<GridFramebuffer<W, H>, FakeHost>;
fn server() -> Srv {
DisplayServer::new(GridFramebuffer::new(), FakeHost::default())
}
/// Opens a window through launcher `opener`: its badge.
fn open(s: &mut Srv, opener: usize, name: &[u8]) -> usize {
let n = w::pack_name(name);
let (reply, cap) = s.handle_call(opener, [w::OPEN, n[0], n[1], n[2]], None);
assert_eq!(reply, [w::OK, W, H - BAR_HEIGHT as usize, 0]);
assert!(cap.is_some_and(|g| g.keep_delegate), "the launcher may pass it on");
*s.host().minted.last().unwrap()
}
/// Paints window `slot`'s buffer one colour and presents all of it.
fn paint(s: &mut Srv, badge: usize, slot: usize, color: Color) -> [usize; 4] {
// The app writes its buffer directly; here, through the fake.
s.host_mut_for_test().buffers[slot].as_mut().unwrap().fill(color);
let rect = w::pack_rect(
types::display::ScreenCoord::new(0),
types::display::ScreenCoord::new(0),
types::display::Dimension::new(W as u16),
types::display::Dimension::new(H as u16),
);
s.handle_call(badge, [w::PRESENT, rect[0], rect[1], 0], None).0
}
fn shown(s: &Srv) -> Option<Color> {
s.framebuffer().pixel(W / 2, H - 1)
}
const RED: Color = Color::rgb(200, 0, 0);
const BLUE: Color = Color::rgb(0, 0, 200);
#[test]
fn the_front_window_is_shown_below_the_bar() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
assert_eq!(paint(&mut s, t, 0, RED)[0], w::OK);
assert_eq!(shown(&s), Some(RED));
assert_ne!(s.framebuffer().pixel(W - 1, 0), Some(RED), "the bar is the display's");
assert_eq!(s.framebuffer().pixel(0, BAR_HEIGHT as usize), Some(RED), "the window starts under it");
}
#[test]
fn text_draws_whole_glyph_cells() {
use types::display::text::Text;
let mut server = test_server();
let (fg, bg) = (Color::rgb(255, 255, 255), Color::rgb(0, 0, 0));
let t = Text::new(ScreenCoord::new(8), ScreenCoord::new(16), fg, bg, b"A");
assert_eq!(server.handle(DisplayRequest::DrawText(t)), DisplayResponse::Ok);
// One 8x16 cell, written row by row.
assert_eq!(server.framebuffer().pixel_count(), 128);
// 'A' row 2 is 0x7c: column 0 off, column 1 on.
let at = |i: usize| server.framebuffer().pixel_at(i);
assert_eq!(at(16), (ScreenCoord::new(8), ScreenCoord::new(18), bg));
assert_eq!(at(17), (ScreenCoord::new(9), ScreenCoord::new(18), fg));
fn a_window_behind_draws_nothing_until_it_is_in_front() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
paint(&mut s, t, 0, RED);
let snake = open(&mut s, 2, b"Snake");
assert_eq!(shown(&s), Some(Color::from_raw(0)), "a new window starts blank");
paint(&mut s, t, 0, BLUE);
assert_eq!(shown(&s), Some(Color::from_raw(0)), "the terminal is behind");
paint(&mut s, snake, 1, RED);
assert_eq!(shown(&s), Some(RED));
// The switch key brings the terminal back, as it last drew.
s.host_mut_for_test().events = vec![[1, 10, 1, 0], [2, 10, 1, 0]];
s.signal(0);
assert_eq!(shown(&s), Some(BLUE));
}
#[test]
fn text_past_the_edge_is_cut_not_wrapped() {
use types::display::text::Text;
let mut server = test_server();
let c = Color::rgb(1, 1, 1);
// 320 wide: from x=304 only two cells fit.
let t = Text::new(ScreenCoord::new(304), ScreenCoord::new(0), c, c, b"abc");
server.handle(DisplayRequest::DrawText(t));
assert_eq!(server.framebuffer().pixel_count(), 256);
let low = Text::new(ScreenCoord::new(0), ScreenCoord::new(230), c, c, b"a");
server.handle(DisplayRequest::DrawText(low));
assert_eq!(server.framebuffer().pixel_count(), 256);
fn input_goes_to_the_front_window_and_the_switch_key_to_nobody() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
let notify = CapSlot::new(9).unwrap();
assert_eq!(s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(notify)).0[0], w::OK);
s.host_mut_for_test().events = vec![[1, 9, b'a' as usize, 0], [1, 10, 1, 0]];
s.signal(0);
assert_eq!(s.host().signalled, [notify]);
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0, [1, 9, b'a' as usize, 0]);
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0, [0; 4], "F1 was the display's");
}
#[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");
fn only_launchers_open_windows_and_only_init_sets_the_user() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
let n = w::pack_name(b"Fake");
assert_eq!(s.handle_call(t, [w::OPEN, n[0], n[1], n[2]], None).0[0], MALFORMED, "not through a window");
assert_eq!(s.handle_call(1, [w::SET_USER, n[0], n[1], n[2]], None).0[0], MALFORMED);
assert_eq!(s.handle_call(0, [w::SET_USER, n[0], n[1], n[2]], None).0[0], w::OK);
assert_eq!(s.handle_call(BADGE | 12345, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "a made-up window");
}
#[test]
fn closing_takes_the_window_and_its_buffer_back() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
let snake = open(&mut s, 2, b"Snake");
paint(&mut s, t, 0, BLUE);
assert_eq!(s.handle_call(snake, [w::CLOSE, 0, 0, 0], None).0[0], w::OK);
assert_eq!(s.host().revoked, [CapSlot::new(22).unwrap()]);
assert_eq!(s.host().reclaimed, [1]);
assert_eq!(shown(&s), Some(BLUE), "the terminal is in front again");
assert_eq!(s.handle_call(snake, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "closed");
// Its buffer is reused, blank, for the next window.
s.host_mut_for_test().buffers[1].as_mut().unwrap().fill(RED);
open(&mut s, 2, b"Snake");
assert!(s.host().buffers[1].as_ref().unwrap().iter().all(|c| *c == Color::from_raw(0)));
}
#[test]
fn a_launcher_gone_takes_its_windows_with_it() {
let mut s = server();
open(&mut s, 1, b"Terminal");
open(&mut s, 2, b"Snake");
open(&mut s, 2, b"Counter");
assert_eq!(s.handle_call(3, [w::FORGET, 2, 0, 0], None).0[0], MALFORMED, "only init says so");
assert_eq!(s.handle_call(0, [w::FORGET, 2, 0, 0], None).0[0], w::OK);
assert_eq!(s.host().reclaimed, [1, 2]);
}
#[test]
fn a_present_outside_the_window_is_clipped() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
s.host_mut_for_test().buffers[0].as_mut().unwrap().fill(RED);
let rect = w::pack_rect(
types::display::ScreenCoord::new(30),
types::display::ScreenCoord::new(5),
types::display::Dimension::new(60000),
types::display::Dimension::new(60000),
);
assert_eq!(s.handle_call(t, [w::PRESENT, rect[0], rect[1], 0], None).0[0], w::OK);
assert_eq!(s.framebuffer().pixel(31, H - 1), Some(RED));
assert_ne!(s.framebuffer().pixel(29, H - 1), Some(RED), "left of the rectangle is untouched");
}

View File

@@ -0,0 +1,69 @@
extern crate std;
use crate::windows::{MAX_WINDOWS, Windows};
use std::string::String;
use std::vec::Vec;
fn names(w: &Windows<u8>) -> Vec<(String, bool)> {
w.list().map(|(_, n, front)| (String::from_utf8_lossy(n).into_owned(), front)).collect()
}
#[test]
fn a_new_window_comes_to_the_front_and_the_key_cycles() {
let mut w = Windows::<u8>::new();
let (t, _) = w.open(1, b"Terminal").unwrap();
let (s, _) = w.open(2, b"Snake").unwrap();
assert_eq!(w.front(), Some(s));
assert_eq!(names(&w), [("Terminal".into(), false), ("Snake".into(), true)]);
assert_eq!(w.cycle(), Some(t));
assert_eq!(w.cycle(), Some(s));
}
#[test]
fn closing_the_front_window_shows_the_next_one() {
let mut w = Windows::<u8>::new();
let (t, _) = w.open(1, b"Terminal").unwrap();
let (s, _) = w.open(2, b"Snake").unwrap();
assert!(w.close(s), "the front changed");
assert_eq!(w.front(), Some(t));
assert!(w.close(t));
assert_eq!(w.front(), None);
assert_eq!(w.cycle(), None, "nothing to cycle to");
}
#[test]
fn only_the_front_window_gets_input_and_only_its_own() {
let mut w = Windows::<u8>::new();
let (_, tb) = w.open(1, b"Terminal").unwrap();
assert!(w.subscribe(tb, 7));
let (_, sb) = w.open(2, b"Snake").unwrap();
assert!(w.subscribe(sb, 9));
assert_eq!(w.push([1, 9, 0, 0]), Some(9), "snake is in front");
assert_eq!(w.poll(tb), None, "the terminal sees none of it");
assert_eq!(w.poll(sb), Some([1, 9, 0, 0]));
w.cycle();
assert_eq!(w.push([1, 4, 0, 0]), Some(7));
}
#[test]
fn badges_are_never_reused_and_room_runs_out() {
let mut w = Windows::<u8>::new();
let (i, first) = w.open(1, b"a").unwrap();
w.close(i);
let (_, second) = w.open(1, b"a").unwrap();
assert_ne!(first, second);
assert_eq!(w.slot(first), None, "a closed window's badge reaches nothing");
for _ in 1..MAX_WINDOWS {
assert!(w.open(1, b"x").is_some());
}
assert!(w.open(1, b"x").is_none());
}
#[test]
fn a_launchers_windows_are_found_by_its_badge() {
let mut w = Windows::<u8>::new();
let (a, _) = w.open(5, b"one").unwrap();
w.open(6, b"other").unwrap();
let (b, _) = w.open(5, b"two").unwrap();
assert_eq!(w.opened_by(5).collect::<Vec<_>>(), [a, b]);
}

View File

@@ -0,0 +1,156 @@
//! Which windows there are and which is in front (see `types::display::
//! window`). Every window fills the screen below the OS bar; one is in
//! front at a time, and only it is shown and gets input. A new window comes
//! to the front; a key cycles through them. Pure: the server keeps the
//! pixels.
use types::display::window::{BADGE, NAME_BYTES};
pub const MAX_WINDOWS: usize = 4;
/// Events received and not yet polled by the window in front.
const QUEUED: usize = 32;
struct Queue {
events: [[usize; 4]; QUEUED],
head: usize,
len: usize,
}
impl Queue {
const fn new() -> Self {
Self { events: [[0; 4]; QUEUED], head: 0, len: 0 }
}
/// The oldest is dropped when it is full.
fn push(&mut self, event: [usize; 4]) {
if self.len == QUEUED {
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
}
self.events[(self.head + self.len) % QUEUED] = event;
self.len += 1;
}
fn pop(&mut self) -> Option<[usize; 4]> {
if self.len == 0 {
return None;
}
let event = self.events[self.head];
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
Some(event)
}
}
struct Window<N> {
badge: usize,
/// The launcher's badge: its windows go when it does.
opener: usize,
name: [u8; NAME_BYTES],
name_len: usize,
notify: Option<N>,
queue: Queue,
}
pub struct Windows<N: Copy> {
slots: [Option<Window<N>>; MAX_WINDOWS],
front: Option<usize>,
/// Makes each window's badge new, so a closed window's never returns.
opened: usize,
}
impl<N: Copy> Windows<N> {
pub const fn new() -> Self {
Self { slots: [const { None }; MAX_WINDOWS], front: None, opened: 0 }
}
/// A window for `name`, opened through `opener`'s capability, in
/// front: its slot and its badge. `None` if there is no room.
pub fn open(&mut self, opener: usize, name: &[u8]) -> Option<(usize, usize)> {
let index = self.slots.iter().position(Option::is_none)?;
self.opened += 1;
let badge = BADGE | self.opened << 8 | index;
let mut n = [0u8; NAME_BYTES];
let name_len = name.len().min(NAME_BYTES);
n[..name_len].copy_from_slice(&name[..name_len]);
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new() });
self.front = Some(index);
Some((index, badge))
}
/// The slot of the window behind `badge`, if it is open.
pub fn slot(&self, badge: usize) -> Option<usize> {
self.slots.iter().position(|w| w.as_ref().is_some_and(|w| w.badge == badge))
}
/// Closes the window in `index`; whether the front changed.
pub fn close(&mut self, index: usize) -> bool {
if self.slots.get_mut(index).and_then(Option::take).is_none() {
return false;
}
if self.front != Some(index) {
return false;
}
self.front = None;
self.front = self.after(index);
true
}
/// The slots of every window `opener` opened.
pub fn opened_by(&self, opener: usize) -> impl Iterator<Item = usize> + '_ {
self.slots.iter().enumerate().filter_map(move |(i, w)| w.as_ref().filter(|w| w.opener == opener).map(|_| i))
}
/// The next open window after `index`, round, if any.
fn after(&self, index: usize) -> Option<usize> {
(1..=MAX_WINDOWS).map(|step| (index + step) % MAX_WINDOWS).find(|i| self.slots[*i].is_some())
}
/// Brings the next window to the front; the new front if it changed.
pub fn cycle(&mut self) -> Option<usize> {
let next = self.after(self.front?)?;
(Some(next) != self.front).then(|| {
self.front = Some(next);
next
})
}
pub fn front(&self) -> Option<usize> {
self.front
}
/// Tells `notify` when input for the window `badge` arrives.
pub fn subscribe(&mut self, badge: usize, notify: N) -> bool {
let Some(w) = self.slot(badge).and_then(|i| self.slots[i].as_mut()) else { return false };
w.notify = Some(notify);
true
}
/// Queues an event for the window in front; whom to tell, if anyone.
pub fn push(&mut self, event: [usize; 4]) -> Option<N> {
let w = self.front.and_then(|i| self.slots[i].as_mut())?;
w.queue.push(event);
w.notify
}
/// The window `badge`'s next event.
pub fn poll(&mut self, badge: usize) -> Option<[usize; 4]> {
let i = self.slot(badge)?;
self.slots[i].as_mut()?.queue.pop()
}
/// Every open window, for the bar: its slot, its name, and whether it
/// is in front.
pub fn list(&self) -> impl Iterator<Item = (usize, &[u8], bool)> + '_ {
self.slots
.iter()
.enumerate()
.filter_map(move |(i, w)| w.as_ref().map(|w| (i, &w.name[..w.name_len], self.front == Some(i))))
}
}
impl<N: Copy> Default for Windows<N> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -53,6 +53,7 @@ fn encode_key(key: Key) -> (usize, usize) {
Key::Backspace => (7, 0),
Key::Tab => (8, 0),
Key::Char(c) => (9, c as usize),
Key::Function(n) => (types::input::event::FUNCTION_KEY, n as usize),
}
}
@@ -68,6 +69,7 @@ fn decode_key(kind: usize, data: usize) -> Option<Key> {
7 => Some(Key::Backspace),
8 => Some(Key::Tab),
9 => Some(Key::Char(data as u8)),
types::input::event::FUNCTION_KEY => Some(Key::Function(data as u8)),
_ => None,
}
}

View File

@@ -43,6 +43,8 @@ struct Running {
granted: Capabilities,
/// Its data folder grant, taken back when it dies.
data: Option<CapSlot>,
/// Its window, closed when it dies.
window: Option<CapSlot>,
}
pub struct PermissionServer {
@@ -177,13 +179,19 @@ impl PermissionServer {
true => self.store.data_grant(&plan.app.id),
false => None,
};
// Opened here, named from its manifest: the app cannot name its own.
let window = match plan.granted.contains(Capability::Display) {
true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window),
false => None,
};
let staged = (badge == self.staged.badge).then_some(&self.staged);
let arg = StartArg {
log: staged.and_then(|s| s.output).and_then(|o| child.give(o, call)).or(child.log()),
storage: staged.and_then(|s| s.folder).and_then(|f| child.give(f, call)),
data: data.and_then(|d| child.give(d, call)),
// It subscribes to input by sending the display a notification.
display: self.display.filter(|_| plan.granted.contains(Capability::Display)).and_then(|d| child.give_sending(d, call)),
// Delegate, to send its notification when it subscribes.
display: window.and_then(|w| child.give_sending(w, call)),
args: child.set_args(args),
console: None,
};
@@ -193,10 +201,11 @@ impl PermissionServer {
if let Some(d) = data {
let _ = libkos::fs::release(d);
}
close_window(window);
syscall::drop_cap(child.process);
return Err(code(reply::FAILED));
}
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data });
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window });
Ok((child.process, plan.granted))
}
@@ -271,6 +280,7 @@ impl PermissionServer {
if let Some(d) = r.data {
let _ = libkos::fs::release(d);
}
close_window(r.window);
syscall::drop_cap(r.process);
}
}
@@ -331,3 +341,10 @@ impl Server for PermissionServer {
self.reap(bits);
}
}
fn close_window(window: Option<CapSlot>) {
if let Some(w) = window {
libkos::display::close(w);
syscall::drop_cap(w);
}
}

View File

@@ -12,3 +12,5 @@ bench = false
types = { workspace = true }
libkos = { workspace = true }
server-framework = { workspace = true }
port-framebuffer = { workspace = true }
raster = { workspace = true }

View File

@@ -11,8 +11,9 @@ pub mod server;
#[path = "tests/mod.rs"]
mod tests;
use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH};
use libkos::display::{self, Canvas};
use libkos::input::{InputEvent, Key};
use port_framebuffer::Framebuffer;
use libkos::syscall;
use types::badge::Badge;
use types::bounded::CapSlot;
@@ -23,6 +24,9 @@ use types::terminal::{KEYS_BIT, TIMER_BIT};
const EP: CapSlot = CapSlot::const_new(0);
const DISPLAY: CapSlot = CapSlot::const_new(1);
const CELL_WIDTH: usize = raster::font::WIDTH as usize;
const CELL_HEIGHT: usize = raster::font::HEIGHT as usize;
const FG: Color = Color::rgb(220, 220, 220);
const BG: Color = Color::rgb(16, 16, 24);
/// Output is shown at most this long after it is written. The timebase is
@@ -30,12 +34,9 @@ const BG: Color = Color::rgb(16, 16, 24);
const FLUSH_DELAY: u64 = 20 * 10_000;
struct Screen {
/// Bound to the serving thread: the display signals it with
/// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
notify: CapSlot,
/// The terminal's window, all of which scrolls.
canvas: Canvas,
timer: CapSlot,
/// The screen's height, all of which scrolls.
height: Dimension,
}
/// A key as the line editor wants it, if it is one.
@@ -56,21 +57,22 @@ fn at(col: usize, row: usize) -> (ScreenCoord, ScreenCoord) {
impl server::Console for Screen {
fn draw_row(&mut self, row: usize, text: &[u8]) {
let (x, y) = at(0, row);
display::draw_text(DISPLAY, x, y, FG, BG, text);
raster::draw_text(&mut self.canvas, x, y, FG, BG, text);
}
fn draw_cursor(&mut self, col: usize, row: usize, under: u8) {
let (x, y) = at(col, row);
display::draw_text(DISPLAY, x, y, BG, FG, &[under]);
raster::draw_text(&mut self.canvas, x, y, BG, FG, &[under]);
}
fn flush(&mut self) {
display::flush(DISPLAY);
self.canvas.flush();
}
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);
let height = self.canvas.height();
self.canvas.scroll_up(ScreenCoord::new(0), height, by, BG);
}
fn flush_later(&mut self) {
@@ -79,7 +81,7 @@ impl server::Console for Screen {
}
fn poll_key(&mut self) -> Option<u8> {
while let Some(event) = display::poll_input(DISPLAY) {
while let Some(event) = display::poll_input(self.canvas.window()) {
if let Some(key) = byte(event) {
return Some(key);
}
@@ -95,11 +97,6 @@ impl server::Console for Screen {
let _ = syscall::reply_saved(reply, words, None);
}
fn take_focus(&mut self) {
display::subscribe(DISPLAY, self.notify, Badge::new(KEYS_BIT));
display::clear(DISPLAY, BG);
}
fn interrupt(&mut self, notification: CapSlot) {
let _ = syscall::signal(notification);
}
@@ -119,14 +116,17 @@ pub fn run() -> ! {
libkos::println!("terminal: no timer, exiting");
syscall::exit();
};
if !display::subscribe(DISPLAY, notify, Badge::new(KEYS_BIT)) {
libkos::println!("terminal: no input focus, exiting");
let Some(mut canvas) = display::open(DISPLAY, b"Terminal").and_then(|w| Canvas::attach(w.window)) else {
libkos::println!("terminal: no window, exiting");
syscall::exit();
};
if !display::subscribe(canvas.window(), notify, Badge::new(KEYS_BIT)) {
libkos::println!("terminal: no input, exiting");
syscall::exit();
}
let (w, h) = display::get_dimensions(DISPLAY);
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, height: h }, cols, rows);
canvas.clear(BG);
let cols = canvas.width().raw() as usize / CELL_WIDTH;
let rows = canvas.height().raw() as usize / CELL_HEIGHT;
let mut srv = server::TerminalServer::new(Screen { canvas, timer }, cols, rows);
server_framework::serve(&mut srv, EP)
}

View File

@@ -24,8 +24,6 @@ pub trait Console {
/// Keeps the caller of the request being handled, to answer later.
fn save_reply(&mut self) -> Option<SavedReply>;
fn answer(&mut self, reply: SavedReply, words: [usize; 4]);
/// Takes input focus back from whoever took it, and clears the screen.
fn take_focus(&mut self);
fn interrupt(&mut self, notification: CapSlot);
/// Lets go of a notification it was given.
fn forget(&mut self, notification: CapSlot);
@@ -259,15 +257,6 @@ impl<C: Console> Server for TerminalServer<C> {
// Kept for later: what goes back now is not sent.
types::terminal::READ => self.read(false).unwrap_or([0; 4]),
types::terminal::READ_SECRET => self.read(true).unwrap_or([0; 4]),
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();
[0; 4]
}
types::terminal::ON_INTERRUPT => match cap {
Some(n) if badge & types::terminal::CONTROL != 0 => {
if let Some(old) = self.on_interrupt.replace(n) {

View File

@@ -15,7 +15,6 @@ struct Fake {
typed: Vec<u8>,
flushes: usize,
timers: usize,
focus_taken: usize,
scrolled: usize,
/// Every row drawn, with how many cells.
rows_drawn: Vec<(usize, usize)>,
@@ -52,9 +51,6 @@ impl Console for Fake {
fn answer(&mut self, reply: SavedReply, words: [usize; 4]) {
self.answered.push((reply, words));
}
fn take_focus(&mut self) {
self.focus_taken += 1;
}
fn interrupt(&mut self, notification: CapSlot) {
self.interrupted.push(notification);
}
@@ -149,15 +145,6 @@ fn ctrl_c_drops_the_line_being_typed() {
assert_eq!(line_of(&mut t), b"ls\n");
}
#[test]
fn redraw_takes_focus_back_and_paints_every_row() {
let mut t = term(&[]);
t.handle_call(1, text_words(b"hi"), None);
t.handle_call(1, [types::terminal::REDRAW, 0, 0, 0], None);
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(&[]);

View File

@@ -49,78 +49,87 @@ 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;
/// The window protocol, shared by the display server and
/// `libkos::display` (see docs/ipc.md). The display owns the screen and
/// draws the OS bar; every app draws only into its own window's pixel
/// buffer, which the display shows while the window is in front.
pub mod window {
use super::{Dimension, ScreenCoord};
pub mod text {
use super::{Color, ScreenCoord};
/// Through a launcher's copy of the display endpoint (one init gave a
/// system program): opens a window for the app named in words 1..4
/// (`pack_name`). Reply `[OK, width, height, 0]` carrying the window
/// capability, which the launcher passes on; the new window is in front.
pub const OPEN: usize = 1;
/// Through a window: reply `[OK, width, height, 0]` carrying its pixel buffer, a
/// region the holder maps read-write: `width * height` pixels, rows
/// one after another, each pixel a `Color` as a little-endian u32.
pub const BUFFER: usize = 2;
/// Through a window: the rectangle (`pack_rect`) that changed.
pub const PRESENT: usize = 3;
/// Through a window: carries the notification signalled when input
/// for it arrives (only while it is in front).
pub const SUBSCRIBE: usize = 4;
/// Through a window: its next input event, as the input server words
/// it (tag 0: none).
pub const POLL: usize = 5;
/// Through a window: closes it; every copy of it stops working.
pub const CLOSE: usize = 6;
/// From init (unbadged) only: who is signed in, for the OS bar (words
/// 1..4, `pack_name`; empty when nobody is).
pub const SET_USER: usize = 7;
pub const TEXT: usize = 4;
pub const TEXT_BYTES: usize = 16;
/// From init (unbadged) only: word 1 a launcher's badge, which has
/// gone; every window it opened closes.
pub const FORGET: usize = 8;
/// The function key that brings the next window to the front; the
/// display keeps it to itself.
pub const SWITCH_KEY: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Text {
pub x: ScreenCoord,
pub y: ScreenCoord,
pub fg: Color,
pub bg: Color,
pub bytes: [u8; TEXT_BYTES],
pub len: usize,
/// On the badge of every window capability; launchers' badges are
/// small, so the two never meet.
pub const BADGE: usize = 1 << 61;
pub const OK: usize = 0;
pub const PIXEL_BYTES: usize = 4;
/// Longest name the OS bar shows.
pub const NAME_BYTES: usize = 24;
/// A name in three words, NUL padded, cut at `NAME_BYTES`.
pub fn pack_name(name: &[u8]) -> [usize; 3] {
let mut bytes = [0u8; NAME_BYTES];
let n = name.len().min(NAME_BYTES);
bytes[..n].copy_from_slice(&name[..n]);
core::array::from_fn(|i| {
u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])) as usize
})
}
impl Text {
/// `s` past `TEXT_BYTES` is cut off.
pub fn new(x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Color, s: &[u8]) -> Self {
let len = s.len().min(TEXT_BYTES);
let mut bytes = [0u8; TEXT_BYTES];
bytes[..len].copy_from_slice(&s[..len]);
Self {
x,
y,
fg,
bg,
bytes,
len,
}
/// The name in `words`, and how long it is.
pub fn unpack_name(words: [usize; 3]) -> ([u8; NAME_BYTES], usize) {
let mut bytes = [0u8; NAME_BYTES];
for (chunk, w) in bytes.chunks_mut(8).zip(words) {
chunk.copy_from_slice(&(w as u64).to_le_bytes());
}
let len = bytes.iter().position(|b| *b == 0).unwrap_or(NAME_BYTES);
(bytes, len)
}
pub fn chars(&self) -> &[u8] {
&self.bytes[..self.len.min(TEXT_BYTES)]
}
/// A rectangle in two words: `x | y << 16` and `w | h << 16`.
pub fn pack_rect(x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension) -> [usize; 2] {
[
x.raw() as usize | (y.raw() as usize) << 16,
w.raw() as usize | (h.raw() as usize) << 16,
]
}
pub fn words(&self) -> [usize; 4] {
let (lo, hi) = self.bytes.as_chunks::<8>().0.split_at(1);
[
TEXT | self.len << 8 | (self.fg.raw() as usize) << 16,
self.x.raw() as usize
| (self.y.raw() as usize) << 16
| (self.bg.raw() as usize) << 32,
u64::from_le_bytes(lo[0]) as usize,
u64::from_le_bytes(hi[0]) as usize,
]
}
pub fn from_words(w: [usize; 4]) -> Option<Self> {
let len = (w[0] >> 8) & 0xFF;
if w[0] & 0xFF != TEXT || len > TEXT_BYTES {
return None;
}
let mut bytes = [0u8; TEXT_BYTES];
for (b, word) in bytes.as_chunks_mut::<8>().0.iter_mut().zip(&w[2..]) {
*b = (*word as u64).to_le_bytes();
}
Some(Self {
x: ScreenCoord::new((w[1] & 0xFFFF) as u16),
y: ScreenCoord::new(((w[1] >> 16) & 0xFFFF) as u16),
fg: Color::from_raw((w[0] >> 16) as u32),
bg: Color::from_raw((w[1] >> 32) as u32),
bytes,
len,
})
}
pub fn unpack_rect([xy, wh]: [usize; 2]) -> (ScreenCoord, ScreenCoord, Dimension, Dimension) {
let half = |w: usize, shift: u32| ((w >> shift) & 0xFFFF) as u16;
(
ScreenCoord::new(half(xy, 0)),
ScreenCoord::new(half(xy, 16)),
Dimension::new(half(wh, 0)),
Dimension::new(half(wh, 16)),
)
}
#[cfg(test)]
@@ -128,26 +137,18 @@ pub mod text {
use super::*;
#[test]
fn text_round_trips() {
let t = Text::new(
ScreenCoord::new(1272),
ScreenCoord::new(784),
Color::rgb(1, 2, 3),
Color::rgb(4, 5, 6),
b"$ ls -la /home/x",
fn names_and_rects_round_trip() {
let (bytes, len) = unpack_name(pack_name(b"Snake"));
assert_eq!(&bytes[..len], b"Snake");
let (bytes, len) = unpack_name(pack_name(&[b'a'; 40]));
assert_eq!(&bytes[..len], &[b'a'; NAME_BYTES]);
let r = (
ScreenCoord::new(1279),
ScreenCoord::new(783),
Dimension::new(1),
Dimension::new(784),
);
assert_eq!(Text::from_words(t.words()), Some(t));
assert_eq!(Text::new(t.x, t.y, t.fg, t.bg, &[b'a'; 40]).len, TEXT_BYTES);
assert_eq!(unpack_rect(pack_rect(r.0, r.1, r.2, r.3)), r);
}
}
}
/// Input through the display server: an app subscribes with a notification
/// and is then the one keys go to (focus); when it is signalled it polls.
/// A poll's reply is an input event exactly as the input server sends it
/// (tag 0: no event).
pub mod input {
/// Carries the notification to signal; the subscriber takes focus.
pub const SUBSCRIBE: usize = 5;
pub const POLL: usize = 6;
}

View File

@@ -3,3 +3,11 @@
pub const POLL_EVENT: usize = 0;
/// Carries a notification the server signals whenever new events arrive.
pub const SUBSCRIBE: usize = 2;
/// An event as the input server words it: `[tag, kind, extra, 0]`.
pub mod event {
pub const KEY_DOWN: usize = 1;
pub const KEY_UP: usize = 2;
/// A function key: `extra` is its number (F1 is 1).
pub const FUNCTION_KEY: usize = 10;
}

View File

@@ -8,9 +8,6 @@ pub const CLEAR: usize = 11;
/// As `READ`, but what is typed shows as `*` and is forgotten once read:
/// for passcodes.
pub const READ_SECRET: usize = 14;
/// Repaints the whole screen and takes input focus back, after a program
/// that drew on the screen itself.
pub const REDRAW: usize = 12;
/// Carries a notification the terminal signals on Ctrl+C while no line is
/// being read. Only through a copy whose badge has `CONTROL`; the latest
/// one counts, so a restarted shell simply registers again.

View File

@@ -83,7 +83,7 @@ optional one is simply not granted.
| Capability | What the app gets |
|---|---|
| `display` | the display server: the screen, and the keyboard while in focus |
| `display` | a window of its own, named from its manifest: its pixels, and the keyboard while it is in front |
| `storage` | the user's `data/<id>/`, read-write (its own folder only) |
## Decisions
@@ -143,10 +143,9 @@ permission manager:
The user withdraws a capability: the decision changes and the next launch
gets less. A running app that held it is **stopped**: all its access dies
at once. (Endpoint capabilities like `display` are copies the permission
manager cannot take back from one app alone yet; once servers issue
per-client badges, as storage does, revoking a single capability from a
running app will not need to stop it.)
at once. (Capabilities like `display` could now be taken back alone, as
storage's grants and the display's windows are issued per app; stopping
the app is still what revoking does.)
### Not yet

View File

@@ -163,13 +163,34 @@ every holder (storage revokes its own copy, so the kernel deletes the
rest), closes their files and unmaps their buffers. Badge 0 (init) is the
whole disk; a badge storage did not issue, or took back, is `DENIED`.
The display server alone holds the screen (`types::display::window`,
client `libkos::display`). It draws the OS bar along the top: every
window's name, the one in front highlighted, and who is signed in (init
says, `SET_USER`). Below it, the window in front fills the screen; one is
in front at a time, a new one comes to the front, and F1 brings the next.
A window is opened (`OPEN`, with the name the bar shows) only through a
launcher's copy of the display endpoint, one init gave a system program:
the terminal opens its own, the permission manager one for each app
granted `display`, named from its manifest. The reply carries a window
capability the display mints itself (badge bit `BADGE`), which can do
nothing but use that window: map its pixel buffer (`BUFFER`), say what
changed (`PRESENT`), take its input (`SUBSCRIBE`, `POLL`), close it. So an
app can neither open more windows nor name its own, nor draw outside its
window. The buffers are the display's: one per slot, at most four, made
once and reused (blank) for each new window, so its memory has a fixed
ceiling; closing a window takes every copy of its buffer back. A window's
opener is remembered: when init sees a program that uses the display go,
it says so (`FORGET`, through its printer thread so it never waits on the
display), and that program's windows close.
Input reaches apps through the display server, the input server's only
client. It subscribes to the input server with a notification, drains it
when signalled, and passes events on to the app in focus: the last one to
subscribe (`types::display::input`). Apps out of focus get nothing.
when signalled, keeps F1, and queues the rest for the window in front,
signalling the notification it subscribed with. Windows behind get
nothing.
The terminal server takes focus and draws text with the display server's
font. Its output side is the log protocol (`types::log`), so a program whose
The terminal server draws its text into its own window (`raster`, which
holds the font). Its output side is the log protocol (`types::log`), so a program whose
log endpoint is the terminal prints there with `println!`: standard output
is whatever text endpoint a program was given. `READ` (`types::terminal`)
blocks until a line is typed and returns it in chunks, the last ending in
@@ -177,8 +198,7 @@ blocks until a line is typed and returns it in chunks, the last ending in
kept for the next one, except Ctrl+C, which signals the notification the
latest `ON_INTERRUPT` caller gave. Only a terminal copy whose badge has
`CONTROL` may register: init mints one for the shell alone, and the copy
the shell passes on to apps is its plain one. `REDRAW` repaints the screen and takes
input focus back.
the shell passes on to apps is its plain one.
The desktop is the shell, given the terminal, the permission manager,
the user's `home/` (its `/`) and signing out. Everything the owner runs is an app (docs/apps.md): a
@@ -187,8 +207,7 @@ client `libkos::apps`) to run it, passing the terminal as its output and a
read-only grant to the current folder (`cd`, `pwd`). The permission manager
adds what the app's manifest asks for and the owner allowed (the display,
its own data folder), runs it, and hands back the process; the shell waits
for its death (Ctrl+C kills it), releases the grant, and has the terminal
redraw if the app had the display. `install`, `apps`, `allow` and `revoke`
for its death (Ctrl+C kills it) and releases the grant. `install`, `apps`, `allow` and `revoke`
manage apps.
Only init holds `ProcessCreate`. Everyone else runs programs through init's