diff --git a/docs/design/raw-keyboard-api-design.md b/docs/design/raw-keyboard-api-design.md index ce195647..bd8cac93 100644 --- a/docs/design/raw-keyboard-api-design.md +++ b/docs/design/raw-keyboard-api-design.md @@ -1,7 +1,10 @@ # Raw Keyboard Event API — Design Document > **Version:** v0.1-draft -> **Status:** Design +> **Status:** Implemented (ring + VK + E0 split, `KeyboardRead` 44 / +> `KeyboardPoll` 45 / `KeyboardSetRawMode` 55, `keyread.nxe`). Deferred: +> `sys_poll` keyboard readiness and a distinct timeout status for the blocking +> read (caller re-issues, like `ob_wait`). > **Target Release:** v0.52+ > **Issue:** [#466](https://github.com/NeoDOS-Project/NeoDOS/issues/466) > **Related:** [#391](https://github.com/NeoDOS-Project/NeoDOS/issues/391), diff --git a/libneodos/src/keyboard.rs b/libneodos/src/keyboard.rs index 8235897a..dc80c806 100644 --- a/libneodos/src/keyboard.rs +++ b/libneodos/src/keyboard.rs @@ -225,4 +225,113 @@ pub fn kbd_set_leds(leds: u8) -> Result<(), i64> { r } +// ── Raw keyboard events (#466) ── + +/// Size of the `KeyEvent` ABI struct (16 bytes). +pub const KBD_EVENT_SIZE: usize = core::mem::size_of::(); +const KBD_READ_MAX: usize = 64; + +/// Raw keyboard event (mirrors the kernel `input::kbd::ring::KeyEvent`). +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct KeyEvent { + /// Raw set-1 scancode (E0-prefixed keys keep the base code). + pub scancode: u16, + /// Virtual key ([`VK_*`]); 0 if none. + pub vk: u16, + /// Layout translation (0 for key-up / non-character keys). + pub codepoint: u32, + pub modifiers: u8, + /// 1 = make (down), 0 = break (up). + pub pressed: u8, + /// bit0 extended (E0), bit1 repeat. + pub flags: u8, + /// Padding to make the struct exactly 16 bytes (ABI). + pub _pad: u32, +} + +impl KeyEvent { + pub const NONE: KeyEvent = KeyEvent { + scancode: 0, + vk: 0, + codepoint: 0, + modifiers: 0, + pressed: 0, + flags: 0, + _pad: 0, + }; + pub fn is_pressed(&self) -> bool { self.pressed != 0 } + pub fn is_extended(&self) -> bool { self.flags & 0x01 != 0 } + pub fn is_repeat(&self) -> bool { self.flags & 0x02 != 0 } +} + +pub const KBD_EV_EXTENDED: u8 = 0x01; +pub const KBD_EV_REPEAT: u8 = 0x02; + +pub const VK_BACK: u16 = 0x08; +pub const VK_TAB: u16 = 0x09; +pub const VK_RETURN: u16 = 0x0D; +pub const VK_SHIFT: u16 = 0x10; +pub const VK_CONTROL: u16 = 0x11; +pub const VK_MENU: u16 = 0x12; +pub const VK_ESCAPE: u16 = 0x1B; +pub const VK_SPACE: u16 = 0x20; +pub const VK_PRIOR: u16 = 0x21; +pub const VK_NEXT: u16 = 0x22; +pub const VK_END: u16 = 0x23; +pub const VK_HOME: u16 = 0x24; +pub const VK_LEFT: u16 = 0x25; +pub const VK_UP: u16 = 0x26; +pub const VK_RIGHT: u16 = 0x27; +pub const VK_DOWN: u16 = 0x28; +pub const VK_INSERT: u16 = 0x2D; +pub const VK_DELETE: u16 = 0x2E; +pub const VK_F1: u16 = 0x70; + +/// Enable/disable raw keyboard mode for the caller's VT (#466). +pub fn kbd_set_raw(enabled: bool) -> Result<(), i64> { + let fd = open_kbd(ob_access::READ | ob_access::WRITE)?; + let r = syscall::sys_ob_set_info(fd, ObSetInfoClass::KeyboardSetRawMode, &[enabled as u8]); + let _ = syscall::sys_close(fd); + r +} + +/// Non-blocking: drain up to `buf.len()` events; returns the count. +pub fn kbd_poll_event(buf: &mut [KeyEvent]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let fd = open_kbd(ob_access::READ)?; + let bytes = unsafe { + core::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len() * KBD_EVENT_SIZE) + }; + let r = syscall::sys_ob_query_info(fd, ObInfoClass::KeyboardPoll, bytes); + let _ = syscall::sys_close(fd); + r.map(|n| n / KBD_EVENT_SIZE) +} + +/// Read up to `buf.len()` events. Requires raw mode. `timeout_ms == 0` blocks +/// until an event arrives (the caller re-issues after the wake); `> 0` uses a +/// deadline. Returns the number of events copied. +pub fn kbd_read_event(buf: &mut [KeyEvent], timeout_ms: u32) -> Result { + if buf.is_empty() { + return Ok(0); + } + let count = buf.len().min(KBD_READ_MAX); + let mut raw = [0u8; 8 + KBD_READ_MAX * KBD_EVENT_SIZE]; + raw[0..4].copy_from_slice(&(count as u32).to_ne_bytes()); + raw[4..8].copy_from_slice(&timeout_ms.to_ne_bytes()); + let len = 8 + count * KBD_EVENT_SIZE; + let fd = open_kbd(ob_access::READ)?; + let r = syscall::sys_ob_query_info(fd, ObInfoClass::KeyboardRead, &mut raw[..len]); + let _ = syscall::sys_close(fd); + let n = r? as usize; + let nav = (n / KBD_EVENT_SIZE).min(buf.len()); + for i in 0..nav { + buf[i] = unsafe { *(raw.as_ptr().add(i * KBD_EVENT_SIZE) as *const KeyEvent) }; + } + Ok(nav) +} + + diff --git a/libneodos/src/syscall/ob.rs b/libneodos/src/syscall/ob.rs index 2a2d25d2..175d7f7a 100644 --- a/libneodos/src/syscall/ob.rs +++ b/libneodos/src/syscall/ob.rs @@ -74,6 +74,9 @@ pub enum ObInfoClass { /// output buffer receives the new connection's fd (u32). Returns EAGAIN /// (`-8`) when none is pending. SocketAccept = 43, + /// Raw keyboard events for the caller's VT (#466). + KeyboardRead = 44, + KeyboardPoll = 45, /// Monotonic uptime: `{ uptime_ms: u64, tick_hz: u32, _pad: u32 }` (#467). Uptime = 46, /// High-resolution monotonic time: `{ tsc: u64, tsc_khz: u32, _pad: u32 }` (#467). @@ -154,6 +157,9 @@ pub enum ObSetInfoClass { /// Mark the target process object as the foreground process of the caller's /// VT (the Ctrl+C target). Used by the interactive shell. SetForegroundProcess = 51, + /// Toggle raw keyboard mode for the caller's VT (#466). 1 byte: 0 = cooked, + /// 1 = raw. + KeyboardSetRawMode = 55, } /// Backward-compatible constants for `ObSetInfoClass`. diff --git a/neodos-kernel/src/input/kbd/mod.rs b/neodos-kernel/src/input/kbd/mod.rs index 8730449f..a9b825c4 100644 --- a/neodos-kernel/src/input/kbd/mod.rs +++ b/neodos-kernel/src/input/kbd/mod.rs @@ -10,6 +10,7 @@ mod unicode; pub(crate) mod config; pub(crate) mod event; mod hotkey; +pub mod ring; pub use layout::KbdLayout; @@ -229,6 +230,20 @@ impl NeoKbd { 3, scancode as u64, self.state.modifiers as u64, 0, ); + // Raw event for Ring 3 (#466): every make/break, with vk + modifiers. + { + let ev = ring::KeyEvent { + scancode: code as u16, + vk: ring::vk_from_scancode(code, is_extended), + codepoint: 0, + modifiers: self.state.modifiers, + pressed: is_make as u8, + flags: if is_extended { ring::KBD_EV_EXTENDED } else { 0 }, + _pad: 0, + }; + crate::input::manager::push_key_event(ev); + } + if !is_make { return; } @@ -242,6 +257,11 @@ impl NeoKbd { return; } + // Raw mode (#466): suppress cooked byte delivery for this VT. + if crate::input::manager::raw_mode(crate::input::manager::active_vt()) { + return; + } + let mods = self.state.modifiers; // Ctrl+C: physical 'C' (scancode 0x2E) with Ctrl held. Handled here, diff --git a/neodos-kernel/src/input/kbd/ring.rs b/neodos-kernel/src/input/kbd/ring.rs new file mode 100644 index 00000000..1fe3c1bb --- /dev/null +++ b/neodos-kernel/src/input/kbd/ring.rs @@ -0,0 +1,282 @@ +//! Raw keyboard event ring + virtual-key mapping (#466). +//! +//! A bounded, lock-free SPSC ring of [`KeyEvent`]s, one per VT. The producer is +//! the PS/2 scancode path (`kbd::process_scancode`, IRQ/DPC context); the +//! consumer is the foreground Ring-3 process via `ObInfoClass::KeyboardRead` / +//! `KeyboardPoll`. A full ring overwrites the oldest entry and bumps `dropped` +//! (a game prefers losing an old key to stalling an IRQ). + +use core::cell::UnsafeCell; +use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; + +/// Ring capacity (events). +pub const KEY_RING_CAP: usize = 256; + +/// `KeyEvent.flags`: E0-prefixed (extended) key. +pub const KBD_EV_EXTENDED: u8 = 0x01; +/// `KeyEvent.flags`: auto-repeat. +pub const KBD_EV_REPEAT: u8 = 0x02; + +// Virtual-key codes (a stable enumeration so clients need not hard-code +// scancodes). Values follow the widespread Windows VK numbering. +pub const VK_BACK: u16 = 0x08; +pub const VK_TAB: u16 = 0x09; +pub const VK_RETURN: u16 = 0x0D; +pub const VK_SHIFT: u16 = 0x10; +pub const VK_CONTROL: u16 = 0x11; +pub const VK_MENU: u16 = 0x12; // Alt +pub const VK_ESCAPE: u16 = 0x1B; +pub const VK_SPACE: u16 = 0x20; +pub const VK_PRIOR: u16 = 0x21; // PageUp +pub const VK_NEXT: u16 = 0x22; // PageDown +pub const VK_END: u16 = 0x23; +pub const VK_HOME: u16 = 0x24; +pub const VK_LEFT: u16 = 0x25; +pub const VK_UP: u16 = 0x26; +pub const VK_RIGHT: u16 = 0x27; +pub const VK_DOWN: u16 = 0x28; +pub const VK_INSERT: u16 = 0x2D; +pub const VK_DELETE: u16 = 0x2E; +pub const VK_NUMPAD0: u16 = 0x60; +pub const VK_NUMPAD1: u16 = 0x61; +pub const VK_NUMPAD2: u16 = 0x62; +pub const VK_NUMPAD3: u16 = 0x63; +pub const VK_NUMPAD4: u16 = 0x64; +pub const VK_NUMPAD5: u16 = 0x65; +pub const VK_NUMPAD6: u16 = 0x66; +pub const VK_NUMPAD7: u16 = 0x67; +pub const VK_NUMPAD8: u16 = 0x68; +pub const VK_NUMPAD9: u16 = 0x69; +pub const VK_MULTIPLY: u16 = 0x6A; +pub const VK_ADD: u16 = 0x6B; +pub const VK_SUBTRACT: u16 = 0x6D; +pub const VK_DECIMAL: u16 = 0x6E; +pub const VK_DIVIDE: u16 = 0x6F; +pub const VK_F1: u16 = 0x70; +pub const VK_F2: u16 = 0x71; +pub const VK_F3: u16 = 0x72; +pub const VK_F4: u16 = 0x73; +pub const VK_F5: u16 = 0x74; +pub const VK_F6: u16 = 0x75; +pub const VK_F7: u16 = 0x76; +pub const VK_F8: u16 = 0x77; +pub const VK_F9: u16 = 0x78; +pub const VK_F10: u16 = 0x79; +pub const VK_F11: u16 = 0x7A; +pub const VK_F12: u16 = 0x7B; + +/// Map a set-1 make scancode to a virtual key (#391: the E0 prefix splits +/// extended keys from their numpad collisions). +/// +/// Returns 0 for keys with no dedicated VK (letters/digits: use `codepoint`). +pub fn vk_from_scancode(code: u8, extended: bool) -> u16 { + let code = code & 0x7F; + if extended { + return match code { + 0x48 => VK_UP, + 0x50 => VK_DOWN, + 0x4B => VK_LEFT, + 0x4D => VK_RIGHT, + 0x47 => VK_HOME, + 0x4F => VK_END, + 0x49 => VK_PRIOR, + 0x51 => VK_NEXT, + 0x52 => VK_INSERT, + 0x53 => VK_DELETE, + 0x1D => VK_CONTROL, // right Ctrl + 0x38 => VK_MENU, // right Alt + 0x1C => VK_RETURN, // keypad Enter + 0x35 => VK_DIVIDE, // keypad / + _ => 0, + }; + } + match code { + 0x01 => VK_ESCAPE, + 0x0E => VK_BACK, + 0x0F => VK_TAB, + 0x1C => VK_RETURN, + 0x1D => VK_CONTROL, + 0x2A => VK_SHIFT, + 0x36 => VK_SHIFT, + 0x38 => VK_MENU, + 0x39 => VK_SPACE, + 0x3B..=0x44 => VK_F1 + (code as u16 - 0x3B), + 0x57 => VK_F11, + 0x58 => VK_F12, + 0x37 => VK_MULTIPLY, + 0x47 => VK_NUMPAD7, + 0x48 => VK_NUMPAD8, + 0x49 => VK_NUMPAD9, + 0x4A => VK_SUBTRACT, + 0x4B => VK_NUMPAD4, + 0x4C => VK_NUMPAD5, + 0x4D => VK_NUMPAD6, + 0x4E => VK_ADD, + 0x4F => VK_NUMPAD1, + 0x50 => VK_NUMPAD2, + 0x51 => VK_NUMPAD3, + 0x52 => VK_NUMPAD0, + 0x53 => VK_DECIMAL, + _ => 0, + } +} + +/// A single raw keyboard event (16 bytes, `#[repr(C)]` — the Ring-3 ABI). +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct KeyEvent { + /// Raw set-1 scancode (E0-prefixed keys keep the base code). + pub scancode: u16, + /// Virtual key ([`vk_from_scancode`]); 0 if none. + pub vk: u16, + /// Layout translation (0 on key-up / non-character keys). + pub codepoint: u32, + /// Modifier bitmask (KBD_SHIFT/CTRL/ALT/ALTGR/CAPS/NUMLOCK/SCROLLLOCK). + pub modifiers: u8, + /// 1 = make (down), 0 = break (up). + pub pressed: u8, + /// [`KBD_EV_EXTENDED`] / [`KBD_EV_REPEAT`]. + pub flags: u8, + /// Padding to make the struct exactly 16 bytes (ABI). + pub _pad: u32, +} + +impl KeyEvent { + pub const NONE: KeyEvent = KeyEvent { + scancode: 0, + vk: 0, + codepoint: 0, + modifiers: 0, + pressed: 0, + flags: 0, + _pad: 0, + }; +} + +/// Bounded lock-free SPSC ring of [`KeyEvent`]s. +pub struct KeyEventRing { + buf: UnsafeCell<[KeyEvent; KEY_RING_CAP]>, + head: AtomicUsize, + tail: AtomicUsize, + dropped: AtomicU64, +} + +// SAFETY: single producer (IRQ/DPC) and single consumer (one Ring-3 thread or +// the kernel test thread); indices are atomic and the slot is published with a +// Release store / read with an Acquire load. +unsafe impl Sync for KeyEventRing {} + +impl KeyEventRing { + pub const fn new() -> Self { + KeyEventRing { + buf: UnsafeCell::new([KeyEvent::NONE; KEY_RING_CAP]), + head: AtomicUsize::new(0), + tail: AtomicUsize::new(0), + dropped: AtomicU64::new(0), + } + } + + /// Push an event. Full ring: overwrite the oldest entry and count a drop. + pub fn push(&self, ev: KeyEvent) { + let t = self.tail.load(Ordering::Relaxed); + let h = self.head.load(Ordering::Acquire); + if t.wrapping_sub(h) >= KEY_RING_CAP { + self.head.store(h.wrapping_add(1), Ordering::Release); + self.dropped.fetch_add(1, Ordering::Relaxed); + } + // SAFETY: `t % CAP` is in bounds; the producer owns `tail`. + unsafe { + (*self.buf.get())[t % KEY_RING_CAP] = ev; + } + self.tail.store(t.wrapping_add(1), Ordering::Release); + } + + pub fn pop(&self) -> Option { + let h = self.head.load(Ordering::Relaxed); + let t = self.tail.load(Ordering::Acquire); + if h == t { + return None; + } + // SAFETY: `h % CAP` is in bounds; the consumer owns `head`. + let ev = unsafe { (*self.buf.get())[h % KEY_RING_CAP] }; + self.head.store(h.wrapping_add(1), Ordering::Release); + Some(ev) + } + + pub fn has_data(&self) -> bool { + self.head.load(Ordering::Acquire) != self.tail.load(Ordering::Acquire) + } + + pub fn len(&self) -> usize { + self.tail + .load(Ordering::Acquire) + .wrapping_sub(self.head.load(Ordering::Acquire)) + } + + pub fn is_empty(&self) -> bool { + !self.has_data() + } + + pub fn dropped(&self) -> u64 { + self.dropped.load(Ordering::Relaxed) + } + + pub fn clear(&self) { + let t = self.tail.load(Ordering::Acquire); + self.head.store(t, Ordering::Release); + } +} + +pub fn register_tests() { + use crate::{test_case, test_eq, test_true}; + + test_case!("kbd_ring_push_pop_fifo", { + let r = KeyEventRing::new(); + test_true!(r.is_empty()); + for i in 0..10u16 { + let mut ev = KeyEvent::NONE; + ev.scancode = i; + r.push(ev); + } + test_eq!(r.len(), 10); + for i in 0..10u16 { + test_eq!(r.pop().unwrap().scancode, i); + } + test_true!(r.pop().is_none()); + }); + + test_case!("kbd_ring_overflow_drops_oldest", { + let r = KeyEventRing::new(); + for i in 0..(KEY_RING_CAP as u16 + 1) { + let mut ev = KeyEvent::NONE; + ev.scancode = i; + r.push(ev); + } + test_eq!(r.len(), KEY_RING_CAP); + test_eq!(r.dropped(), 1); + // The oldest (scancode 0) was overwritten; the next pop is 1. + test_eq!(r.pop().unwrap().scancode, 1); + }); + + test_case!("kbd_key_event_is_16_bytes", { + test_eq!(core::mem::size_of::(), 16); + }); + + test_case!("kbd_vk_extended_split", { + // #391: E0 arrows must not collide with numpad. + test_eq!(vk_from_scancode(0x48, false), VK_NUMPAD8); + test_eq!(vk_from_scancode(0x48, true), VK_UP); + test_eq!(vk_from_scancode(0x50, true), VK_DOWN); + test_eq!(vk_from_scancode(0x4B, true), VK_LEFT); + test_eq!(vk_from_scancode(0x4D, true), VK_RIGHT); + test_eq!(vk_from_scancode(0x47, true), VK_HOME); + test_eq!(vk_from_scancode(0x4F, true), VK_END); + test_eq!(vk_from_scancode(0x49, true), VK_PRIOR); + test_eq!(vk_from_scancode(0x51, true), VK_NEXT); + test_eq!(vk_from_scancode(0x52, true), VK_INSERT); + test_eq!(vk_from_scancode(0x53, true), VK_DELETE); + test_eq!(vk_from_scancode(0x3B, false), VK_F1); + test_eq!(vk_from_scancode(0x44, false), VK_F10); + test_eq!(vk_from_scancode(0x01, false), VK_ESCAPE); + }); +} diff --git a/neodos-kernel/src/input/manager.rs b/neodos-kernel/src/input/manager.rs index ced4dcea..b9b2d437 100644 --- a/neodos-kernel/src/input/manager.rs +++ b/neodos-kernel/src/input/manager.rs @@ -1,5 +1,6 @@ use crate::input::vt::{VtInputQueue, VtShadowBuffer, ConsoleState, VT_COUNT}; -use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use crate::input::kbd::ring::{KeyEvent, KeyEventRing}; +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; pub struct InputManager { active_vt: AtomicUsize, @@ -10,6 +11,11 @@ pub struct InputManager { /// read from the keyboard IRQ (Ctrl+C) and written from syscall context, so /// it must be atomic. vt_foreground_pid: [AtomicU32; VT_COUNT], + /// Raw keyboard event ring per VT (#466), drained by the foreground client. + key_rings: [KeyEventRing; VT_COUNT], + /// Per-VT raw mode: when on, scancodes go to `key_rings` instead of cooked + /// bytes on stdin (#466). + raw_mode: [AtomicBool; VT_COUNT], } static mut INPUT_MANAGER: InputManager = InputManager::new(); @@ -37,6 +43,13 @@ impl InputManager { VtShadowBuffer::new(), ], vt_foreground_pid: [const { AtomicU32::new(0) }; VT_COUNT], + key_rings: [ + KeyEventRing::new(), + KeyEventRing::new(), + KeyEventRing::new(), + KeyEventRing::new(), + ], + raw_mode: [const { AtomicBool::new(false) }; VT_COUNT], } } @@ -64,9 +77,12 @@ impl InputManager { if pid == 0 { return; } - for q in self.vt_foreground_pid.iter() { + for (i, q) in self.vt_foreground_pid.iter().enumerate() { if q.load(Ordering::Acquire) == pid { q.store(0, Ordering::Release); + // A crashed/exited foreground client must not leave the VT deaf + // to cooked input (#466). + self.raw_mode[i].store(false, Ordering::Release); } } } @@ -89,6 +105,34 @@ impl InputManager { pub fn pop_byte_from_vt(&self, vt: usize) -> Option { if vt >= VT_COUNT { None } else { self.vt_queues[vt].pop() } } + + /// Push a raw key event to VT `vt` and wake a blocked raw reader (#466). + pub fn push_key_event_to(&self, vt: usize, ev: KeyEvent) { + if vt >= VT_COUNT { + return; + } + self.key_rings[vt].push(ev); + crate::kwait::kwait_wake(&crate::kwait::WaitReason::Keyboard { vt: vt as u32 }); + } + + pub fn key_ring(&self, vt: usize) -> Option<&KeyEventRing> { + if vt < VT_COUNT { Some(&self.key_rings[vt]) } else { None } + } + + pub fn set_raw_mode(&self, vt: usize, on: bool) { + if vt < VT_COUNT { + if on { + // Start from a clean ring so cooked-mode keystrokes are not + // replayed as raw events (#466). + self.key_rings[vt].clear(); + } + self.raw_mode[vt].store(on, Ordering::Release); + } + } + + pub fn raw_mode(&self, vt: usize) -> bool { + vt < VT_COUNT && self.raw_mode[vt].load(Ordering::Acquire) + } } pub fn init() { @@ -98,6 +142,27 @@ pub fn active_vt() -> usize { unsafe { INPUT_MANAGER.active_vt() } } pub fn switch_vt(vt: usize) { unsafe { INPUT_MANAGER.switch_vt(vt); } } pub fn push_byte(b: u8) -> Result<(), ()> { unsafe { INPUT_MANAGER.push_byte(b) } } pub fn pop_byte_from_vt(vt: usize) -> Option { unsafe { INPUT_MANAGER.pop_byte_from_vt(vt) } } + +/// Push a raw key event to the active VT (#466). Called from the scancode path. +pub fn push_key_event(ev: KeyEvent) { + let vt = active_vt(); + unsafe { INPUT_MANAGER.push_key_event_to(vt, ev) }; +} + +/// Raw event ring for `vt` (None when out of range). +pub fn key_ring_for(vt: usize) -> Option<&'static KeyEventRing> { + unsafe { INPUT_MANAGER.key_ring(vt) } +} + +/// Enable/disable raw mode for `vt` (#466). Idempotent. +pub fn set_raw_mode(vt: usize, on: bool) { + unsafe { INPUT_MANAGER.set_raw_mode(vt, on); } +} + +/// Whether `vt` is in raw mode. +pub fn raw_mode(vt: usize) -> bool { + unsafe { INPUT_MANAGER.raw_mode(vt) } +} pub fn input_manager_mut() -> Option<&'static mut InputManager> { unsafe { Some(&mut INPUT_MANAGER) } } diff --git a/neodos-kernel/src/input/mod.rs b/neodos-kernel/src/input/mod.rs index 2ac213c3..8c18fcce 100644 --- a/neodos-kernel/src/input/mod.rs +++ b/neodos-kernel/src/input/mod.rs @@ -13,6 +13,9 @@ pub fn register_tests() { use crate::test_eq; use crate::test_ne; + // Raw keyboard event ring + VK mapping (#466). + crate::input::kbd::ring::register_tests(); + test_case!("input_empty_pop", { let buf = InputBuffer::new(); test_eq!(buf.pop(), None); diff --git a/neodos-kernel/src/kwait/mod.rs b/neodos-kernel/src/kwait/mod.rs index d10145a4..fea96804 100644 --- a/neodos-kernel/src/kwait/mod.rs +++ b/neodos-kernel/src/kwait/mod.rs @@ -23,6 +23,7 @@ const TAG_SEMAPHORE: u32 = 0x0008; const TAG_SOCKET_READ: u32 = 0x0009; const TAG_SOCKET_CONNECT: u32 = 0x000A; const TAG_SOCKET_ACCEPT: u32 = 0x000B; +const TAG_KEYBOARD: u32 = 0x000C; #[inline(always)] fn make_magic(tag: u32, id: u32) -> u64 { @@ -60,6 +61,8 @@ pub enum WaitReason { SocketConnect { socket_id: u32 }, /// Waiting to accept a new connection (socket_id) SocketAccept { socket_id: u32 }, + /// Waiting for a raw keyboard event on VT `vt` (#466). + Keyboard { vt: u32 }, } impl WaitReason { @@ -78,6 +81,7 @@ impl WaitReason { WaitReason::SocketRead { socket_id } => make_magic(TAG_SOCKET_READ, socket_id), WaitReason::SocketConnect { socket_id } => make_magic(TAG_SOCKET_CONNECT, socket_id), WaitReason::SocketAccept { socket_id } => make_magic(TAG_SOCKET_ACCEPT, socket_id), + WaitReason::Keyboard { vt } => make_magic(TAG_KEYBOARD, vt), } } @@ -96,6 +100,7 @@ impl WaitReason { TAG_SOCKET_READ => WaitReason::SocketRead { socket_id: id }, TAG_SOCKET_CONNECT => WaitReason::SocketConnect { socket_id: id }, TAG_SOCKET_ACCEPT => WaitReason::SocketAccept { socket_id: id }, + TAG_KEYBOARD => WaitReason::Keyboard { vt: id }, _ => return None, }) } diff --git a/neodos-kernel/src/object/types.rs b/neodos-kernel/src/object/types.rs index e8ead806..808610d3 100644 --- a/neodos-kernel/src/object/types.rs +++ b/neodos-kernel/src/object/types.rs @@ -175,6 +175,11 @@ pub enum ObInfoClass { /// (u32) as a direct fd. Returns `Again` (EAGAIN) when no connection is /// pending. Read on the listener's Socket object. SocketAccept = 43, + /// Read raw keyboard events for the caller's VT (#466). + /// in: `{ count: u32, timeout_ms: u32 }`; out: `count` × `KeyEvent`. + KeyboardRead = 44, + /// Non-blocking raw keyboard event poll (#466). out: `KeyEvent` array. + KeyboardPoll = 45, /// Monotonic system uptime: `{ uptime_ms: u64, tick_hz: u32, _pad: u32 }` /// (16 bytes). Never decreases; SMP-safe (single BSP tick source) (#467). Uptime = 46, @@ -237,6 +242,9 @@ pub enum ObSetInfoClass { /// VT, i.e. the Ctrl+C target. Declared explicitly by the interactive shell /// for the child it is about to wait on; ignored for other callers. SetForegroundProcess = 51, + /// Toggle raw keyboard mode for the caller's VT (#466). 1 byte: 0 = cooked, + /// 1 = raw. Idempotent; cleared when the VT's foreground process exits. + KeyboardSetRawMode = 55, } // ═══════════════════════════════════════════════════════════════════════ diff --git a/neodos-kernel/src/syscall/ob/query/keyboard.rs b/neodos-kernel/src/syscall/ob/query/keyboard.rs new file mode 100644 index 00000000..c63905f3 --- /dev/null +++ b/neodos-kernel/src/syscall/ob/query/keyboard.rs @@ -0,0 +1,104 @@ +//! Ob query — raw keyboard events (#466). +//! +//! `KeyboardRead` (44): request `{ count: u32, timeout_ms: u32 }`; drains up to +//! `count` [`KeyEvent`]s into the same buffer. Requires raw mode. When the ring +//! is empty it blocks once (indefinitely if `timeout_ms == 0`, else with a +//! deadline) and returns 0 — the caller loops, mirroring `ob_wait`. +//! +//! `KeyboardPoll` (45): non-blocking; drains up to `buf_size / 16` events and +//! returns `count * 16`. + +use alloc::vec::Vec; +use crate::input::kbd::ring::KeyEvent; +use crate::object::types::ObInfoClass; +use crate::syscall::{copy_from_user, copy_to_user, err_to_u64, SyscallError}; + +pub(super) fn handles(info_class: u32) -> bool { + info_class == ObInfoClass::KeyboardRead as u32 + || info_class == ObInfoClass::KeyboardPoll as u32 +} + +fn ev_bytes(ev: &KeyEvent) -> &[u8] { + // SAFETY: KeyEvent is #[repr(C)] Copy; the slice covers exactly 16 bytes. + unsafe { + core::slice::from_raw_parts(ev as *const KeyEvent as *const u8, core::mem::size_of::()) + } +} + +fn drain(ring: &crate::input::kbd::ring::KeyEventRing, max: usize) -> Vec { + let mut out = Vec::with_capacity(max * core::mem::size_of::()); + let mut n = 0usize; + while n < max { + match ring.pop() { + Some(ev) => { + out.extend_from_slice(ev_bytes(&ev)); + n += 1; + } + None => break, + } + } + out +} + +pub(super) fn dispatch( + info_class: u32, + _fd: u8, + _entry: crate::handle::HandleEntry, + buf_ptr: u64, + buf_size: usize, +) -> u64 { + let vt = crate::scheduler::current_vt_num() as usize; + let ring = match crate::input::manager::key_ring_for(vt) { + Some(r) => r, + None => return err_to_u64(SyscallError::Inval), + }; + let evsz = core::mem::size_of::(); + + if info_class == ObInfoClass::KeyboardRead as u32 { + if buf_size < 8 { + return err_to_u64(SyscallError::Inval); + } + // Raw mode is required so a client cannot consume a key twice. + if !crate::input::manager::raw_mode(vt) { + return err_to_u64(SyscallError::Again); + } + let mut hdr = [0u8; 8]; + if copy_from_user(&mut hdr, buf_ptr).is_err() { + return err_to_u64(SyscallError::Fault); + } + let count = u32::from_ne_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]) as usize; + let timeout_ms = u32::from_ne_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]); + let max = count.min(buf_size.saturating_sub(8) / evsz); + if max == 0 { + return err_to_u64(SyscallError::Inval); + } + let out = drain(ring, max); + if out.is_empty() { + // Block once; the caller re-issues the read after the wake. + let reason = crate::kwait::WaitReason::Keyboard { vt: vt as u32 }; + if timeout_ms > 0 { + crate::kwait::register_deadline( + reason.encode_magic(), + crate::net::tcp::tcp_deadline_from_ms(timeout_ms as u64), + ); + } + crate::kwait::kwait_block(reason); + return 0; + } + if copy_to_user(buf_ptr, &out).is_err() { + return err_to_u64(SyscallError::Fault); + } + out.len() as u64 + } else { + // KeyboardPoll (45): non-blocking drain, events only. + let max = buf_size / evsz; + if max == 0 { + return err_to_u64(SyscallError::Inval); + } + let out = drain(ring, max); + if !out.is_empty() && copy_to_user(buf_ptr, &out).is_err() { + return err_to_u64(SyscallError::Fault); + } + out.len() as u64 + } +} diff --git a/neodos-kernel/src/syscall/ob/query/mod.rs b/neodos-kernel/src/syscall/ob/query/mod.rs index 5b9241a2..852e2f48 100644 --- a/neodos-kernel/src/syscall/ob/query/mod.rs +++ b/neodos-kernel/src/syscall/ob/query/mod.rs @@ -15,6 +15,7 @@ mod time; mod devices; mod fs; mod session; +mod keyboard; pub use stats::register_ob_stats_tests; @@ -29,6 +30,7 @@ pub fn info_class_handled(info_class: u32) -> bool { || devices::handles(info_class) || fs::handles(info_class) || session::handles(info_class) + || keyboard::handles(info_class) } pub fn handler_ob_query_info(regs: crate::syscall::Registers) -> u64 { @@ -58,6 +60,7 @@ pub fn handler_ob_query_info(regs: crate::syscall::Registers) -> u64 { _ if devices::handles(info_class) => devices::dispatch(info_class, fd, entry, buf_ptr, buf_size), _ if fs::handles(info_class) => fs::dispatch(info_class, fd, entry, buf_ptr, buf_size), _ if session::handles(info_class) => session::dispatch(info_class, fd, entry, buf_ptr, buf_size), + _ if keyboard::handles(info_class) => keyboard::dispatch(info_class, fd, entry, buf_ptr, buf_size), _ => err_to_u64(SyscallError::Inval), } } diff --git a/neodos-kernel/src/syscall/ob/set/keyboard.rs b/neodos-kernel/src/syscall/ob/set/keyboard.rs index c790ad77..026146b6 100644 --- a/neodos-kernel/src/syscall/ob/set/keyboard.rs +++ b/neodos-kernel/src/syscall/ob/set/keyboard.rs @@ -11,6 +11,7 @@ pub(super) fn handles(info_class: u32) -> bool { || info_class == ObSetInfoClass::KeyboardSetRepeatRate as u32 || info_class == ObSetInfoClass::KeyboardSetLeds as u32 || info_class == ObSetInfoClass::KeyboardSetModifier as u32 + || info_class == ObSetInfoClass::KeyboardSetRawMode as u32 } /// Dispatch the `keyboard` info classes. @@ -22,6 +23,19 @@ pub(super) fn dispatch( buf_size: usize, ) -> u64 { match info_class { + // #466: toggle raw mode for the caller's VT (1 byte: 0/1). + _ if info_class == ObSetInfoClass::KeyboardSetRawMode as u32 => { + if buf_size < 1 { + return err_to_u64(SyscallError::Inval); + } + let mut b = [0u8; 1]; + if copy_from_user(&mut b, buf_ptr).is_err() { + return err_to_u64(SyscallError::Fault); + } + let vt = crate::scheduler::current_vt_num() as usize; + crate::input::manager::set_raw_mode(vt, b[0] != 0); + 0 + } _ if info_class == ObSetInfoClass::KeyboardLayout as u32 => { if entry.object_id == 0 { return err_to_u64(SyscallError::Inval); diff --git a/userbin/keyread/.cargo/config.toml b/userbin/keyread/.cargo/config.toml new file mode 100644 index 00000000..0465985e --- /dev/null +++ b/userbin/keyread/.cargo/config.toml @@ -0,0 +1,9 @@ +[build] +target = "x86_64-unknown-none" + +[target.x86_64-unknown-none] +rustflags = [ + "-C", "link-arg=-T../../libneodos/user.ld", + "-C", "link-arg=-melf_x86_64", + "-C", "relocation-model=pie", +] diff --git a/userbin/keyread/Cargo.lock b/userbin/keyread/Cargo.lock new file mode 100644 index 00000000..8bbc5d90 --- /dev/null +++ b/userbin/keyread/Cargo.lock @@ -0,0 +1,21 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "keyread" +version = "0.1.0" +dependencies = [ + "libneodos", +] + +[[package]] +name = "libneodos" +version = "0.2.1" +dependencies = [ + "libnlt", +] + +[[package]] +name = "libnlt" +version = "0.1.0" diff --git a/userbin/keyread/Cargo.toml b/userbin/keyread/Cargo.toml new file mode 100644 index 00000000..66d8179b --- /dev/null +++ b/userbin/keyread/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "keyread" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "keyread" +path = "src/main.rs" +test = false + +[dependencies] +libneodos = { path = "../../libneodos" } + +[profile.release] +panic = "abort" +opt-level = 3 +lto = true +debug = false + +[profile.dev] +panic = "abort" diff --git a/userbin/keyread/src/main.rs b/userbin/keyread/src/main.rs new file mode 100644 index 00000000..ac811680 --- /dev/null +++ b/userbin/keyread/src/main.rs @@ -0,0 +1,118 @@ +#![no_std] +#![no_main] + +//! `keyread` — read raw keyboard events (Ring 3, #466). +//! +//! Enables raw mode for the calling VT, prints up to 10 make/break events +//! (scancode, virtual key, pressed, modifiers, extended), then restores cooked +//! mode and exits. Demonstrates the `ObInfoClass::KeyboardRead` API and the +//! E0 extended-key mapping (#391). + +extern crate alloc; + +use core::alloc::{GlobalAlloc, Layout}; +use libneodos::keyboard::{self, KeyEvent}; +use libneodos::syscall; + +struct SbrkAlloc; +unsafe impl GlobalAlloc for SbrkAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let size = layout.size().max(8) as i64; + let ptr = libneodos::mem::sbrk(size).ok().unwrap_or(0) as *mut u8; + if ptr.is_null() { core::ptr::null_mut() } else { ptr } + } + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} +#[global_allocator] +static ALLOC: SbrkAlloc = SbrkAlloc; + +fn write_str(s: &[u8]) { + let _ = syscall::sys_write(1, s); +} +fn write_dec(mut v: u64) { + let mut b = [0u8; 20]; + let mut i = b.len(); + if v == 0 { write_str(b"0"); return; } + while v > 0 && i > 0 { i -= 1; b[i] = b'0' + (v % 10) as u8; v /= 10; } + write_str(&b[i..]); +} + +fn vk_name(vk: u16) -> &'static [u8] { + match vk { + keyboard::VK_UP => b"UP", + keyboard::VK_DOWN => b"DOWN", + keyboard::VK_LEFT => b"LEFT", + keyboard::VK_RIGHT => b"RIGHT", + keyboard::VK_HOME => b"HOME", + keyboard::VK_END => b"END", + keyboard::VK_PRIOR => b"PGUP", + keyboard::VK_NEXT => b"PGDN", + keyboard::VK_INSERT => b"INS", + keyboard::VK_DELETE => b"DEL", + keyboard::VK_ESCAPE => b"ESC", + keyboard::VK_RETURN => b"ENTER", + keyboard::VK_SPACE => b"SPACE", + keyboard::VK_TAB => b"TAB", + keyboard::VK_BACK => b"BACKSPACE", + 0 => b"-", + _ => b"?", + } +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + write_str(b"[keyread] raw mode ON; press keys (arrows/Ctrl/Esc/Space)...\r\n"); + if keyboard::kbd_set_raw(true).is_err() { + write_str(b"[keyread] kbd_set_raw failed\r\n"); + syscall::sys_exit(1); + } + if keyboard::kbd_set_raw(true).is_err() { + // idempotent; ignore + } + + let mut evs = [KeyEvent::NONE; 8]; + let mut printed = 0u32; + let mut idle = 0u32; + // `kbd_read_event` blocks (with a deadline) and returns 0 on a wake or + // timeout — like `ob_wait`, the caller re-issues. `idle` bounds the demo. + while printed < 10 && idle < 20 { + match keyboard::kbd_read_event(&mut evs, 500) { + Ok(n) if n > 0 => { + idle = 0; + for e in evs.iter().take(n) { + write_str(b" sc="); + write_dec(e.scancode as u64); + write_str(b" vk="); + write_dec(e.vk as u64); + write_str(b" "); + write_str(vk_name(e.vk)); + write_str(b" "); + write_str(if e.is_pressed() { b"down" } else { b"up" }); + write_str(b" mod="); + write_dec(e.modifiers as u64); + if e.is_extended() { + write_str(b" ext"); + } + write_str(b"\r\n"); + printed += 1; + } + } + Ok(_) => { + idle += 1; + } + Err(code) => { + write_str(b"[keyread] read error "); + write_dec((-code) as u64); + write_str(b"\r\n"); + break; + } + } + } + if printed == 0 { + write_str(b"[keyread] (no keys captured)\r\n"); + } + + let _ = keyboard::kbd_set_raw(false); + write_str(b"[keyread] raw mode OFF, done\r\n"); + syscall::sys_exit(0) +}