diff --git a/Cargo.lock b/Cargo.lock index 9efba8a..6e1d77e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 734516f..c2fb166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/apps/shell/src/main.rs b/crates/apps/shell/src/main.rs index 633fde2..f9b1218 100644 --- a/crates/apps/shell/src/main.rs +++ b/crates/apps/shell/src/main.rs @@ -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), } diff --git a/crates/apps/snake/Cargo.toml b/crates/apps/snake/Cargo.toml index 5ae4302..1dbe2b6 100644 --- a/crates/apps/snake/Cargo.toml +++ b/crates/apps/snake/Cargo.toml @@ -12,5 +12,4 @@ bench = false types = { workspace = true } libkos = { workspace = true } -display-server = { workspace = true } -server-framework = { workspace = true } +port-framebuffer = { workspace = true } diff --git a/crates/apps/snake/src/main.rs b/crates/apps/snake/src/main.rs index 2c4edce..7321ee4 100644 --- a/crates/apps/snake/src/main.rs +++ b/crates/apps/snake/src/main.rs @@ -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), diff --git a/crates/bins/display-qemu/src/main.rs b/crates/bins/display-qemu/src/main.rs index 4ae2495..4028281 100644 --- a/crates/bins/display-qemu/src/main.rs +++ b/crates/bins/display-qemu/src/main.rs @@ -37,18 +37,12 @@ fn subscribe_to_input() -> Option { sent.ok().map(|_| INPUT) } -fn server(fb: F, input: Option) -> display_server::server::DisplayServer { - match input { - Some(input) => display_server::server::DisplayServer::with_input(fb, input), - None => display_server::server::DisplayServer::new(fb), - } +fn server(fb: F, input: Option) -> display_server::server::DisplayServer { + 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) -> ! { - 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) -> ! { let mut srv = server(fb, input); server_framework::serve(&mut srv, EP) } diff --git a/crates/drivers/framebuffer/mock/src/lib.rs b/crates/drivers/framebuffer/mock/src/lib.rs index ec6e4de..5794480 100644 --- a/crates/drivers/framebuffer/mock/src/lib.rs +++ b/crates/drivers/framebuffer/mock/src/lib.rs @@ -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 { + pixels: [[Color; W]; H], + pub flushes: usize, +} + +impl GridFramebuffer { + 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 { + self.pixels.get(y).and_then(|row| row.get(x)).copied() + } +} + +impl Default for GridFramebuffer { + fn default() -> Self { + Self::new() + } +} + +impl Framebuffer for GridFramebuffer { + 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] }; + } + } +} diff --git a/crates/drivers/input/virtio/src/keymap.rs b/crates/drivers/input/virtio/src/keymap.rs index eb8520b..336adca 100644 --- a/crates/drivers/input/virtio/src/keymap.rs +++ b/crates/drivers/input/virtio/src/keymap.rs @@ -37,6 +37,8 @@ pub fn key(code: u16, shift: bool) -> Option { 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 { 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)); diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 7ff33bf..0521be3 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -63,6 +63,9 @@ pub(crate) struct Launched { /// to take back when it dies. pub(crate) grants: [Option; 2], pub(crate) spawn: Option, + /// 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, } impl<'a> Launcher<'a> { @@ -101,7 +104,7 @@ impl<'a> Launcher<'a> { (self.storage, self.scratch) } - fn endpoint(&self, name: &str) -> Option { + pub(crate) fn endpoint(&self, name: &str) -> Option { 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 diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 35bdfd6..70fe447 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -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), ]; diff --git a/crates/init/src/output.rs b/crates/init/src/output.rs index 61db0eb..e673ccf 100644 --- a/crates/init/src/output.rs +++ b/crates/init/src/output.rs @@ -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 { 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); + } + } } } diff --git a/crates/init/src/session.rs b/crates/init/src/session.rs index 2d270ec..d9ade65 100644 --- a/crates/init/src/session.rs +++ b/crates/init/src/session.rs @@ -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(); diff --git a/crates/init/src/supervise.rs b/crates/init/src/supervise.rs index da07cd7..5cd371f 100644 --- a/crates/init/src/supervise.rs +++ b/crates/init/src/supervise.rs @@ -83,14 +83,23 @@ pub(crate) struct Supervised { ep: Option, /// 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, +} + +/// What a program held through init, to take back when it goes. +struct Held { + grants: [Option; 2], + spawn: Option, + display_badge: Option, } 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; 2], Option)> { - let held = (self.grants, self.spawn); + fn take_held(&mut self) -> Option { + 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); diff --git a/crates/libkos/Cargo.toml b/crates/libkos/Cargo.toml index b112228..ebc2398 100644 --- a/crates/libkos/Cargo.toml +++ b/crates/libkos/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] types = { workspace = true } +port-framebuffer = { workspace = true } diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 510c36f..a8848b5 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -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 { + 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 { - 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 { - 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 { + 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, +} + +impl Canvas { + /// Maps `window`'s buffer. + pub fn attach(window: CapSlot) -> Option { + 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] +} diff --git a/crates/libkos/src/input.rs b/crates/libkos/src/input.rs index f3fd0ed..b08f2ae 100644 --- a/crates/libkos/src/input.rs +++ b/crates/libkos/src/input.rs @@ -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 { 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, } } diff --git a/crates/libkos/src/terminal.rs b/crates/libkos/src/terminal.rs index 36f4932..3a42bd4 100644 --- a/crates/libkos/src/terminal.rs +++ b/crates/libkos/src/terminal.rs @@ -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 { diff --git a/crates/ports/input-device/src/lib.rs b/crates/ports/input-device/src/lib.rs index 0ccfe14..07b82ad 100644 --- a/crates/ports/input-device/src/lib.rs +++ b/crates/ports/input-device/src/lib.rs @@ -13,6 +13,8 @@ pub enum Key { Backspace, Tab, Char(u8), + /// F1 is 1. + Function(u8), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/raster/Cargo.toml b/crates/raster/Cargo.toml new file mode 100644 index 0000000..9ebcbc8 --- /dev/null +++ b/crates/raster/Cargo.toml @@ -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 } diff --git a/crates/servers/display-server/src/font/LICENSE-spleen b/crates/raster/src/font/LICENSE-spleen similarity index 100% rename from crates/servers/display-server/src/font/LICENSE-spleen rename to crates/raster/src/font/LICENSE-spleen diff --git a/crates/servers/display-server/src/font/mod.rs b/crates/raster/src/font/mod.rs similarity index 99% rename from crates/servers/display-server/src/font/mod.rs rename to crates/raster/src/font/mod.rs index 82ad163..b3b6754 100644 --- a/crates/servers/display-server/src/font/mod.rs +++ b/crates/raster/src/font/mod.rs @@ -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. diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs new file mode 100644 index 0000000..571e9c2 --- /dev/null +++ b/crates/raster/src/lib.rs @@ -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)); + } +} diff --git a/crates/servers/display-server/Cargo.toml b/crates/servers/display-server/Cargo.toml index 35cb5bc..c3b6b78 100644 --- a/crates/servers/display-server/Cargo.toml +++ b/crates/servers/display-server/Cargo.toml @@ -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 } diff --git a/crates/servers/display-server/src/focus.rs b/crates/servers/display-server/src/focus.rs deleted file mode 100644 index f761c82..0000000 --- a/crates/servers/display-server/src/focus.rs +++ /dev/null @@ -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 { - /// 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 Focus { - 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 { - 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 Default for Focus { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/servers/display-server/src/host.rs b/crates/servers/display-server/src/host.rs new file mode 100644 index 0000000..4e7a22a --- /dev/null +++ b/crates/servers/display-server/src/host.rs @@ -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, + buffers: [Option<(CapSlot, Window)>; MAX_WINDOWS], +} + +impl KernelHost { + pub fn new(ep: CapSlot, input: Option) -> Self { + Self { ep, input, buffers: [None; MAX_WINDOWS] } + } +} + +impl Host for KernelHost { + fn buffer(&mut self, index: usize, pixels: usize) -> Option { + 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::(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 { + 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) + } +} diff --git a/crates/servers/display-server/src/lib.rs b/crates/servers/display-server/src/lib.rs index f561f26..12e4490 100644 --- a/crates/servers/display-server/src/lib.rs +++ b/crates/servers/display-server/src/lib.rs @@ -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"] diff --git a/crates/servers/display-server/src/protocol.rs b/crates/servers/display-server/src/protocol.rs deleted file mode 100644 index 239f09f..0000000 --- a/crates/servers/display-server/src/protocol.rs +++ /dev/null @@ -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 { - 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 { - 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, - } - } -} diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index 466b7d9..bd73185 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -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 { - fb: F, - /// The input server, which signals this server's bound notification. - input: Option, - focus: Focus, +/// 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; + /// 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; + /// 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 DisplayServer { - 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 { + None + } +} + +impl Encode for Never { + fn encode(&self) -> [usize; 4] { + [MALFORMED, 0, 0, 0] + } +} + +pub struct DisplayServer { + fb: F, + host: H, + windows: Windows, + /// Each open window's capability, as minted. + caps: [Option; MAX_WINDOWS], + user: [u8; NAME_BYTES], + user_len: usize, +} + +impl DisplayServer { + 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) { + 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 Server for DisplayServer { - type Request = DisplayRequest; - type Response = DisplayResponse; - - fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { - 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) -> ([usize; 4], Option) { + 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 Server for DisplayServer { + 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) -> ([usize; 4], Option) { + 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); } } } diff --git a/crates/servers/display-server/src/tests/focus.rs b/crates/servers/display-server/src/tests/focus.rs deleted file mode 100644 index 852ffc6..0000000 --- a/crates/servers/display-server/src/tests/focus.rs +++ /dev/null @@ -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 = 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 = 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 = 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])); -} diff --git a/crates/servers/display-server/src/tests/mod.rs b/crates/servers/display-server/src/tests/mod.rs index 27b16ff..4a3e3a3 100644 --- a/crates/servers/display-server/src/tests/mod.rs +++ b/crates/servers/display-server/src/tests/mod.rs @@ -1,3 +1,2 @@ -mod focus; -mod protocol; mod server; +mod windows; diff --git a/crates/servers/display-server/src/tests/protocol.rs b/crates/servers/display-server/src/tests/protocol.rs deleted file mode 100644 index c583582..0000000 --- a/crates/servers/display-server/src/tests/protocol.rs +++ /dev/null @@ -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)); -} diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs index 804a808..b08fe84 100644 --- a/crates/servers/display-server/src/tests/server.rs +++ b/crates/servers/display-server/src/tests/server.rs @@ -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 { - 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>>, + minted: Vec, + revoked: Vec, + reclaimed: Vec, + signalled: Vec, + 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 { + 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 { + 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, 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 { + 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"); } diff --git a/crates/servers/display-server/src/tests/windows.rs b/crates/servers/display-server/src/tests/windows.rs new file mode 100644 index 0000000..9fe6cb4 --- /dev/null +++ b/crates/servers/display-server/src/tests/windows.rs @@ -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) -> 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::::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::::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::::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::::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::::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::>(), [a, b]); +} diff --git a/crates/servers/display-server/src/windows.rs b/crates/servers/display-server/src/windows.rs new file mode 100644 index 0000000..a04f7ab --- /dev/null +++ b/crates/servers/display-server/src/windows.rs @@ -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 { + badge: usize, + /// The launcher's badge: its windows go when it does. + opener: usize, + name: [u8; NAME_BYTES], + name_len: usize, + notify: Option, + queue: Queue, +} + +pub struct Windows { + slots: [Option>; MAX_WINDOWS], + front: Option, + /// Makes each window's badge new, so a closed window's never returns. + opened: usize, +} + +impl Windows { + 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 { + 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 + '_ { + 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 { + (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 { + let next = self.after(self.front?)?; + (Some(next) != self.front).then(|| { + self.front = Some(next); + next + }) + } + + pub fn front(&self) -> Option { + 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 { + 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 + '_ { + 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 Default for Windows { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/servers/input-server/src/protocol.rs b/crates/servers/input-server/src/protocol.rs index 0ca118d..7861d4b 100644 --- a/crates/servers/input-server/src/protocol.rs +++ b/crates/servers/input-server/src/protocol.rs @@ -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 { 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, } } diff --git a/crates/servers/permission-manager/src/server.rs b/crates/servers/permission-manager/src/server.rs index 47dec0b..41d49ab 100644 --- a/crates/servers/permission-manager/src/server.rs +++ b/crates/servers/permission-manager/src/server.rs @@ -43,6 +43,8 @@ struct Running { granted: Capabilities, /// Its data folder grant, taken back when it dies. data: Option, + /// Its window, closed when it dies. + window: Option, } 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) { + if let Some(w) = window { + libkos::display::close(w); + syscall::drop_cap(w); + } +} diff --git a/crates/servers/terminal-server/Cargo.toml b/crates/servers/terminal-server/Cargo.toml index 2ba5cee..71de1cf 100644 --- a/crates/servers/terminal-server/Cargo.toml +++ b/crates/servers/terminal-server/Cargo.toml @@ -12,3 +12,5 @@ bench = false types = { workspace = true } libkos = { workspace = true } server-framework = { workspace = true } +port-framebuffer = { workspace = true } +raster = { workspace = true } diff --git a/crates/servers/terminal-server/src/lib.rs b/crates/servers/terminal-server/src/lib.rs index 777ee72..5f47209 100644 --- a/crates/servers/terminal-server/src/lib.rs +++ b/crates/servers/terminal-server/src/lib.rs @@ -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 { - 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) } diff --git a/crates/servers/terminal-server/src/server.rs b/crates/servers/terminal-server/src/server.rs index 14886f5..48fe731 100644 --- a/crates/servers/terminal-server/src/server.rs +++ b/crates/servers/terminal-server/src/server.rs @@ -24,8 +24,6 @@ pub trait Console { /// Keeps the caller of the request being handled, to answer later. fn save_reply(&mut self) -> Option; 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 Server for TerminalServer { // 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) { diff --git a/crates/servers/terminal-server/src/tests/server.rs b/crates/servers/terminal-server/src/tests/server.rs index c2991c0..b722b61 100644 --- a/crates/servers/terminal-server/src/tests/server.rs +++ b/crates/servers/terminal-server/src/tests/server.rs @@ -15,7 +15,6 @@ struct Fake { typed: Vec, 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(&[]); diff --git a/crates/types/src/display.rs b/crates/types/src/display.rs index b8dce57..e30c80e 100644 --- a/crates/types/src/display.rs +++ b/crates/types/src/display.rs @@ -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 { - 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; -} diff --git a/crates/types/src/input.rs b/crates/types/src/input.rs index 8e6e603..26ab49a 100644 --- a/crates/types/src/input.rs +++ b/crates/types/src/input.rs @@ -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; +} diff --git a/crates/types/src/terminal.rs b/crates/types/src/terminal.rs index 8ce0120..c38822d 100644 --- a/crates/types/src/terminal.rs +++ b/crates/types/src/terminal.rs @@ -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. diff --git a/docs/apps.md b/docs/apps.md index 8ec6beb..a956009 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -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//`, 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 diff --git a/docs/ipc.md b/docs/ipc.md index 34691eb..279d085 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -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