From 9b9758e5ee2564b04685eadf2b9ce23dd1060b86 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 19:34:40 +0200 Subject: rsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mouse (IRQ12, stream mode): ByteRing generalized for both PS/2 devices, ISR routes data-port bytes by the controller's aux bit; mouse_init with bounded-ACK handshake (boots without a mouse); packet decoder with bit-3 resync, screen-relative deltas, buttons. Exposed as /dev/mouse, the `mouse` stream command, and `make test-mouse`. Message queues (ADR 0003's GUI-era primitive): every task gets a static 16-slot TaskMsg ring; send/recv/recv_blocking with direct wake + tick scan safety net for the lost-wakeup race; `msgrecv`/`send` commands, `mouse ` routes real IRQ12 events into a task's queue, jobs shows q=. `make test-msg` + `make test-disk` regression pass. --- rsnix/src/sched.rs | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 195 insertions(+), 4 deletions(-) (limited to 'rsnix/src/sched.rs') diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index 6ffa745..dbe3647 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs @@ -42,6 +42,63 @@ const STACK_PAGES: usize = 3; // 1 guard page + 8 KiB stack const NAME_MAX: usize = 24; const PANIC_MAX: usize = 64; +// --------------------------------------------------------------------------- +// Inter-task message queues +// --------------------------------------------------------------------------- + +/// Capacity of each task's message ring (power of two; full sends drop +/// with a "queue full" error). Sized so input bursts (a 100 Hz mouse, +/// key repeat) far outrun a blocked consumer: 16 × ~1.6 s at worst. +const MSG_CAP: usize = 16; + +/// One inter-task message. Input events use the same decoded types the +/// PS/2 drivers produce, so a window's input routing is "post what the +/// driver would have delivered to the console". `User(u64)` is the +/// generic escape hatch (window commands, app signals, …). +#[derive(Clone, Copy, Debug)] +pub enum TaskMsg { + Key(crate::ps2::KeyEvent), + Mouse(crate::ps2::MouseEvent), + Timer, + User(u64), +} + +/// Decode a message for logging/display (no Debug dep in the shell task). +pub fn msg_describe(msg: &TaskMsg) -> alloc::string::String { + use crate::ps2::KeyEvent; + match msg { + TaskMsg::Key(k) => match k { + KeyEvent::Char(c) => alloc::format!("Key('{}')", c), + KeyEvent::Enter => "Key(Enter)".into(), + KeyEvent::Backspace => "Key(Backspace)".into(), + KeyEvent::Tab => "Key(Tab)".into(), + KeyEvent::Escape => "Key(Escape)".into(), + KeyEvent::Ctrl(c) => alloc::format!("Key(Ctrl-{})", c), + KeyEvent::CtrlArrow(d) => alloc::format!("Key(CtrlArrow{})", d), + KeyEvent::ArrowUp => "Key(Up)".into(), + KeyEvent::ArrowDown => "Key(Down)".into(), + KeyEvent::ArrowLeft => "Key(Left)".into(), + KeyEvent::ArrowRight => "Key(Right)".into(), + KeyEvent::Home => "Key(Home)".into(), + KeyEvent::End => "Key(End)".into(), + KeyEvent::PageUp => "Key(PgUp)".into(), + KeyEvent::PageDown => "Key(PgDn)".into(), + KeyEvent::Delete => "Key(Delete)".into(), + KeyEvent::F(n) => alloc::format!("Key(F{})", n), + KeyEvent::Unknown(b) => alloc::format!("Key(0x{:02x})", b), + }, + TaskMsg::Mouse(m) => alloc::format!( + "Mouse({:+},{:+} {} {})", + m.dx, + m.dy, + if m.left { "L" } else { "-" }, + if m.right { "R" } else { "-" } + ), + TaskMsg::Timer => "Timer".into(), + TaskMsg::User(n) => alloc::format!("User({})", n), + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TaskState { Ready, @@ -65,6 +122,13 @@ struct Task { ticks: u64, panic: [u8; PANIC_MAX], panic_len: u8, + /// Per-task message ring (see `TaskMsg`). Accessed by the owning + /// task (pop) and by any poster (push) — both under the scheduler + /// lock, so `send` is callable from ISR or task context. + msg: [TaskMsg; MSG_CAP], + msg_head: u8, + msg_tail: u8, + msg_count: u8, } const TASK_NONE: Task = Task { @@ -78,6 +142,10 @@ const TASK_NONE: Task = Task { ticks: 0, panic: [0; PANIC_MAX], panic_len: 0, + msg: [TaskMsg::User(0); MSG_CAP], + msg_head: 0, + msg_tail: 0, + msg_count: 0, }; static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS]; @@ -133,8 +201,18 @@ pub fn on_tick(cur: usize) -> usize { TASKS[CURRENT].ticks += 1; let now = crate::interrupts::uptime_ms(); for t in 0..MAX_TASKS { - if TASKS[t].state == TaskState::Sleeping && TASKS[t].wake_at <= now { - TASKS[t].state = TaskState::Ready; + match TASKS[t].state { + TaskState::Sleeping if TASKS[t].wake_at <= now => { + TASKS[t].state = TaskState::Ready; + } + // Safety net for the recv race: a task that blocked + // *after* its message was queued (or whose poster never + // got to wake it) wakes on the next tick as long as its + // queue is non-empty. + TaskState::Blocked if TASKS[t].msg_count > 0 => { + TASKS[t].state = TaskState::Ready; + } + _ => {} } } pick_next(cur) @@ -279,6 +357,10 @@ fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { ticks: 0, panic: [0; PANIC_MAX], panic_len: 0, + msg: [TaskMsg::User(0); MSG_CAP], + msg_head: 0, + msg_tail: 0, + msg_count: 0, }; } true @@ -319,6 +401,110 @@ pub fn yield_now() { yield_int(); } +// --------------------------------------------------------------------------- +// Message passing +// --------------------------------------------------------------------------- + +/// Post a message to a task's queue and wake it. Safe from any context: +/// a task (e.g. a window manager routing input), the shell, or an ISR — +/// the scheduler lock makes the queue mutation atomic w.r.t. switching. +/// Returns an error when the task is gone or its queue is full. +pub fn send(id: usize, msg: TaskMsg) -> Result<(), &'static str> { + if id >= MAX_TASKS { + return Err("no such task"); + } + lock(); + let r = unsafe { + let t = &TASKS[id]; + if t.name_len == 0 || t.state == TaskState::Done { + Err("no such task") + } else if push_msg(id, msg) { + if TASKS[id].state == TaskState::Blocked { + TASKS[id].state = TaskState::Ready; + } + Ok(()) + } else { + Err("queue full") + } + }; + unlock(); + r +} + +/// Pop the oldest pending message of the current task, if any. +pub fn recv() -> Option { + lock(); + let m = unsafe { pop_msg(CURRENT) }; + unlock(); + m +} + +/// Block until a message arrives, then return it. Wake is two-fold: +/// `send` flips a Blocked task straight to Ready, and the tick scan +/// (see `on_tick`) catches the race where the task blocked after the +/// message was already queued. +pub fn recv_blocking() -> TaskMsg { + loop { + if let Some(m) = recv() { + return m; + } + lock(); + unsafe { + TASKS[CURRENT].state = TaskState::Blocked; + } + unlock(); + yield_int(); + } +} + +/// Push onto the task's ring (caller holds the scheduler lock). +unsafe fn push_msg(id: usize, msg: TaskMsg) -> bool { + unsafe { + let t = &mut TASKS[id]; + if t.msg_count as usize >= MSG_CAP { + return false; + } + t.msg[t.msg_tail as usize] = msg; + t.msg_tail = (t.msg_tail + 1) & (MSG_CAP as u8 - 1); + t.msg_count += 1; + true + } +} + +/// Pop from the current task's ring (caller holds the scheduler lock). +unsafe fn pop_msg(id: usize) -> Option { + unsafe { + let t = &mut TASKS[id]; + if t.msg_count == 0 { + return None; + } + let m = t.msg[t.msg_head as usize]; + t.msg_head = (t.msg_head + 1) & (MSG_CAP as u8 - 1); + t.msg_count -= 1; + Some(m) + } +} + +/// Pending message count of a task (for `jobs`). +pub fn pending_msgs(id: usize) -> usize { + lock(); + let n = unsafe { TASKS[id].msg_count as usize }; + unlock(); + n +} + +/// Is the task slot live (spawned and not Done)? Used by the shell's +/// `mouse ` routing check and by the window manager later. +pub fn is_alive(id: usize) -> bool { + if id >= MAX_TASKS { + return false; + } + lock(); + let alive = unsafe { TASKS[id].name_len != 0 && TASKS[id].state != TaskState::Done }; + unlock(); + alive +} + /// Software-interrupt the scheduler (vector 0x60). Net stack usage is /// zero (the frame is popped by iretq), so no `nostack` — the compiler /// must not use the red zone around the interrupt. @@ -418,11 +604,12 @@ pub fn jobs() -> Vec { } }; let mut line = alloc::format!( - " {:>2} {:<12} {:<15} {:>6} ticks", + " {:>2} {:<12} {:<15} {:>6} ticks q={}", i, name_str(i), state, - t.ticks + t.ticks, + t.msg_count ); if t.panic_len > 0 { let m = core::str::from_utf8(&t.panic[..t.panic_len as usize]).unwrap_or("?"); @@ -505,6 +692,10 @@ pub fn init() { ticks: 0, panic: [0; PANIC_MAX], panic_len: 0, + msg: [TaskMsg::User(0); MSG_CAP], + msg_head: 0, + msg_tail: 0, + msg_count: 0, }; spawn_at(IDLE_TID, idle_main, "idle"); SCHED_READY.store(true, Ordering::Relaxed); -- cgit v1.2.3