diff --git a/crates/apps/snake/src/main.rs b/crates/apps/snake/src/main.rs index 56cae66..d439a98 100644 --- a/crates/apps/snake/src/main.rs +++ b/crates/apps/snake/src/main.rs @@ -12,8 +12,8 @@ use libkos::syscall; use server_framework::Encode; use display_server::protocol::DisplayRequest; -const INPUT_EP: CapSlot = CapSlot::const_new(0); -const DISPLAY_EP: CapSlot = CapSlot::const_new(1); +/// Input comes through the display server too, while this app has focus. +const DISPLAY_EP: CapSlot = CapSlot::const_new(0); const TICK_DELAY: u64 = 5_000_000; const MAX_RENDER_CMDS: usize = 140; @@ -37,6 +37,8 @@ fn flush() { #[unsafe(no_mangle)] extern "C" fn main() { + // Takes focus; the game polls every tick, so it never waits on the signal. + let _ = libkos::display::subscribe_input(DISPLAY_EP); let mut snake = Snake::new(); let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS]; @@ -65,7 +67,7 @@ extern "C" fn main() { fn handle_input(snake: &mut Snake) { // Drain all pending events, apply the latest direction loop { - let Ok([tag, kind, _, _]) = syscall::request(INPUT_EP, [0; 4]) else { + let Ok([tag, kind, _, _]) = syscall::request(DISPLAY_EP, [types::display::input::POLL, 0, 0, 0]) else { break; }; if tag == 0 { break; } // NoEvent — queue drained diff --git a/crates/bins/display-qemu/src/main.rs b/crates/bins/display-qemu/src/main.rs index 968ccf9..9de47ef 100644 --- a/crates/bins/display-qemu/src/main.rs +++ b/crates/bins/display-qemu/src/main.rs @@ -5,25 +5,50 @@ extern crate libkos; use types::bounded::CapSlot; +/// Where init put them (see init's manifest: own endpoint, then `uses`). +const EP: CapSlot = CapSlot::const_new(0); +const INPUT: CapSlot = CapSlot::const_new(1); + #[unsafe(no_mangle)] extern "C" fn main() { - let ep = CapSlot::const_new(0); // Init hands over the GPU, or nothing if the machine has none. - let dev = libkos::device::receive_device(ep); + let dev = libkos::device::receive_device(EP); + let input = subscribe_to_input(); if let Some(fb) = dev.mmio.and_then(driver_framebuffer_virtio::VirtioFramebuffer::try_new) { - run_virtio(fb, ep); + run_virtio(fb, input); } libkos::println!("display: no GPU, running headless"); - run_headless(ep); + run_headless(input); +} + +/// Has the input server signal this thread whenever events arrive; the +/// server then passes them on to the app in focus. +fn subscribe_to_input() -> Option { + use types::badge::Badge; + use types::permission::{Permission, PermissionSet}; + let notify = libkos::syscall::create_notification().ok()?; + libkos::syscall::bind_notification(notify).ok()?; + let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate); + let copy = libkos::syscall::mint(notify, Badge::NONE, signal).ok()?; + let sent = libkos::syscall::call(INPUT, [types::input::SUBSCRIBE, 0, 0, 0], Some(copy)); + libkos::syscall::drop_cap(copy); + 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), + } } #[inline(never)] -fn run_virtio(mut fb: driver_framebuffer_virtio::VirtioFramebuffer, ep: CapSlot) -> ! { +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(); - let mut srv = display_server::server::DisplayServer::new(fb); - server_framework::serve(&mut srv, ep) + let mut srv = server(fb, input); + server_framework::serve(&mut srv, EP) } /// No GPU: clients still get answers (and the dimensions they draw for); @@ -52,7 +77,7 @@ impl port_framebuffer::Framebuffer for Headless { } #[inline(never)] -fn run_headless(ep: CapSlot) -> ! { - let mut srv = display_server::server::DisplayServer::new(Headless); - server_framework::serve(&mut srv, ep) +fn run_headless(input: Option) -> ! { + let mut srv = server(Headless, input); + server_framework::serve(&mut srv, EP) } diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index d549b96..4d29c5a 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -27,6 +27,8 @@ pub(crate) struct Launcher<'a> { devices: Devices, /// The endpoint of every server started so far, by name. served: [Option<(&'static str, CapSlot)>; MAX_SERVED], + /// The badge on the next client's endpoint copy (0 is init's own). + next_badge: usize, /// Init's own endpoint, for the spawn service. pub(crate) spawn_ep: CapSlot, /// Bound to init's thread: spawned children's deaths, a bit each. @@ -42,7 +44,7 @@ impl<'a> Launcher<'a> { ) -> Result { let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?; let deaths = syscall::create_notification().map_err(|_| "no death notification")?; - Ok(Self { storage, scratch, devices, served: [None; MAX_SERVED], spawn_ep, deaths, spawner: Spawner::new() }) + Ok(Self { storage, scratch, devices, served: [None; MAX_SERVED], next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() }) } pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) { @@ -68,11 +70,18 @@ impl<'a> Launcher<'a> { } false => None, }; - // A client only calls: Execute, never Read, so it cannot pose as the - // server and take other clients' requests. + // 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. for name in p.uses { let ep = self.endpoint(name).ok_or("needs a server that is not running")?; - syscall::delegate(ep, child, perms(&[Permission::Execute])).ok_or("cannot give an endpoint")?; + let badge = Badge::new(self.next_badge); + self.next_badge += 1; + 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])); + syscall::drop_cap(copy); + given.ok_or("cannot give an endpoint")?; } if let Some(allowance) = p.spawns { self.give_spawn(child, allowance)?; diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 93c3c5d..5de7d1a 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -16,7 +16,8 @@ pub struct Program { /// Serves an endpoint, which it finds in slot 0. pub serves: bool, pub device: Option, - /// Earlier programs' endpoints it may call, in its next slots. + /// Earlier programs' endpoints it may call, in its next slots; each + /// program's copy carries its own badge, so a server can tell them apart. pub uses: &'static [&'static str], /// Takes the console over from init; everyone else prints through it. pub console: bool, @@ -51,12 +52,14 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog /// Started on every boot. The log server comes first so the rest can print. pub const SERVERS: &[Program] = &[ Program { console: true, ..server("log", b"log.elf", SMALL) }, + Program { device: Some(Device::Input), ..server("input", b"input.elf", Limits { regions: 4, ..SMALL }) }, + // The input server's only client: it passes input on to the app in focus. Program { device: Some(Device::Gpu), + uses: &["input"], // The framebuffer alone is ~1000 pages at 1280x800. ..server("display", b"display.elf", Limits { pages: 1280, regions: 4, ..SMALL }) }, - Program { device: Some(Device::Input), ..server("input", b"input.elf", Limits { regions: 4, ..SMALL }) }, server("permis", b"permis.elf", SMALL), server("session", b"session.elf", SMALL), ]; @@ -73,6 +76,6 @@ pub const TESTS: &[Program] = &[Program { /// The desktop; the test image runs the SMP self-test instead. pub const APPS: &[Program] = &[Program { serves: false, - uses: &["input", "display"], + uses: &["display"], ..server("snake", b"snake.elf", Limits { pages: 128, ..SMALL }) }]; diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 464e600..3465832 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -50,3 +50,23 @@ pub fn draw_text(ep: CapSlot, x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Col /// 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; + use types::permission::{Permission, PermissionSet}; + let notify = syscall::create_notification().ok()?; + // Signal only (Delegate just to send it; the transfer strips it). + let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate); + let copy = syscall::mint(notify, Badge::NONE, signal).ok()?; + let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy)); + syscall::drop_cap(copy); + (sent.ok()?.words()[0] == 0).then_some(notify) +} + +/// 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()?; + crate::input::decode(words) +} diff --git a/crates/libkos/src/input.rs b/crates/libkos/src/input.rs index 718e9f6..f3fd0ed 100644 --- a/crates/libkos/src/input.rs +++ b/crates/libkos/src/input.rs @@ -38,7 +38,11 @@ fn decode_key(kind: usize, extra: usize) -> Option { } pub fn poll_event(ep: CapSlot) -> Option { - let [tag, kind, extra, _] = syscall::request(ep, [0; 4]).ok()?; + decode(syscall::request(ep, [0; 4]).ok()?) +} + +/// An event as the input server words it. +pub fn decode([tag, kind, extra, _]: [usize; 4]) -> Option { match tag { 0 => None, 1 => decode_key(kind, extra).map(InputEvent::KeyDown), diff --git a/crates/servers/display-server/Cargo.toml b/crates/servers/display-server/Cargo.toml index ec00464..35cb5bc 100644 --- a/crates/servers/display-server/Cargo.toml +++ b/crates/servers/display-server/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] types = { workspace = true } server-framework = { workspace = true } +libkos = { workspace = true } port-framebuffer = { workspace = true } [dev-dependencies] diff --git a/crates/servers/display-server/src/focus.rs b/crates/servers/display-server/src/focus.rs new file mode 100644 index 0000000..f761c82 --- /dev/null +++ b/crates/servers/display-server/src/focus.rs @@ -0,0 +1,56 @@ +//! 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/lib.rs b/crates/servers/display-server/src/lib.rs index 51c2760..07d841b 100644 --- a/crates/servers/display-server/src/lib.rs +++ b/crates/servers/display-server/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] +pub mod focus; pub mod font; pub mod protocol; pub mod server; diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index 595afe4..b56dcaa 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -1,19 +1,29 @@ -use server_framework::Server; +use server_framework::{Decode, Encode, MALFORMED, Server}; +use types::bounded::CapSlot; use crate::protocol::{DisplayRequest, DisplayResponse}; use port_framebuffer::Framebuffer; use types::display::text::Text; use types::display::{Dimension, ScreenCoord}; +use crate::focus::Focus; use crate::font; pub struct DisplayServer { fb: F, + /// The input server, which signals this server's bound notification. + input: Option, + focus: Focus, } impl DisplayServer { pub fn new(fb: F) -> Self { - Self { fb } + Self { fb, input: None, focus: Focus::new() } + } + + /// 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) } } pub fn framebuffer(&self) -> &F { @@ -47,6 +57,39 @@ 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) + } + (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), + 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; + } + 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 } => { diff --git a/crates/servers/display-server/src/tests/focus.rs b/crates/servers/display-server/src/tests/focus.rs new file mode 100644 index 0000000..852ffc6 --- /dev/null +++ b/crates/servers/display-server/src/tests/focus.rs @@ -0,0 +1,36 @@ +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 aa77244..27b16ff 100644 --- a/crates/servers/display-server/src/tests/mod.rs +++ b/crates/servers/display-server/src/tests/mod.rs @@ -1,2 +1,3 @@ +mod focus; mod protocol; mod server; diff --git a/crates/servers/input-server/src/server.rs b/crates/servers/input-server/src/server.rs index 5936882..72e3078 100644 --- a/crates/servers/input-server/src/server.rs +++ b/crates/servers/input-server/src/server.rs @@ -1,4 +1,4 @@ -use server_framework::Server; +use server_framework::{Decode, Encode, MALFORMED, Server}; use port_input_device::{InputDevice, InputEvent}; use types::bounded::CapSlot; @@ -14,12 +14,14 @@ pub struct InputServer { events: [Option; BUFFERED], head: usize, len: usize, + /// Signalled when events arrive (the display server's notification). + subscriber: Option, } impl InputServer { /// A server that reads the device only when a client asks. pub fn new(device: D) -> Self { - Self { device, irq: None, events: [None; BUFFERED], head: 0, len: 0 } + Self { device, irq: None, events: [None; BUFFERED], head: 0, len: 0, subscriber: None } } /// A server woken by `irq` (routed and bound by the caller): it drains @@ -61,6 +63,10 @@ impl InputServer { &self.device } + pub fn subscriber(&self) -> Option { + self.subscriber + } + #[cfg(test)] pub fn replace_device_for_test(&mut self, device: D) { self.device = device; @@ -82,9 +88,25 @@ impl Server for InputServer { } } + fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { + if words[0] == types::input::SUBSCRIBE { + self.subscriber = cap; + return ([0; 4], None); + } + match InputRequest::decode(words) { + Some(request) => (self.handle(request).encode(), None), + None => ([MALFORMED, 0, 0, 0], None), + } + } + fn signal(&mut self, _bits: usize) { + let mut arrived = false; while let Some(event) = self.device.poll() { self.push(event); + arrived = true; + } + if let (true, Some(subscriber)) = (arrived, self.subscriber) { + let _ = libkos::syscall::signal(subscriber); } self.device.acknowledge(); if let Some(irq) = self.irq { diff --git a/crates/servers/input-server/src/tests/server.rs b/crates/servers/input-server/src/tests/server.rs index 0db94a9..e19af49 100644 --- a/crates/servers/input-server/src/tests/server.rs +++ b/crates/servers/input-server/src/tests/server.rs @@ -1,7 +1,7 @@ use crate::adapters::mock::MockInputDevice; use crate::protocol::*; use crate::server::InputServer; -use server_framework::Server; +use server_framework::{Decode, Encode, Server}; fn test_server() -> InputServer { InputServer::new(MockInputDevice::new()) @@ -85,3 +85,14 @@ fn a_full_buffer_drops_the_oldest_event() { InputResponse::Event(InputEvent::KeyDown(Key::Char(8))) ); } + +#[test] +fn a_subscription_is_kept_and_other_requests_still_work() { + let mut server = test_server(); + let note = types::bounded::CapSlot::new(7); + let (reply, _) = server.handle_call(0, [types::input::SUBSCRIBE, 0, 0, 0], note); + assert_eq!(reply, [0; 4]); + assert_eq!(server.subscriber(), note); + let (reply, _) = server.handle_call(0, InputRequest::PollEvent.encode(), None); + assert_eq!(InputResponse::decode(reply), Some(InputResponse::NoEvent)); +} diff --git a/crates/types/src/display.rs b/crates/types/src/display.rs index f3f5171..dfc7f59 100644 --- a/crates/types/src/display.rs +++ b/crates/types/src/display.rs @@ -123,3 +123,13 @@ pub mod text { } } } + +/// 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 new file mode 100644 index 0000000..8e6e603 --- /dev/null +++ b/crates/types/src/input.rs @@ -0,0 +1,5 @@ +//! Input server requests other crates need to name. + +pub const POLL_EVENT: usize = 0; +/// Carries a notification the server signals whenever new events arrive. +pub const SUBSCRIBE: usize = 2; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index fc982e2..3116171 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -8,6 +8,7 @@ pub mod budget; pub mod display; pub mod generation; pub mod id; +pub mod input; pub mod log; pub mod macros; pub mod mapping; diff --git a/docs/ipc.md b/docs/ipc.md index 2faf859..72a563c 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -84,6 +84,16 @@ and budget listed there. Storage, started before the log server, receives its log endpoint from init later (`SET_LOG`, accepted only from the unbadged holder of its endpoint). +A program's `uses` endpoints arrive as badged copies with `Execute` and +`Delegate`: the badge (unique per copy) tells the server which client is +calling, and `Delegate` lets the client send capabilities with its calls +and hand its access to programs it starts. + +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. + Only init holds `ProcessCreate`. Everyone else runs programs through init's spawn service (`types::spawn`, client `libkos::spawn`), which init serves once boot is done. A manifest entry with `spawns` gets a badged copy of