diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 19:34:40 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 19:34:40 +0200 |
| commit | 9b9758e5ee2564b04685eadf2b9ce23dd1060b86 (patch) | |
| tree | 57fce9e715510ec70df70dc399b4440f3480fd77 | |
| parent | fix pi-lens warnings: unwraps, shell SC2046, dup code (fmtbuf/scripts/hexdump... (diff) | |
| download | rs-dos-9b9758e5ee2564b04685eadf2b9ce23dd1060b86.tar.gz rs-dos-9b9758e5ee2564b04685eadf2b9ce23dd1060b86.zip | |
rsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues
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 <id>` routes real IRQ12 events into a task's queue, jobs shows
q=. `make test-msg` + `make test-disk` regression pass.
| -rw-r--r-- | CONTEXT.md | 73 | ||||
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | Makefile | 12 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 5 | ||||
| -rw-r--r-- | rsnix/src/fs.rs | 38 | ||||
| -rw-r--r-- | rsnix/src/interrupts.rs | 68 | ||||
| -rw-r--r-- | rsnix/src/ps2.rs | 183 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 199 | ||||
| -rwxr-xr-x | scripts/test-mouse.sh | 130 | ||||
| -rwxr-xr-x | scripts/test-msg.sh | 159 | ||||
| -rw-r--r-- | shell/src/lib.rs | 211 |
12 files changed, 1061 insertions, 25 deletions
| @@ -113,7 +113,9 @@ Workspace version is `a.b.c` (single version for all crates): | |||
| 113 | 113 | ||
| 114 | Bump the workspace version in the root `Cargo.toml` when a change of | 114 | Bump the workspace version in the root `Cargo.toml` when a change of |
| 115 | that class lands — do not leave the version to drift. Current: | 115 | that class lands — do not leave the version to drift. Current: |
| 116 | `0.2.0`; the multitasking work lands as a `b` bump (`0.3.0`). | 116 | `0.4.0` — the GUI input foundation (PS/2 mouse + scheduler message |
| 117 | queues) landed as a `b` bump; the GUI era itself is reserved for the | ||
| 118 | `a` bump (1.0.0). | ||
| 117 | 119 | ||
| 118 | ## Project layout | 120 | ## Project layout |
| 119 | 121 | ||
| @@ -404,6 +406,75 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 404 | `bg overflow` dies on the guard page with the machine intact; full | 406 | `bg overflow` dies on the guard page with the machine intact; full |
| 405 | regression (rescan/til/klog/grammar/pager) re-passes. | 407 | regression (rescan/til/klog/grammar/pager) re-passes. |
| 406 | 408 | ||
| 409 | ✅ **PS/2 mouse** — auxiliary-channel driver on IRQ12, stream mode | ||
| 410 | (3-byte packets), the GUI's second input device: | ||
| 411 | |||
| 412 | - `interrupts`: the keyboard byte ring is generalised to `ByteRing`; | ||
| 413 | IRQ12 (vector 44) gets its own ring and stub. Both PS/2 handlers | ||
| 414 | route each data-port byte by the controller status register's aux | ||
| 415 | bit (bit 5), so keyboard and mouse never steal each other's bytes | ||
| 416 | even when both are pending at once. PIC unmask now covers IRQ2 | ||
| 417 | (cascade) and IRQ12 (slave bit 4). | ||
| 418 | - `ps2`: `mouse_init()` enables the aux port, sets config bits | ||
| 419 | IRQ12+aux-clock (preserving the keyboard bit), then `set defaults` | ||
| 420 | (100 Hz / 4 counts/mm / 1:1) + `enable reporting` via the 0xD4 | ||
| 421 | channel — each waits for the device ACK with a bounded timeout so | ||
| 422 | a machine without a mouse boots normally (log: `ps2: mouse not | ||
| 423 | present`). `try_poll_mouse()` reassembles packets in main | ||
| 424 | context from the raw byte ring; the packet-start marker (bit 3) | ||
| 425 | resyncs the stream after any lost byte. Events are **screen- | ||
| 426 | relative** (`dx` + = right, `dy` + = down — the PS/2 Y-up wire | ||
| 427 | convention is negated), buttons decoded; 9-bit sign extension via | ||
| 428 | the packet sign bits. | ||
| 429 | - `fs`: `/dev/mouse` char device — `cat /dev/mouse` drains up to 32 | ||
| 430 | pending events (`dx=+N dy=+N buttons=L-R`), reports | ||
| 431 | `mouse: no PS/2 mouse` / `mouse: no events` otherwise. | ||
| 432 | - shell: `mouse` command streams live events (10 ms poll via | ||
| 433 | `sleep_ms`) until ^C. | ||
| 434 | |||
| 435 | Verified in QEMU (`make test-mouse`, headless, via the monitor's | ||
| 436 | `mouse_move`/`mouse_button` HMP commands): boot line `ps2: mouse | ||
| 437 | ready`, `/dev/mouse` drains a pre-consumer move (`8,4`), the live | ||
| 438 | stream decodes `mouse_move 20 7` → `dx=+20 dy=+7 buttons=---`, | ||
| 439 | `mouse_button 1` → `buttons=L--`, a held-button move | ||
| 440 | (`-5,-3` while L down) → `dx=-5 dy=-3 buttons=L--`, release, | ||
| 441 | and ^C stops the stream. `make test-disk` regression re-passes. | ||
| 442 | |||
| 443 | ✅ **Scheduler message queues (the GUI-era primitive from ADR 0003)** — | ||
| 444 | every task gets a static 16-slot ring of `TaskMsg` (4 KB in .bss, | ||
| 445 | no ISR-time allocation): | ||
| 446 | |||
| 447 | - `TaskMsg` = `Key(KeyEvent)` | `Mouse(MouseEvent)` | `Timer` | | ||
| 448 | `User(u64)` — input uses the same decoded types the PS/2 drivers | ||
| 449 | produce, so window input routing is literally "post what the | ||
| 450 | console would have gotten"; `User(u64)` is the generic escape | ||
| 451 | hatch. `KeyEvent`/`MouseEvent` derive Copy for ring storage. | ||
| 452 | - `sched::send(id, msg)` — posts from any context (task, shell, ISR) | ||
| 453 | under the scheduler lock; wakes a Blocked task straight to Ready; | ||
| 454 | errors cleanly on a gone task or a full queue (input is lossy | ||
| 455 | under extreme load by design). `recv()` pops without blocking; | ||
| 456 | `recv_blocking()` marks the task Blocked and yields. **Lost-wakeup | ||
| 457 | safety net**: the tick scan also wakes Blocked tasks whose queue is | ||
| 458 | non-empty, so a task that blocked *after* its message arrived (or | ||
| 459 | whose poster never woke it) is picked up within one 10 ms tick. | ||
| 460 | - shell: `msgrecv` spawns a task that blocks on recv and logs every | ||
| 461 | message to serial (`msgrecv[2]: Key(F3)` …); `send <id> <type> | ||
| 462 | [args]` posts `user <n>` / `timer` / `mouse <dx> <dy> <btn>` | ||
| 463 | (1=L 2=R 4=M) / `key <name>` (enter esc tab space bksp arrows | ||
| 464 | home end pgup pgdn del ctrl-c f1..f12, or a single char); `jobs` | ||
| 465 | now shows `q=` (pending message count). | ||
| 466 | - **Real-input routing, proven**: `mouse <id>` (angle brackets) streams | ||
| 467 | decoded IRQ12 events into that task's queue instead of the console | ||
| 468 | — the exact loop the window manager will run. `msgrecv` + `mouse 2` | ||
| 469 | - `mouse_move 20 7` showed `msgrecv[2]: Mouse(+20,+7 …)` in QEMU. | ||
| 470 | |||
| 471 | Verified in QEMU (`make test-msg`): all six message types delivered | ||
| 472 | in order (user, timer, key enter/ctrl-c/f3, synthesized mouse), | ||
| 473 | real routed mouse moves (including a held-button move), the 17th | ||
| 474 | send against a 16-cap queue reports `queue full` (tested against | ||
| 475 | the shell's never-consumed queue), and `jobs` shows q=0 + task back | ||
| 476 | to `blocked` after draining. `make test-disk` regression re-passes. | ||
| 477 | |||
| 407 | ## Known issues / perf notes | 478 | ## Known issues / perf notes |
| 408 | 479 | ||
| 409 | - **Scroll cost** — scrolling still shifts the whole framebuffer (a | 480 | - **Scroll cost** — scrolling still shifts the whole framebuffer (a |
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.3.0" | 7 | version = "0.4.0" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.3.0" | 22 | version = "0.4.0" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.3.0" | 26 | version = "0.4.0" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.3.0" | 6 | version = "0.4.0" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. |
| @@ -14,7 +14,7 @@ export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI | |||
| 14 | # Create one with: make disk (or: ./scripts/mkdisk.sh) | 14 | # Create one with: make disk (or: ./scripts/mkdisk.sh) |
| 15 | export DISK_IMG ?= /tmp/disk.img | 15 | export DISK_IMG ?= /tmp/disk.img |
| 16 | 16 | ||
| 17 | .PHONY: all run run-gui clean image debug release fmt bootloader boot-image disk disk-fresh inspect test-disk | 17 | .PHONY: all run run-gui clean image debug release fmt bootloader boot-image disk disk-fresh inspect test-disk test-mouse test-msg |
| 18 | 18 | ||
| 19 | # ---- Build --------------------------------------------------------------- | 19 | # ---- Build --------------------------------------------------------------- |
| 20 | 20 | ||
| @@ -113,6 +113,16 @@ inspect: | |||
| 113 | test-disk: | 113 | test-disk: |
| 114 | ./scripts/test-disk.sh | 114 | ./scripts/test-disk.sh |
| 115 | 115 | ||
| 116 | # Automated PS/2 mouse round trip: boot -> inject mouse_move/mouse_button | ||
| 117 | # via the monitor -> verify /dev/mouse + the `mouse` stream -> poweroff. | ||
| 118 | test-mouse: | ||
| 119 | ./scripts/test-mouse.sh | ||
| 120 | |||
| 121 | # Automated scheduler message-queue round trip: msgrecv/send, real-input | ||
| 122 | # routing into a task's queue, queue-full boundary. | ||
| 123 | test-msg: | ||
| 124 | ./scripts/test-msg.sh | ||
| 125 | |||
| 116 | # ---- Full boot chain ----------------------------------------------------- | 126 | # ---- Full boot chain ----------------------------------------------------- |
| 117 | 127 | ||
| 118 | # Build bootloader only (rsboot submodule) | 128 | # Build bootloader only (rsboot submodule) |
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 700dcc7..fc47c82 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -138,6 +138,11 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 138 | rsnix::serial_println!("serial: COM1 115200 8N1"); | 138 | rsnix::serial_println!("serial: COM1 115200 8N1"); |
| 139 | rsnix::ps2::init(); | 139 | rsnix::ps2::init(); |
| 140 | rsnix::serial_println!("ps2: keyboard initialized"); | 140 | rsnix::serial_println!("ps2: keyboard initialized"); |
| 141 | if rsnix::ps2::mouse_init() { | ||
| 142 | rsnix::serial_println!("ps2: mouse ready (IRQ12, stream mode)"); | ||
| 143 | } else { | ||
| 144 | rsnix::serial_println!("ps2: mouse not present"); | ||
| 145 | } | ||
| 141 | if rsnix::gdt::init() { | 146 | if rsnix::gdt::init() { |
| 142 | rsnix::serial_println!("gdt: own GDT + TSS (IST: page-fault/double-fault)"); | 147 | rsnix::serial_println!("gdt: own GDT + TSS (IST: page-fault/double-fault)"); |
| 143 | } else { | 148 | } else { |
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index b560bfa..0b7a772 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs | |||
| @@ -46,6 +46,7 @@ enum CharDevice { | |||
| 46 | Zero, | 46 | Zero, |
| 47 | Console, | 47 | Console, |
| 48 | Serial, | 48 | Serial, |
| 49 | Mouse, | ||
| 49 | } | 50 | } |
| 50 | 51 | ||
| 51 | enum NodeData { | 52 | enum NodeData { |
| @@ -171,6 +172,12 @@ pub fn init() -> (usize, usize) { | |||
| 171 | FileType::Char, | 172 | FileType::Char, |
| 172 | NodeData::Char(CharDevice::Serial), | 173 | NodeData::Char(CharDevice::Serial), |
| 173 | ); | 174 | ); |
| 175 | add( | ||
| 176 | "mouse", | ||
| 177 | Some(dev), | ||
| 178 | FileType::Char, | ||
| 179 | NodeData::Char(CharDevice::Mouse), | ||
| 180 | ); | ||
| 174 | 181 | ||
| 175 | add( | 182 | add( |
| 176 | "motd", | 183 | "motd", |
| @@ -700,11 +707,42 @@ fn dev_text(dev: CharDevice, _pos: u64) -> String { | |||
| 700 | CharDevice::Version => version_text(), | 707 | CharDevice::Version => version_text(), |
| 701 | CharDevice::Meminfo => meminfo_text(), | 708 | CharDevice::Meminfo => meminfo_text(), |
| 702 | CharDevice::Klog => crate::klog::dump(), | 709 | CharDevice::Klog => crate::klog::dump(), |
| 710 | CharDevice::Mouse => mouse_text(), | ||
| 703 | CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), | 711 | CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), |
| 704 | CharDevice::Zero => String::new(), | 712 | CharDevice::Zero => String::new(), |
| 705 | } | 713 | } |
| 706 | } | 714 | } |
| 707 | 715 | ||
| 716 | /// Render `/dev/mouse`: drain every pending PS/2 event (up to 32) into one | ||
| 717 | /// line each, so `cat /dev/mouse` shows the movement that happened since | ||
| 718 | /// the last read. Falls back to a clear message when there is no device or | ||
| 719 | /// nothing pending. | ||
| 720 | fn mouse_text() -> String { | ||
| 721 | if !crate::ps2::mouse_present() { | ||
| 722 | return String::from("mouse: no PS/2 mouse\n"); | ||
| 723 | } | ||
| 724 | let mut s = String::new(); | ||
| 725 | for _ in 0..32 { | ||
| 726 | let Some(ev) = crate::ps2::try_poll_mouse() else { | ||
| 727 | break; | ||
| 728 | }; | ||
| 729 | let mut b = String::new(); | ||
| 730 | b.push(if ev.left { 'L' } else { '-' }); | ||
| 731 | b.push(if ev.right { 'R' } else { '-' }); | ||
| 732 | b.push(if ev.middle { 'M' } else { '-' }); | ||
| 733 | s.push_str(&alloc::format!( | ||
| 734 | "dx={:+} dy={:+} buttons={}\n", | ||
| 735 | ev.dx, | ||
| 736 | ev.dy, | ||
| 737 | b | ||
| 738 | )); | ||
| 739 | } | ||
| 740 | if s.is_empty() { | ||
| 741 | s.push_str("mouse: no events\n"); | ||
| 742 | } | ||
| 743 | s | ||
| 744 | } | ||
| 745 | |||
| 708 | // --- self-test ------------------------------------------------------------ | 746 | // --- self-test ------------------------------------------------------------ |
| 709 | 747 | ||
| 710 | pub fn self_test() -> bool { | 748 | pub fn self_test() -> bool { |
diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index 6abd54d..7ea0b07 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs | |||
| @@ -153,19 +153,22 @@ pub fn uptime_ms() -> u64 { | |||
| 153 | } | 153 | } |
| 154 | 154 | ||
| 155 | // --------------------------------------------------------------------------- | 155 | // --------------------------------------------------------------------------- |
| 156 | // Lock-free SPSC ring buffer for keyboard scancodes | 156 | // Lock-free SPSC ring buffers for PS/2 input (keyboard scancodes + mouse |
| 157 | // bytes). One per device: the ISR routes each data-port byte to the right | ||
| 158 | // ring via the controller status register's aux bit (bit 5), so the two | ||
| 159 | // devices never steal each other's bytes even when both are pending at once. | ||
| 157 | // --------------------------------------------------------------------------- | 160 | // --------------------------------------------------------------------------- |
| 158 | 161 | ||
| 159 | /// Single-producer (ISR) / single-consumer (main) lock-free ring buffer. | 162 | /// Single-producer (ISR) / single-consumer (main) lock-free ring buffer. |
| 160 | struct KeyBuf { | 163 | struct ByteRing { |
| 161 | buf: [core::cell::UnsafeCell<u8>; 256], | 164 | buf: [core::cell::UnsafeCell<u8>; 256], |
| 162 | head: AtomicU16, // next slot to read (written by consumer / main) | 165 | head: AtomicU16, // next slot to read (written by consumer / main) |
| 163 | tail: AtomicU16, // next slot to write (written by producer / ISR) | 166 | tail: AtomicU16, // next slot to write (written by producer / ISR) |
| 164 | } | 167 | } |
| 165 | 168 | ||
| 166 | unsafe impl Sync for KeyBuf {} | 169 | unsafe impl Sync for ByteRing {} |
| 167 | 170 | ||
| 168 | impl KeyBuf { | 171 | impl ByteRing { |
| 169 | const fn new() -> Self { | 172 | const fn new() -> Self { |
| 170 | // Inline-const array repeat (stable since 1.79): each element is a | 173 | // Inline-const array repeat (stable since 1.79): each element is a |
| 171 | // fresh `UnsafeCell::new(0)` value, so no interior-mutable const. | 174 | // fresh `UnsafeCell::new(0)` value, so no interior-mutable const. |
| @@ -205,7 +208,8 @@ impl KeyBuf { | |||
| 205 | } | 208 | } |
| 206 | } | 209 | } |
| 207 | 210 | ||
| 208 | static KEYBUF: KeyBuf = KeyBuf::new(); | 211 | static KEYBUF: ByteRing = ByteRing::new(); |
| 212 | static MOUSEBUF: ByteRing = ByteRing::new(); | ||
| 209 | 213 | ||
| 210 | // --------------------------------------------------------------------------- | 214 | // --------------------------------------------------------------------------- |
| 211 | // Interrupt stubs (assembly) and dispatch | 215 | // Interrupt stubs (assembly) and dispatch |
| @@ -240,6 +244,14 @@ core::arch::global_asm!( | |||
| 240 | " push 33", | 244 | " push 33", |
| 241 | " jmp isr_common", | 245 | " jmp isr_common", |
| 242 | "", | 246 | "", |
| 247 | // PS/2 mouse IRQ (IRQ12 → vector 44). | ||
| 248 | ".globl isr_stub_44", | ||
| 249 | ".p2align 4", | ||
| 250 | "isr_stub_44:", | ||
| 251 | " push 0", | ||
| 252 | " push 44", | ||
| 253 | " jmp isr_common", | ||
| 254 | "", | ||
| 243 | // Software yield (vector 0x60 — outside the PIC's 32-47 range, which | 255 | // Software yield (vector 0x60 — outside the PIC's 32-47 range, which |
| 244 | // is why 0x40 was rejected: 0x40 = 64 decimal = slave IRQ 8 slot). | 256 | // is why 0x40 was rejected: 0x40 = 64 decimal = slave IRQ 8 slot). |
| 245 | ".globl isr_stub_60", | 257 | ".globl isr_stub_60", |
| @@ -345,13 +357,27 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { | |||
| 345 | eoi(0); | 357 | eoi(0); |
| 346 | crate::sched::on_tick(fp) | 358 | crate::sched::on_tick(fp) |
| 347 | } | 359 | } |
| 348 | 33 => { | 360 | 33 | 44 => { |
| 349 | let byte: u8; | 361 | // PS/2 data byte. The controller's status register tells us |
| 350 | core::arch::asm!("in al, dx", out("al") byte, in("dx") 0x60u16, | 362 | // which device it came from (bit 5 = auxiliary/mouse), so the |
| 363 | // byte is routed to the matching ring — both handlers must | ||
| 364 | // check, because when both devices have data pending the | ||
| 365 | // first IRQ serviced can find the other device's byte in the | ||
| 366 | // output buffer. | ||
| 367 | let status: u8; | ||
| 368 | core::arch::asm!("in al, dx", out("al") status, in("dx") 0x64u16, | ||
| 351 | options(nostack, preserves_flags)); | 369 | options(nostack, preserves_flags)); |
| 352 | // Push scancode to ring buffer (called with interrupts disabled). | 370 | if status & 0x01 != 0 { |
| 353 | KEYBUF.push(byte); | 371 | let byte: u8; |
| 354 | eoi(1); | 372 | core::arch::asm!("in al, dx", out("al") byte, in("dx") 0x60u16, |
| 373 | options(nostack, preserves_flags)); | ||
| 374 | if status & 0x20 != 0 { | ||
| 375 | MOUSEBUF.push(byte); | ||
| 376 | } else { | ||
| 377 | KEYBUF.push(byte); | ||
| 378 | } | ||
| 379 | } | ||
| 380 | eoi((frame.vector - 32) as u8); | ||
| 355 | fp | 381 | fp |
| 356 | } | 382 | } |
| 357 | 8 => { | 383 | 8 => { |
| @@ -381,6 +407,12 @@ pub fn read_scancode() -> Option<u8> { | |||
| 381 | KEYBUF.pop() | 407 | KEYBUF.pop() |
| 382 | } | 408 | } |
| 383 | 409 | ||
| 410 | /// Read one mouse data byte from the interrupt-driven ring buffer | ||
| 411 | /// (non-blocking). Packets are assembled in `ps2::try_poll_mouse`. | ||
| 412 | pub fn read_mouse_byte() -> Option<u8> { | ||
| 413 | MOUSEBUF.pop() | ||
| 414 | } | ||
| 415 | |||
| 384 | /// Global tick counter (incremented by timer IRQ, ~100 Hz). u64 so | 416 | /// Global tick counter (incremented by timer IRQ, ~100 Hz). u64 so |
| 385 | /// uptime never wraps; `uptime_ms` interpolates the PIT counter for the | 417 | /// uptime never wraps; `uptime_ms` interpolates the PIT counter for the |
| 386 | /// sub-tick fraction. | 418 | /// sub-tick fraction. |
| @@ -406,6 +438,8 @@ pub fn init() { | |||
| 406 | // Override vectors for interrupt we actually handle. | 438 | // Override vectors for interrupt we actually handle. |
| 407 | idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); | 439 | idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); |
| 408 | idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); | 440 | idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); |
| 441 | // PS/2 mouse (IRQ12). | ||
| 442 | idt_set_entry(idt_ptr, 44, isr_stub_44 as *const () as u64, cs, 0x0Eu8, 0); | ||
| 409 | // Yield (software interrupt). | 443 | // Yield (software interrupt). |
| 410 | idt_set_entry( | 444 | idt_set_entry( |
| 411 | idt_ptr, | 445 | idt_ptr, |
| @@ -439,13 +473,20 @@ pub fn init() { | |||
| 439 | pit_set_rate(); | 473 | pit_set_rate(); |
| 440 | PIT_READY.store(true, Ordering::Relaxed); | 474 | PIT_READY.store(true, Ordering::Relaxed); |
| 441 | 475 | ||
| 442 | // Unmask timer (IRQ0 → bit 0) and keyboard (IRQ1 → bit 1) on master. | 476 | // Unmask timer (IRQ0), keyboard (IRQ1) and the cascade line (IRQ2) |
| 477 | // on the master; unmask IRQ12 (PS/2 mouse) on the slave. | ||
| 443 | let mut mask: u8; | 478 | let mut mask: u8; |
| 444 | asm!("in al, dx", out("al") mask, in("dx") PIC1_DATA, | 479 | asm!("in al, dx", out("al") mask, in("dx") PIC1_DATA, |
| 445 | options(nostack, preserves_flags)); | 480 | options(nostack, preserves_flags)); |
| 446 | mask &= !0x03u8; | 481 | mask &= !0x07u8; |
| 447 | asm!("out dx, al", in("dx") PIC1_DATA, in("al") mask, | 482 | asm!("out dx, al", in("dx") PIC1_DATA, in("al") mask, |
| 448 | options(nostack, preserves_flags)); | 483 | options(nostack, preserves_flags)); |
| 484 | let mut mask2: u8; | ||
| 485 | asm!("in al, dx", out("al") mask2, in("dx") PIC2_DATA, | ||
| 486 | options(nostack, preserves_flags)); | ||
| 487 | mask2 &= !0x10u8; // IRQ12 → bit 4 of the slave PIC | ||
| 488 | asm!("out dx, al", in("dx") PIC2_DATA, in("al") mask2, | ||
| 489 | options(nostack, preserves_flags)); | ||
| 449 | 490 | ||
| 450 | // Enable interrupts. | 491 | // Enable interrupts. |
| 451 | asm!("sti", options(nostack)); | 492 | asm!("sti", options(nostack)); |
| @@ -469,6 +510,7 @@ unsafe extern "C" { | |||
| 469 | fn isr_stub_14(); | 510 | fn isr_stub_14(); |
| 470 | fn isr_stub_32(); | 511 | fn isr_stub_32(); |
| 471 | fn isr_stub_33(); | 512 | fn isr_stub_33(); |
| 513 | fn isr_stub_44(); | ||
| 472 | fn isr_stub_60(); | 514 | fn isr_stub_60(); |
| 473 | fn isr_stub_spurious(); | 515 | fn isr_stub_spurious(); |
| 474 | } | 516 | } |
diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 35848b6..5f4a804 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs | |||
| @@ -16,6 +16,7 @@ const MOD_RALT: u8 = 0x20; | |||
| 16 | const MOD_CAPS: u8 = 0x40; | 16 | const MOD_CAPS: u8 = 0x40; |
| 17 | 17 | ||
| 18 | /// A decoded key event. | 18 | /// A decoded key event. |
| 19 | #[derive(Clone, Copy, Debug)] | ||
| 19 | pub enum KeyEvent { | 20 | pub enum KeyEvent { |
| 20 | Char(char), | 21 | Char(char), |
| 21 | Enter, | 22 | Enter, |
| @@ -400,3 +401,185 @@ fn ctrl_held() -> bool { | |||
| 400 | let state = kbd(); | 401 | let state = kbd(); |
| 401 | state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0 | 402 | state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0 |
| 402 | } | 403 | } |
| 404 | |||
| 405 | // --------------------------------------------------------------------------- | ||
| 406 | // PS/2 mouse — auxiliary channel, IRQ12, stream mode (3-byte packets) | ||
| 407 | // --------------------------------------------------------------------------- | ||
| 408 | |||
| 409 | // PS/2 controller commands (written to port 0x64). | ||
| 410 | const CMD_AUX_ENABLE: u8 = 0xA8; // enable the auxiliary (second) port | ||
| 411 | const CMD_READ_CFG: u8 = 0x20; // read Configuration Byte → port 0x60 | ||
| 412 | const CMD_WRITE_CFG: u8 = 0x60; // write Configuration Byte ← port 0x60 | ||
| 413 | const CMD_SEND_TO_AUX: u8 = 0xD4; // next byte on 0x60 goes to the mouse | ||
| 414 | |||
| 415 | // Configuration Byte bits (as sent via CMD_READ_CFG / CMD_WRITE_CFG). | ||
| 416 | // Bit 0 (IRQ1/keyboard) is set by `init`; this driver preserves it by | ||
| 417 | // reading the byte and only ORing in the two mouse bits below. | ||
| 418 | const CFG_AUX_IRQ: u8 = 0x02; // bit 1 — IRQ12 (mouse) | ||
| 419 | const CFG_AUX_CLOCK: u8 = 0x20; // bit 5 — auxiliary port clock enable | ||
| 420 | |||
| 421 | // Mouse device commands (sent via CMD_SEND_TO_AUX). | ||
| 422 | const AUX_SET_DEFAULTS: u8 = 0xF6; // 100 Hz sample, 4 counts/mm, 1:1 | ||
| 423 | const AUX_ENABLE: u8 = 0xF4; // start data reporting (stream mode) | ||
| 424 | const AUX_ACK: u8 = 0xFA; | ||
| 425 | |||
| 426 | /// A decoded PS/2 mouse event (one 3-byte packet). | ||
| 427 | /// | ||
| 428 | /// Movement is **screen-relative**: `dx` positive = right, `dy` positive = | ||
| 429 | /// down. (The PS/2 wire format is Y-up-positive; the driver negates it so | ||
| 430 | /// consumers can apply the deltas to framebuffer coordinates directly.) | ||
| 431 | #[derive(Clone, Copy, Debug)] | ||
| 432 | pub struct MouseEvent { | ||
| 433 | pub dx: i16, | ||
| 434 | pub dy: i16, | ||
| 435 | pub left: bool, | ||
| 436 | pub right: bool, | ||
| 437 | pub middle: bool, | ||
| 438 | } | ||
| 439 | |||
| 440 | /// Packet assembly state — lives in main context (consumer side), fed by | ||
| 441 | /// the raw byte ring that the IRQ12 handler fills. | ||
| 442 | struct MouseState { | ||
| 443 | phase: u8, // 0 = awaiting start byte, 1 = await X, 2 = await Y | ||
| 444 | pkt: [u8; 3], | ||
| 445 | } | ||
| 446 | |||
| 447 | static mut MOUSE: MouseState = MouseState { | ||
| 448 | phase: 0, | ||
| 449 | pkt: [0; 3], | ||
| 450 | }; | ||
| 451 | static MOUSE_PRESENT: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); | ||
| 452 | |||
| 453 | /// True once `mouse_init` found a device that ACKed its setup commands. | ||
| 454 | pub fn mouse_present() -> bool { | ||
| 455 | MOUSE_PRESENT.load(core::sync::atomic::Ordering::Relaxed) | ||
| 456 | } | ||
| 457 | |||
| 458 | /// Enable the auxiliary (mouse) channel and put the device into stream | ||
| 459 | /// mode. Returns false when no device ACKs (controller without a second | ||
| 460 | /// port, or a missing mouse) — the kernel then just runs keyboard-only. | ||
| 461 | /// | ||
| 462 | /// Must run *before* `interrupts::init` unmasks IRQ12: the init handshake | ||
| 463 | /// reads the data port directly, so any stray mouse bytes would otherwise | ||
| 464 | /// be split across the ISR path mid-handshake. | ||
| 465 | pub fn mouse_init() -> bool { | ||
| 466 | drain_input(); | ||
| 467 | unsafe { | ||
| 468 | // 1. Enable the auxiliary port itself. | ||
| 469 | wait_write(); | ||
| 470 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE, | ||
| 471 | options(nostack, preserves_flags)); | ||
| 472 | |||
| 473 | // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read, | ||
| 474 | // OR the two bits, write back (preserves the keyboard IRQ bit | ||
| 475 | // that ps2::init set). | ||
| 476 | wait_write(); | ||
| 477 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG, | ||
| 478 | options(nostack, preserves_flags)); | ||
| 479 | wait_read(); | ||
| 480 | let mut cfg: u8; | ||
| 481 | core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT, | ||
| 482 | options(nostack, preserves_flags)); | ||
| 483 | cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK; | ||
| 484 | wait_write(); | ||
| 485 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG, | ||
| 486 | options(nostack, preserves_flags)); | ||
| 487 | wait_write(); | ||
| 488 | core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg, | ||
| 489 | options(nostack, preserves_flags)); | ||
| 490 | |||
| 491 | // 3. Device setup. `set defaults` normalises the sample rate / | ||
| 492 | // resolution before enabling reporting, so the packet stream | ||
| 493 | // starts at a known cadence. Both commands need a device ACK; | ||
| 494 | // a timeout here means no mouse — bail out cleanly. | ||
| 495 | if !aux_command(AUX_SET_DEFAULTS) || !aux_command(AUX_ENABLE) { | ||
| 496 | return false; | ||
| 497 | } | ||
| 498 | } | ||
| 499 | MOUSE_PRESENT.store(true, core::sync::atomic::Ordering::Relaxed); | ||
| 500 | true | ||
| 501 | } | ||
| 502 | |||
| 503 | /// Send one byte to the mouse via the 0xD4 channel and wait for its ACK. | ||
| 504 | /// The wait is bounded so a missing device cannot hang the boot. | ||
| 505 | fn aux_command(cmd: u8) -> bool { | ||
| 506 | unsafe { | ||
| 507 | wait_write(); | ||
| 508 | core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX, | ||
| 509 | options(nostack, preserves_flags)); | ||
| 510 | wait_write(); | ||
| 511 | core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd, | ||
| 512 | options(nostack, preserves_flags)); | ||
| 513 | for _ in 0..100_000 { | ||
| 514 | let status: u8; | ||
| 515 | core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, | ||
| 516 | options(nostack, preserves_flags)); | ||
| 517 | if status & STAT_OUTPUT_FULL != 0 { | ||
| 518 | let b: u8; | ||
| 519 | core::arch::asm!("in al, dx", out("al") b, in("dx") DATA_PORT, | ||
| 520 | options(nostack, preserves_flags)); | ||
| 521 | return b == AUX_ACK; | ||
| 522 | } | ||
| 523 | } | ||
| 524 | } | ||
| 525 | false | ||
| 526 | } | ||
| 527 | |||
| 528 | /// Non-blocking: decode the next pending mouse event, if a full packet has | ||
| 529 | /// arrived. Raw bytes are drained from the IRQ ring; incomplete packets | ||
| 530 | /// stay buffered in `MouseState` until the remaining bytes land. | ||
| 531 | pub fn try_poll_mouse() -> Option<MouseEvent> { | ||
| 532 | while let Some(b) = crate::interrupts::read_mouse_byte() { | ||
| 533 | if let Some(ev) = mouse_byte(b) { | ||
| 534 | return Some(ev); | ||
| 535 | } | ||
| 536 | } | ||
| 537 | None | ||
| 538 | } | ||
| 539 | |||
| 540 | /// Feed one raw byte into the packet assembler. Byte 0 of every standard | ||
| 541 | /// PS/2 mouse packet has bit 3 set (the sync marker), so a lost byte (ring | ||
| 542 | /// overrun, init response) is detected on the next packet and the stream | ||
| 543 | /// re-syncs — the marker is only accepted as a packet start. | ||
| 544 | fn mouse_byte(b: u8) -> Option<MouseEvent> { | ||
| 545 | let m = unsafe { &mut *core::ptr::addr_of_mut!(MOUSE) }; | ||
| 546 | match m.phase { | ||
| 547 | 0 => { | ||
| 548 | if b & 0x08 != 0 { | ||
| 549 | m.pkt[0] = b; | ||
| 550 | m.phase = 1; | ||
| 551 | } | ||
| 552 | None | ||
| 553 | } | ||
| 554 | 1 => { | ||
| 555 | m.pkt[1] = b; | ||
| 556 | m.phase = 2; | ||
| 557 | None | ||
| 558 | } | ||
| 559 | _ => { | ||
| 560 | m.pkt[2] = b; | ||
| 561 | m.phase = 0; | ||
| 562 | Some(MouseEvent { | ||
| 563 | // 9-bit signed: low 8 bits in the data byte, sign bit in | ||
| 564 | // byte 0 (bit 4 = X, bit 5 = Y). | ||
| 565 | dx: signed9(m.pkt[0], m.pkt[1], 0x10), | ||
| 566 | // PS/2 reports Y positive = up; flip to screen convention. | ||
| 567 | dy: -signed9(m.pkt[0], m.pkt[2], 0x20), | ||
| 568 | left: m.pkt[0] & 0x01 != 0, | ||
| 569 | right: m.pkt[0] & 0x02 != 0, | ||
| 570 | middle: m.pkt[0] & 0x04 != 0, | ||
| 571 | }) | ||
| 572 | } | ||
| 573 | } | ||
| 574 | } | ||
| 575 | |||
| 576 | /// Sign-extend an 8-bit movement byte to 9 bits using the packet's sign | ||
| 577 | /// flag (bit `sign` of byte 0), widening to i16 for consumers. | ||
| 578 | fn signed9(b0: u8, b: u8, sign: u8) -> i16 { | ||
| 579 | let mut v = (b as i8) as i16; | ||
| 580 | if b0 & sign != 0 { | ||
| 581 | // Set the 9th bit (0xFF00 = sign-extension mask for i16). | ||
| 582 | v |= 0xFF00u16 as i16; | ||
| 583 | } | ||
| 584 | v | ||
| 585 | } | ||
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 | |||
| 42 | const NAME_MAX: usize = 24; | 42 | const NAME_MAX: usize = 24; |
| 43 | const PANIC_MAX: usize = 64; | 43 | const PANIC_MAX: usize = 64; |
| 44 | 44 | ||
| 45 | // --------------------------------------------------------------------------- | ||
| 46 | // Inter-task message queues | ||
| 47 | // --------------------------------------------------------------------------- | ||
| 48 | |||
| 49 | /// Capacity of each task's message ring (power of two; full sends drop | ||
| 50 | /// with a "queue full" error). Sized so input bursts (a 100 Hz mouse, | ||
| 51 | /// key repeat) far outrun a blocked consumer: 16 × ~1.6 s at worst. | ||
| 52 | const MSG_CAP: usize = 16; | ||
| 53 | |||
| 54 | /// One inter-task message. Input events use the same decoded types the | ||
| 55 | /// PS/2 drivers produce, so a window's input routing is "post what the | ||
| 56 | /// driver would have delivered to the console". `User(u64)` is the | ||
| 57 | /// generic escape hatch (window commands, app signals, …). | ||
| 58 | #[derive(Clone, Copy, Debug)] | ||
| 59 | pub enum TaskMsg { | ||
| 60 | Key(crate::ps2::KeyEvent), | ||
| 61 | Mouse(crate::ps2::MouseEvent), | ||
| 62 | Timer, | ||
| 63 | User(u64), | ||
| 64 | } | ||
| 65 | |||
| 66 | /// Decode a message for logging/display (no Debug dep in the shell task). | ||
| 67 | pub fn msg_describe(msg: &TaskMsg) -> alloc::string::String { | ||
| 68 | use crate::ps2::KeyEvent; | ||
| 69 | match msg { | ||
| 70 | TaskMsg::Key(k) => match k { | ||
| 71 | KeyEvent::Char(c) => alloc::format!("Key('{}')", c), | ||
| 72 | KeyEvent::Enter => "Key(Enter)".into(), | ||
| 73 | KeyEvent::Backspace => "Key(Backspace)".into(), | ||
| 74 | KeyEvent::Tab => "Key(Tab)".into(), | ||
| 75 | KeyEvent::Escape => "Key(Escape)".into(), | ||
| 76 | KeyEvent::Ctrl(c) => alloc::format!("Key(Ctrl-{})", c), | ||
| 77 | KeyEvent::CtrlArrow(d) => alloc::format!("Key(CtrlArrow{})", d), | ||
| 78 | KeyEvent::ArrowUp => "Key(Up)".into(), | ||
| 79 | KeyEvent::ArrowDown => "Key(Down)".into(), | ||
| 80 | KeyEvent::ArrowLeft => "Key(Left)".into(), | ||
| 81 | KeyEvent::ArrowRight => "Key(Right)".into(), | ||
| 82 | KeyEvent::Home => "Key(Home)".into(), | ||
| 83 | KeyEvent::End => "Key(End)".into(), | ||
| 84 | KeyEvent::PageUp => "Key(PgUp)".into(), | ||
| 85 | KeyEvent::PageDown => "Key(PgDn)".into(), | ||
| 86 | KeyEvent::Delete => "Key(Delete)".into(), | ||
| 87 | KeyEvent::F(n) => alloc::format!("Key(F{})", n), | ||
| 88 | KeyEvent::Unknown(b) => alloc::format!("Key(0x{:02x})", b), | ||
| 89 | }, | ||
| 90 | TaskMsg::Mouse(m) => alloc::format!( | ||
| 91 | "Mouse({:+},{:+} {} {})", | ||
| 92 | m.dx, | ||
| 93 | m.dy, | ||
| 94 | if m.left { "L" } else { "-" }, | ||
| 95 | if m.right { "R" } else { "-" } | ||
| 96 | ), | ||
| 97 | TaskMsg::Timer => "Timer".into(), | ||
| 98 | TaskMsg::User(n) => alloc::format!("User({})", n), | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 45 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] | 102 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] |
| 46 | pub enum TaskState { | 103 | pub enum TaskState { |
| 47 | Ready, | 104 | Ready, |
| @@ -65,6 +122,13 @@ struct Task { | |||
| 65 | ticks: u64, | 122 | ticks: u64, |
| 66 | panic: [u8; PANIC_MAX], | 123 | panic: [u8; PANIC_MAX], |
| 67 | panic_len: u8, | 124 | panic_len: u8, |
| 125 | /// Per-task message ring (see `TaskMsg`). Accessed by the owning | ||
| 126 | /// task (pop) and by any poster (push) — both under the scheduler | ||
| 127 | /// lock, so `send` is callable from ISR or task context. | ||
| 128 | msg: [TaskMsg; MSG_CAP], | ||
| 129 | msg_head: u8, | ||
| 130 | msg_tail: u8, | ||
| 131 | msg_count: u8, | ||
| 68 | } | 132 | } |
| 69 | 133 | ||
| 70 | const TASK_NONE: Task = Task { | 134 | const TASK_NONE: Task = Task { |
| @@ -78,6 +142,10 @@ const TASK_NONE: Task = Task { | |||
| 78 | ticks: 0, | 142 | ticks: 0, |
| 79 | panic: [0; PANIC_MAX], | 143 | panic: [0; PANIC_MAX], |
| 80 | panic_len: 0, | 144 | panic_len: 0, |
| 145 | msg: [TaskMsg::User(0); MSG_CAP], | ||
| 146 | msg_head: 0, | ||
| 147 | msg_tail: 0, | ||
| 148 | msg_count: 0, | ||
| 81 | }; | 149 | }; |
| 82 | 150 | ||
| 83 | static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS]; | 151 | static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS]; |
| @@ -133,8 +201,18 @@ pub fn on_tick(cur: usize) -> usize { | |||
| 133 | TASKS[CURRENT].ticks += 1; | 201 | TASKS[CURRENT].ticks += 1; |
| 134 | let now = crate::interrupts::uptime_ms(); | 202 | let now = crate::interrupts::uptime_ms(); |
| 135 | for t in 0..MAX_TASKS { | 203 | for t in 0..MAX_TASKS { |
| 136 | if TASKS[t].state == TaskState::Sleeping && TASKS[t].wake_at <= now { | 204 | match TASKS[t].state { |
| 137 | TASKS[t].state = TaskState::Ready; | 205 | TaskState::Sleeping if TASKS[t].wake_at <= now => { |
| 206 | TASKS[t].state = TaskState::Ready; | ||
| 207 | } | ||
| 208 | // Safety net for the recv race: a task that blocked | ||
| 209 | // *after* its message was queued (or whose poster never | ||
| 210 | // got to wake it) wakes on the next tick as long as its | ||
| 211 | // queue is non-empty. | ||
| 212 | TaskState::Blocked if TASKS[t].msg_count > 0 => { | ||
| 213 | TASKS[t].state = TaskState::Ready; | ||
| 214 | } | ||
| 215 | _ => {} | ||
| 138 | } | 216 | } |
| 139 | } | 217 | } |
| 140 | pick_next(cur) | 218 | pick_next(cur) |
| @@ -279,6 +357,10 @@ fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { | |||
| 279 | ticks: 0, | 357 | ticks: 0, |
| 280 | panic: [0; PANIC_MAX], | 358 | panic: [0; PANIC_MAX], |
| 281 | panic_len: 0, | 359 | panic_len: 0, |
| 360 | msg: [TaskMsg::User(0); MSG_CAP], | ||
| 361 | msg_head: 0, | ||
| 362 | msg_tail: 0, | ||
| 363 | msg_count: 0, | ||
| 282 | }; | 364 | }; |
| 283 | } | 365 | } |
| 284 | true | 366 | true |
| @@ -319,6 +401,110 @@ pub fn yield_now() { | |||
| 319 | yield_int(); | 401 | yield_int(); |
| 320 | } | 402 | } |
| 321 | 403 | ||
| 404 | // --------------------------------------------------------------------------- | ||
| 405 | // Message passing | ||
| 406 | // --------------------------------------------------------------------------- | ||
| 407 | |||
| 408 | /// Post a message to a task's queue and wake it. Safe from any context: | ||
| 409 | /// a task (e.g. a window manager routing input), the shell, or an ISR — | ||
| 410 | /// the scheduler lock makes the queue mutation atomic w.r.t. switching. | ||
| 411 | /// Returns an error when the task is gone or its queue is full. | ||
| 412 | pub fn send(id: usize, msg: TaskMsg) -> Result<(), &'static str> { | ||
| 413 | if id >= MAX_TASKS { | ||
| 414 | return Err("no such task"); | ||
| 415 | } | ||
| 416 | lock(); | ||
| 417 | let r = unsafe { | ||
| 418 | let t = &TASKS[id]; | ||
| 419 | if t.name_len == 0 || t.state == TaskState::Done { | ||
| 420 | Err("no such task") | ||
| 421 | } else if push_msg(id, msg) { | ||
| 422 | if TASKS[id].state == TaskState::Blocked { | ||
| 423 | TASKS[id].state = TaskState::Ready; | ||
| 424 | } | ||
| 425 | Ok(()) | ||
| 426 | } else { | ||
| 427 | Err("queue full") | ||
| 428 | } | ||
| 429 | }; | ||
| 430 | unlock(); | ||
| 431 | r | ||
| 432 | } | ||
| 433 | |||
| 434 | /// Pop the oldest pending message of the current task, if any. | ||
| 435 | pub fn recv() -> Option<TaskMsg> { | ||
| 436 | lock(); | ||
| 437 | let m = unsafe { pop_msg(CURRENT) }; | ||
| 438 | unlock(); | ||
| 439 | m | ||
| 440 | } | ||
| 441 | |||
| 442 | /// Block until a message arrives, then return it. Wake is two-fold: | ||
| 443 | /// `send` flips a Blocked task straight to Ready, and the tick scan | ||
| 444 | /// (see `on_tick`) catches the race where the task blocked after the | ||
| 445 | /// message was already queued. | ||
| 446 | pub fn recv_blocking() -> TaskMsg { | ||
| 447 | loop { | ||
| 448 | if let Some(m) = recv() { | ||
| 449 | return m; | ||
| 450 | } | ||
| 451 | lock(); | ||
| 452 | unsafe { | ||
| 453 | TASKS[CURRENT].state = TaskState::Blocked; | ||
| 454 | } | ||
| 455 | unlock(); | ||
| 456 | yield_int(); | ||
| 457 | } | ||
| 458 | } | ||
| 459 | |||
| 460 | /// Push onto the task's ring (caller holds the scheduler lock). | ||
| 461 | unsafe fn push_msg(id: usize, msg: TaskMsg) -> bool { | ||
| 462 | unsafe { | ||
| 463 | let t = &mut TASKS[id]; | ||
| 464 | if t.msg_count as usize >= MSG_CAP { | ||
| 465 | return false; | ||
| 466 | } | ||
| 467 | t.msg[t.msg_tail as usize] = msg; | ||
| 468 | t.msg_tail = (t.msg_tail + 1) & (MSG_CAP as u8 - 1); | ||
| 469 | t.msg_count += 1; | ||
| 470 | true | ||
| 471 | } | ||
| 472 | } | ||
| 473 | |||
| 474 | /// Pop from the current task's ring (caller holds the scheduler lock). | ||
| 475 | unsafe fn pop_msg(id: usize) -> Option<TaskMsg> { | ||
| 476 | unsafe { | ||
| 477 | let t = &mut TASKS[id]; | ||
| 478 | if t.msg_count == 0 { | ||
| 479 | return None; | ||
| 480 | } | ||
| 481 | let m = t.msg[t.msg_head as usize]; | ||
| 482 | t.msg_head = (t.msg_head + 1) & (MSG_CAP as u8 - 1); | ||
| 483 | t.msg_count -= 1; | ||
| 484 | Some(m) | ||
| 485 | } | ||
| 486 | } | ||
| 487 | |||
| 488 | /// Pending message count of a task (for `jobs`). | ||
| 489 | pub fn pending_msgs(id: usize) -> usize { | ||
| 490 | lock(); | ||
| 491 | let n = unsafe { TASKS[id].msg_count as usize }; | ||
| 492 | unlock(); | ||
| 493 | n | ||
| 494 | } | ||
| 495 | |||
| 496 | /// Is the task slot live (spawned and not Done)? Used by the shell's | ||
| 497 | /// `mouse <id>` routing check and by the window manager later. | ||
| 498 | pub fn is_alive(id: usize) -> bool { | ||
| 499 | if id >= MAX_TASKS { | ||
| 500 | return false; | ||
| 501 | } | ||
| 502 | lock(); | ||
| 503 | let alive = unsafe { TASKS[id].name_len != 0 && TASKS[id].state != TaskState::Done }; | ||
| 504 | unlock(); | ||
| 505 | alive | ||
| 506 | } | ||
| 507 | |||
| 322 | /// Software-interrupt the scheduler (vector 0x60). Net stack usage is | 508 | /// Software-interrupt the scheduler (vector 0x60). Net stack usage is |
| 323 | /// zero (the frame is popped by iretq), so no `nostack` — the compiler | 509 | /// zero (the frame is popped by iretq), so no `nostack` — the compiler |
| 324 | /// must not use the red zone around the interrupt. | 510 | /// must not use the red zone around the interrupt. |
| @@ -418,11 +604,12 @@ pub fn jobs() -> Vec<String> { | |||
| 418 | } | 604 | } |
| 419 | }; | 605 | }; |
| 420 | let mut line = alloc::format!( | 606 | let mut line = alloc::format!( |
| 421 | " {:>2} {:<12} {:<15} {:>6} ticks", | 607 | " {:>2} {:<12} {:<15} {:>6} ticks q={}", |
| 422 | i, | 608 | i, |
| 423 | name_str(i), | 609 | name_str(i), |
| 424 | state, | 610 | state, |
| 425 | t.ticks | 611 | t.ticks, |
| 612 | t.msg_count | ||
| 426 | ); | 613 | ); |
| 427 | if t.panic_len > 0 { | 614 | if t.panic_len > 0 { |
| 428 | let m = core::str::from_utf8(&t.panic[..t.panic_len as usize]).unwrap_or("?"); | 615 | let m = core::str::from_utf8(&t.panic[..t.panic_len as usize]).unwrap_or("?"); |
| @@ -505,6 +692,10 @@ pub fn init() { | |||
| 505 | ticks: 0, | 692 | ticks: 0, |
| 506 | panic: [0; PANIC_MAX], | 693 | panic: [0; PANIC_MAX], |
| 507 | panic_len: 0, | 694 | panic_len: 0, |
| 695 | msg: [TaskMsg::User(0); MSG_CAP], | ||
| 696 | msg_head: 0, | ||
| 697 | msg_tail: 0, | ||
| 698 | msg_count: 0, | ||
| 508 | }; | 699 | }; |
| 509 | spawn_at(IDLE_TID, idle_main, "idle"); | 700 | spawn_at(IDLE_TID, idle_main, "idle"); |
| 510 | SCHED_READY.store(true, Ordering::Relaxed); | 701 | SCHED_READY.store(true, Ordering::Relaxed); |
diff --git a/scripts/test-mouse.sh b/scripts/test-mouse.sh new file mode 100755 index 0000000..ed4df44 --- /dev/null +++ b/scripts/test-mouse.sh | |||
| @@ -0,0 +1,130 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # | ||
| 3 | # scripts/test-mouse.sh — automated PS/2 mouse round trip. | ||
| 4 | # | ||
| 5 | # Builds the kernel, boots QEMU headless, injects mouse movement and | ||
| 6 | # button presses through the monitor's `mouse_move` / `mouse_button` | ||
| 7 | # commands, and verifies the guest decoded them via IRQ12 → ring → | ||
| 8 | # packet decoder. Covers both surfaces: | ||
| 9 | # - `cat /dev/mouse` (VFS snapshot of pending events) | ||
| 10 | # - `mouse` (live streaming command, stopped by ^C) | ||
| 11 | # | ||
| 12 | # Usage: | ||
| 13 | # ./scripts/test-mouse.sh | ||
| 14 | # | ||
| 15 | # Environment: | ||
| 16 | # OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd) | ||
| 17 | # QEMU qemu binary (default: qemu-system-x86_64) | ||
| 18 | # | ||
| 19 | set -euo pipefail | ||
| 20 | |||
| 21 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 22 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| 23 | cd "$PROJECT_DIR" | ||
| 24 | |||
| 25 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 26 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 27 | ESP_DIR="$PROJECT_DIR/esp" | ||
| 28 | QPID=/tmp/rsdos-mouse-qpid | ||
| 29 | SOCK=/tmp/rsdos-mouse-qmon.sock | ||
| 30 | LOG=/tmp/rsdos-mouse-serial.log | ||
| 31 | QLOG=/tmp/rsdos-mouse-qemu.log | ||
| 32 | |||
| 33 | # Never leave a QEMU behind. | ||
| 34 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 35 | |||
| 36 | fail() { | ||
| 37 | echo "FAIL: $1" | ||
| 38 | [ -f "$LOG" ] && tail -20 "$LOG" | ||
| 39 | exit 1 | ||
| 40 | } | ||
| 41 | |||
| 42 | # wait_log FILE PATTERN TIMEOUT-SECONDS | ||
| 43 | wait_log() { | ||
| 44 | local i=0 | ||
| 45 | while [ "$i" -lt $((${3:-60} * 10)) ]; do | ||
| 46 | grep -q "$2" "$1" 2>/dev/null && return 0 | ||
| 47 | sleep 0.1 | ||
| 48 | i=$((i + 1)) | ||
| 49 | done | ||
| 50 | return 1 | ||
| 51 | } | ||
| 52 | |||
| 53 | # wait_qemu_exit TIMEOUT-SECONDS | ||
| 54 | wait_qemu_exit() { | ||
| 55 | local i=0 | ||
| 56 | while [ "$i" -lt $((${1:-30} * 10)) ]; do | ||
| 57 | kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 | ||
| 58 | sleep 0.1 | ||
| 59 | i=$((i + 1)) | ||
| 60 | done | ||
| 61 | return 1 | ||
| 62 | } | ||
| 63 | |||
| 64 | # qemu_hmp — send one raw HMP command to the monitor socket. | ||
| 65 | qemu_hmp() { | ||
| 66 | python3 - "$SOCK" "$1" <<'EOF' | ||
| 67 | import socket, sys, time | ||
| 68 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| 69 | s.connect(sys.argv[1]) | ||
| 70 | s.sendall((sys.argv[2] + "\n").encode()) | ||
| 71 | time.sleep(0.4) | ||
| 72 | EOF | ||
| 73 | } | ||
| 74 | |||
| 75 | echo "==> 1/5 build kernel (debug)" | ||
| 76 | cargo build --package rsdos --target x86_64-unknown-uefi | ||
| 77 | |||
| 78 | echo "==> 2/5 deploy to ESP" | ||
| 79 | mkdir -p "$ESP_DIR/EFI/BOOT" | ||
| 80 | cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" | ||
| 81 | |||
| 82 | echo "==> 3/5 boot QEMU (headless)" | ||
| 83 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 84 | sleep 1 | ||
| 85 | rm -f "$SOCK" "$LOG" | ||
| 86 | "$QEMU" -bios "$OVMF" \ | ||
| 87 | -drive file=fat:rw:"$ESP_DIR",format=raw \ | ||
| 88 | -serial file:"$LOG" \ | ||
| 89 | -monitor unix:"$SOCK",server,nowait \ | ||
| 90 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ | ||
| 91 | >"$QLOG" 2>&1 & | ||
| 92 | echo $! >"$QPID" | ||
| 93 | |||
| 94 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 95 | grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" | ||
| 96 | |||
| 97 | # Seed a move *before* any consumer runs, so `cat /dev/mouse` has a | ||
| 98 | # queued event to drain (exercises the VFS snapshot path). | ||
| 99 | qemu_hmp "mouse_move 8 4" | ||
| 100 | |||
| 101 | echo "==> 4/5 drive the shell (cat /dev/mouse, mouse stream, poweroff)" | ||
| 102 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | ||
| 103 | "cat /dev/mouse" \ | ||
| 104 | "mouse" | ||
| 105 | |||
| 106 | # While the `mouse` stream is live: a move, a left-press, a move with the | ||
| 107 | # button held, then release — the button state must ride along on the | ||
| 108 | # movement packet. | ||
| 109 | qemu_hmp "mouse_move 20 7" | ||
| 110 | qemu_hmp "mouse_button 1" | ||
| 111 | qemu_hmp "mouse_move -5 -3" | ||
| 112 | qemu_hmp "mouse_button 0" | ||
| 113 | qemu_hmp "sendkey ctrl-c" | ||
| 114 | |||
| 115 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | ||
| 116 | "poweroff" | ||
| 117 | |||
| 118 | echo "==> 5/5 wait for shutdown + verify" | ||
| 119 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 120 | |||
| 121 | grep -aq "ps2: mouse ready (IRQ12, stream mode)" "$LOG" || fail "no mouse-ready boot line" | ||
| 122 | grep -aq "dx=+8 dy=+4 buttons=---" "$LOG" || fail "/dev/mouse did not drain the queued move (dx=+8 dy=+4)" | ||
| 123 | grep -aq "dx=+20 dy=+7 buttons=---" "$LOG" || fail "mouse stream missed mouse_move 20 7" | ||
| 124 | grep -aq "dx=+0 dy=+0 buttons=L--" "$LOG" || fail "left-button press did not decode" | ||
| 125 | grep -aq "dx=-5 dy=-3 buttons=L--" "$LOG" || fail "held-button movement wrong (dx=-5 dy=-3 L)" | ||
| 126 | grep -aq "mouse: stopped" "$LOG" || fail "^C did not stop the mouse stream" | ||
| 127 | |||
| 128 | echo "" | ||
| 129 | echo "PASS — PS/2 mouse round trip OK (IRQ12, /dev/mouse, live stream, buttons)" | ||
| 130 | echo " events: 8,4 · 20,7 · L-press · -5,-3 held · release" | ||
diff --git a/scripts/test-msg.sh b/scripts/test-msg.sh new file mode 100755 index 0000000..0bd1c81 --- /dev/null +++ b/scripts/test-msg.sh | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # | ||
| 3 | # scripts/test-msg.sh — automated scheduler message-queue round trip. | ||
| 4 | # | ||
| 5 | # Builds the kernel, boots QEMU headless, and verifies inter-task | ||
| 6 | # messaging end to end: | ||
| 7 | # - `msgrecv` spawns a task that blocks on recv_blocking | ||
| 8 | # - `send <id> <type>` delivers every message type in order | ||
| 9 | # - `mouse <id>` routes *real* IRQ12 mouse events into the queue | ||
| 10 | # (the window-manager input path) | ||
| 11 | # - queue-full is a clean error (16-cap; tested against the shell's | ||
| 12 | # own never-consumed queue) | ||
| 13 | # - jobs shows q=0 after everything drains, task back to blocked | ||
| 14 | # | ||
| 15 | # Usage: | ||
| 16 | # ./scripts/test-msg.sh | ||
| 17 | # | ||
| 18 | # Environment: | ||
| 19 | # OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd) | ||
| 20 | # QEMU qemu binary (default: qemu-system-x86_64) | ||
| 21 | # | ||
| 22 | set -euo pipefail | ||
| 23 | |||
| 24 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 25 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| 26 | cd "$PROJECT_DIR" | ||
| 27 | |||
| 28 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 29 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 30 | ESP_DIR="$PROJECT_DIR/esp" | ||
| 31 | QPID=/tmp/rsdos-msg-qpid | ||
| 32 | SOCK=/tmp/rsdos-msg-qmon.sock | ||
| 33 | LOG=/tmp/rsdos-msg-serial.log | ||
| 34 | QLOG=/tmp/rsdos-msg-qemu.log | ||
| 35 | |||
| 36 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 37 | |||
| 38 | fail() { | ||
| 39 | echo "FAIL: $1" | ||
| 40 | [ -f "$LOG" ] && tail -20 "$LOG" | ||
| 41 | exit 1 | ||
| 42 | } | ||
| 43 | |||
| 44 | wait_log() { | ||
| 45 | local i=0 | ||
| 46 | while [ "$i" -lt $((${3:-60} * 10)) ]; do | ||
| 47 | grep -q "$2" "$1" 2>/dev/null && return 0 | ||
| 48 | sleep 0.1 | ||
| 49 | i=$((i + 1)) | ||
| 50 | done | ||
| 51 | return 1 | ||
| 52 | } | ||
| 53 | |||
| 54 | wait_qemu_exit() { | ||
| 55 | local i=0 | ||
| 56 | while [ "$i" -lt $((${1:-30} * 10)) ]; do | ||
| 57 | kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 | ||
| 58 | sleep 0.1 | ||
| 59 | i=$((i + 1)) | ||
| 60 | done | ||
| 61 | return 1 | ||
| 62 | } | ||
| 63 | |||
| 64 | qemu_hmp() { | ||
| 65 | python3 - "$SOCK" "$1" <<'EOF' | ||
| 66 | import socket, sys, time | ||
| 67 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| 68 | s.connect(sys.argv[1]) | ||
| 69 | s.sendall((sys.argv[2] + "\n").encode()) | ||
| 70 | time.sleep(0.4) | ||
| 71 | EOF | ||
| 72 | } | ||
| 73 | |||
| 74 | echo "==> 1/5 build kernel (debug)" | ||
| 75 | cargo build --package rsdos --target x86_64-unknown-uefi | ||
| 76 | |||
| 77 | echo "==> 2/5 deploy to ESP" | ||
| 78 | mkdir -p "$ESP_DIR/EFI/BOOT" | ||
| 79 | cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" | ||
| 80 | |||
| 81 | echo "==> 3/5 boot QEMU (headless)" | ||
| 82 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 83 | sleep 1 | ||
| 84 | rm -f "$SOCK" "$LOG" | ||
| 85 | "$QEMU" -bios "$OVMF" \ | ||
| 86 | -drive file=fat:rw:"$ESP_DIR",format=raw \ | ||
| 87 | -serial file:"$LOG" \ | ||
| 88 | -monitor unix:"$SOCK",server,nowait \ | ||
| 89 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ | ||
| 90 | >"$QLOG" 2>&1 & | ||
| 91 | echo $! >"$QPID" | ||
| 92 | |||
| 93 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 94 | |||
| 95 | echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)" | ||
| 96 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | ||
| 97 | "msgrecv" \ | ||
| 98 | "send 2 user 42" \ | ||
| 99 | "send 2 timer" \ | ||
| 100 | "send 2 key enter" \ | ||
| 101 | "send 2 key ctrl-c" \ | ||
| 102 | "send 2 key f3" \ | ||
| 103 | "send 2 mouse 5 -3 3" \ | ||
| 104 | "sleep 800" \ | ||
| 105 | "jobs" | ||
| 106 | |||
| 107 | # Real IRQ12 events through the queue: start routing to task 2, inject | ||
| 108 | # moves + a left press, stop. | ||
| 109 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 2" | ||
| 110 | qemu_hmp "mouse_move 20 7" | ||
| 111 | qemu_hmp "mouse_button 1" | ||
| 112 | qemu_hmp "mouse_move -5 -3" | ||
| 113 | qemu_hmp "mouse_button 0" | ||
| 114 | qemu_hmp "sendkey ctrl-c" | ||
| 115 | |||
| 116 | # Queue-full boundary: 16 sends fit, the 17th must fail cleanly. The | ||
| 117 | # shell never consumes its own queue, so nothing races the drain here. | ||
| 118 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | ||
| 119 | "send 0 user 1" \ | ||
| 120 | "send 0 user 2" \ | ||
| 121 | "send 0 user 3" \ | ||
| 122 | "send 0 user 4" \ | ||
| 123 | "send 0 user 5" \ | ||
| 124 | "send 0 user 6" \ | ||
| 125 | "send 0 user 7" \ | ||
| 126 | "send 0 user 8" \ | ||
| 127 | "send 0 user 9" \ | ||
| 128 | "send 0 user 10" \ | ||
| 129 | "send 0 user 11" \ | ||
| 130 | "send 0 user 12" \ | ||
| 131 | "send 0 user 13" \ | ||
| 132 | "send 0 user 14" \ | ||
| 133 | "send 0 user 15" \ | ||
| 134 | "send 0 user 16" \ | ||
| 135 | "send 0 user 17" \ | ||
| 136 | "sleep 800" \ | ||
| 137 | "jobs" \ | ||
| 138 | "poweroff" | ||
| 139 | |||
| 140 | echo "==> 5/5 wait for shutdown + verify" | ||
| 141 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 142 | |||
| 143 | grep -aq "msgrecv: task 2 listening" "$LOG" || fail "msgrecv did not spawn" | ||
| 144 | grep -aq "msgrecv\[2\]: User(42)" "$LOG" || fail "user message missing" | ||
| 145 | grep -aq "msgrecv\[2\]: Timer" "$LOG" || fail "timer message missing" | ||
| 146 | grep -aq "msgrecv\[2\]: Key(Enter)" "$LOG" || fail "key enter missing" | ||
| 147 | grep -aq "msgrecv\[2\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing" | ||
| 148 | grep -aq "msgrecv\[2\]: Key(F3)" "$LOG" || fail "key f3 missing" | ||
| 149 | grep -aq "msgrecv\[2\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing" | ||
| 150 | # Real IRQ12 events routed through the queue (button held across the move). | ||
| 151 | grep -aq "msgrecv\[2\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue" | ||
| 152 | grep -aq "msgrecv\[2\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing" | ||
| 153 | grep -aq "queue full" "$LOG" || fail "17th send did not report queue full" | ||
| 154 | grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)" | ||
| 155 | |||
| 156 | echo "" | ||
| 157 | echo "PASS — message queues OK (all types, ordering, real-input routing, queue-full)" | ||
| 158 | echo " sent: user · timer · key(enter,ctrl-c,f3) · mouse · 16+1 queue-full" | ||
| 159 | echo " routed: mouse_move 20,7 → queue → msgrecv[2]" | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 4640697..c18d319 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -703,6 +703,7 @@ write `{} <path> TIL <marker>`", | |||
| 703 | "date" => cmd_date(&parsed), | 703 | "date" => cmd_date(&parsed), |
| 704 | "pci" => cmd_pci(&parsed), | 704 | "pci" => cmd_pci(&parsed), |
| 705 | "rescan" => cmd_rescan(&parsed), | 705 | "rescan" => cmd_rescan(&parsed), |
| 706 | "mouse" => cmd_mouse(&parsed), | ||
| 706 | "ls" => cmd_ls(&parsed), | 707 | "ls" => cmd_ls(&parsed), |
| 707 | "cat" => cmd_cat(&parsed), | 708 | "cat" => cmd_cat(&parsed), |
| 708 | "klog" => cmd_klog(&parsed), | 709 | "klog" => cmd_klog(&parsed), |
| @@ -713,6 +714,8 @@ write `{} <path> TIL <marker>`", | |||
| 713 | "edit" => cmd_edit(&parsed), | 714 | "edit" => cmd_edit(&parsed), |
| 714 | "bg" => cmd_bg(&parsed), | 715 | "bg" => cmd_bg(&parsed), |
| 715 | "jobs" => cmd_jobs(&parsed), | 716 | "jobs" => cmd_jobs(&parsed), |
| 717 | "msgrecv" => cmd_msgrecv(&parsed), | ||
| 718 | "send" => cmd_send(&parsed), | ||
| 716 | "sleep" => cmd_sleep(&parsed), | 719 | "sleep" => cmd_sleep(&parsed), |
| 717 | "kill" => cmd_kill(&parsed), | 720 | "kill" => cmd_kill(&parsed), |
| 718 | "demo" => cmd_demo(&parsed), | 721 | "demo" => cmd_demo(&parsed), |
| @@ -738,8 +741,16 @@ fn cmd_help(_p: &Parsed) { | |||
| 738 | date — show current date/time (RTC) | 741 | date — show current date/time (RTC) |
| 739 | pci — list PCI devices | 742 | pci — list PCI devices |
| 740 | rescan — re-scan for hotplugged/removed disks (also runs automatically) | 743 | rescan — re-scan for hotplugged/removed disks (also runs automatically) |
| 744 | mouse — live mouse events (dx/dy/buttons); ^C stops | ||
| 745 | mouse <id> — post decoded mouse events to task <id>'s queue (input routing) | ||
| 741 | bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow) | 746 | bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow) |
| 742 | jobs — list tasks (id, name, state, ticks) | 747 | msgrecv — spawn a task that blocks on its message queue and logs every |
| 748 | message to serial (ids from `jobs`) | ||
| 749 | send <id> <type> [args] — post a message to a task's queue: | ||
| 750 | user <n> · timer · mouse <dx> <dy> <btn> (1=L 2=R 4=M) | ||
| 751 | key <name> (enter esc tab space bksp up down left right home | ||
| 752 | end pgup pgdn del ctrl-c f1..f12, or a single char) | ||
| 753 | jobs — list tasks (id, name, state, ticks, q=pending messages) | ||
| 743 | sleep <ms> — block the shell for ms; background tasks keep running | 754 | sleep <ms> — block the shell for ms; background tasks keep running |
| 744 | kill <id> — stop a background task (id from jobs) | 755 | kill <id> — stop a background task (id from jobs) |
| 745 | demo — spawn spinner + ticker + counter | 756 | demo — spawn spinner + ticker + counter |
| @@ -1517,7 +1528,12 @@ fn rescan_all() -> (usize, usize, usize, usize, usize, usize) { | |||
| 1517 | fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool { | 1528 | fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool { |
| 1518 | term_println(&alloc::format!( | 1529 | term_println(&alloc::format!( |
| 1519 | "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed", | 1530 | "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed", |
| 1520 | da, dr, ma, mu, na, nr | 1531 | da, |
| 1532 | dr, | ||
| 1533 | ma, | ||
| 1534 | mu, | ||
| 1535 | na, | ||
| 1536 | nr | ||
| 1521 | )); | 1537 | )); |
| 1522 | da + dr + ma + mu + na + nr > 0 | 1538 | da + dr + ma + mu + na + nr > 0 |
| 1523 | } | 1539 | } |
| @@ -1530,6 +1546,62 @@ fn cmd_rescan(_p: &Parsed) { | |||
| 1530 | } | 1546 | } |
| 1531 | } | 1547 | } |
| 1532 | 1548 | ||
| 1549 | /// Live mouse event stream: print each decoded packet (dx/dy/buttons) as | ||
| 1550 | /// it arrives until ^C. With an optional task id, events are **posted to | ||
| 1551 | /// that task's message queue** instead of printed — the input-routing | ||
| 1552 | /// path the window manager will use (`mouse <id>` + `msgrecv` proves the | ||
| 1553 | /// whole chain: IRQ12 → ring → decoder → queue → blocked task). Polls | ||
| 1554 | /// the PS/2 ring every 10 ms (via `sleep_ms`, which only blocks the | ||
| 1555 | /// shell task) so background tasks keep running. | ||
| 1556 | fn cmd_mouse(p: &Parsed) { | ||
| 1557 | let target: Option<usize> = p.args.first().and_then(|s| s.parse::<usize>().ok()); | ||
| 1558 | if let Some(id) = target { | ||
| 1559 | if !rsnix::sched::is_alive(id) { | ||
| 1560 | term_err(&alloc::format!("mouse: no such task {}", id)); | ||
| 1561 | return; | ||
| 1562 | } | ||
| 1563 | term_println(&alloc::format!( | ||
| 1564 | "mouse: posting events to task {} — ^C to stop", | ||
| 1565 | id | ||
| 1566 | )); | ||
| 1567 | } else { | ||
| 1568 | if !rsnix::ps2::mouse_present() { | ||
| 1569 | term_err("mouse: no PS/2 mouse at boot — cat /dev/mouse still works"); | ||
| 1570 | return; | ||
| 1571 | } | ||
| 1572 | term_println("mouse: streaming events — ^C to stop"); | ||
| 1573 | } | ||
| 1574 | loop { | ||
| 1575 | if tty::interrupt_pressed() { | ||
| 1576 | term_println("mouse: stopped"); | ||
| 1577 | return; | ||
| 1578 | } | ||
| 1579 | while let Some(ev) = rsnix::ps2::try_poll_mouse() { | ||
| 1580 | match target { | ||
| 1581 | Some(id) => { | ||
| 1582 | if let Err(e) = rsnix::sched::send(id, rsnix::sched::TaskMsg::Mouse(ev)) { | ||
| 1583 | term_err(&alloc::format!("mouse: {}", e)); | ||
| 1584 | return; | ||
| 1585 | } | ||
| 1586 | } | ||
| 1587 | None => { | ||
| 1588 | let mut b = String::new(); | ||
| 1589 | b.push(if ev.left { 'L' } else { '-' }); | ||
| 1590 | b.push(if ev.right { 'R' } else { '-' }); | ||
| 1591 | b.push(if ev.middle { 'M' } else { '-' }); | ||
| 1592 | term_println(&alloc::format!( | ||
| 1593 | "dx={:+} dy={:+} buttons={}", | ||
| 1594 | ev.dx, | ||
| 1595 | ev.dy, | ||
| 1596 | b | ||
| 1597 | )); | ||
| 1598 | } | ||
| 1599 | } | ||
| 1600 | } | ||
| 1601 | rsnix::sched::sleep_ms(10); | ||
| 1602 | } | ||
| 1603 | } | ||
| 1604 | |||
| 1533 | // --------------------------------------------------------------------------- | 1605 | // --------------------------------------------------------------------------- |
| 1534 | // Multitasking: bg / jobs / sleep / kill / demo | 1606 | // Multitasking: bg / jobs / sleep / kill / demo |
| 1535 | // | 1607 | // |
| @@ -1577,6 +1649,141 @@ fn cmd_jobs(p: &Parsed) { | |||
| 1577 | pipe.end(); | 1649 | pipe.end(); |
| 1578 | } | 1650 | } |
| 1579 | 1651 | ||
| 1652 | /// Demo of the scheduler message queues: spawn a task that blocks on | ||
| 1653 | /// `recv_blocking` and logs every message it receives to serial. The | ||
| 1654 | /// shell then posts to it with `send <id> …` (id from `jobs`). | ||
| 1655 | fn cmd_msgrecv(_p: &Parsed) { | ||
| 1656 | match rsnix::sched::spawn(task_msgrecv, "msgrecv") { | ||
| 1657 | Some(id) => term_println(&alloc::format!( | ||
| 1658 | "msgrecv: task {} listening — post with: send {} <type> …", | ||
| 1659 | id, | ||
| 1660 | id | ||
| 1661 | )), | ||
| 1662 | None => term_err("msgrecv: task table full"), | ||
| 1663 | } | ||
| 1664 | } | ||
| 1665 | |||
| 1666 | /// The recv demo body: block forever, log each message. Headless by | ||
| 1667 | /// design (serial only — it never touches tty/vt/fs, per ADR 0003). | ||
| 1668 | fn task_msgrecv() { | ||
| 1669 | loop { | ||
| 1670 | let msg = rsnix::sched::recv_blocking(); | ||
| 1671 | rsnix::serial_println!( | ||
| 1672 | "msgrecv[{}]: {}", | ||
| 1673 | rsnix::sched::current(), | ||
| 1674 | rsnix::sched::msg_describe(&msg) | ||
| 1675 | ); | ||
| 1676 | } | ||
| 1677 | } | ||
| 1678 | |||
| 1679 | /// `send <id> <type> [args]` — post a TaskMsg to a task's queue. This | ||
| 1680 | /// is the shell-side knob for the primitive the window manager will use | ||
| 1681 | /// to route input; types: user <n>, timer, mouse <dx> <dy> <btn> | ||
| 1682 | /// (1=L 2=R 4=M), key <name> (enter esc tab space bksp up down left | ||
| 1683 | /// right home end pgup pgdn del ctrl-c f1..f12, or a single char). | ||
| 1684 | fn cmd_send(p: &Parsed) { | ||
| 1685 | let mut args = p.args.iter(); | ||
| 1686 | let id: usize = match args.next().and_then(|s| s.parse().ok()) { | ||
| 1687 | Some(v) => v, | ||
| 1688 | None => { | ||
| 1689 | term_err("send: needs a task id — jobs lists them, msgrecv spawns a listener"); | ||
| 1690 | return; | ||
| 1691 | } | ||
| 1692 | }; | ||
| 1693 | let kind = match args.next() { | ||
| 1694 | Some(k) => k.as_str(), | ||
| 1695 | None => { | ||
| 1696 | term_err("send: needs a type — user <n> | timer | mouse <dx> <dy> <btn> | key <name>"); | ||
| 1697 | return; | ||
| 1698 | } | ||
| 1699 | }; | ||
| 1700 | let msg = match kind { | ||
| 1701 | "user" => { | ||
| 1702 | let Some(n) = args.next().and_then(|s| s.parse().ok()) else { | ||
| 1703 | term_err("send: user needs a number"); | ||
| 1704 | return; | ||
| 1705 | }; | ||
| 1706 | rsnix::sched::TaskMsg::User(n) | ||
| 1707 | } | ||
| 1708 | "timer" => rsnix::sched::TaskMsg::Timer, | ||
| 1709 | "mouse" => { | ||
| 1710 | let dx: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 1711 | let dy: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 1712 | let btn: u8 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 1713 | rsnix::sched::TaskMsg::Mouse(rsnix::ps2::MouseEvent { | ||
| 1714 | dx, | ||
| 1715 | dy, | ||
| 1716 | left: btn & 1 != 0, | ||
| 1717 | right: btn & 2 != 0, | ||
| 1718 | middle: btn & 4 != 0, | ||
| 1719 | }) | ||
| 1720 | } | ||
| 1721 | "key" => match args.next() { | ||
| 1722 | Some(k) => match key_from_name(k.as_str()) { | ||
| 1723 | Some(ev) => rsnix::sched::TaskMsg::Key(ev), | ||
| 1724 | None => { | ||
| 1725 | term_err("send: unknown key name"); | ||
| 1726 | return; | ||
| 1727 | } | ||
| 1728 | }, | ||
| 1729 | None => { | ||
| 1730 | term_err("send: key needs a name"); | ||
| 1731 | return; | ||
| 1732 | } | ||
| 1733 | }, | ||
| 1734 | _ => { | ||
| 1735 | term_err("send: unknown type — user <n> | timer | mouse <dx> <dy> <btn> | key <name>"); | ||
| 1736 | return; | ||
| 1737 | } | ||
| 1738 | }; | ||
| 1739 | match rsnix::sched::send(id, msg) { | ||
| 1740 | Ok(()) => {} | ||
| 1741 | Err(e) => term_err(&alloc::format!("send: {}", e)), | ||
| 1742 | } | ||
| 1743 | } | ||
| 1744 | |||
| 1745 | /// Map a small key-name vocabulary to a KeyEvent for `send key …`. | ||
| 1746 | fn key_from_name(name: &str) -> Option<rsnix::ps2::KeyEvent> { | ||
| 1747 | use rsnix::ps2::KeyEvent; | ||
| 1748 | Some(match name { | ||
| 1749 | "enter" => KeyEvent::Enter, | ||
| 1750 | "esc" | "escape" => KeyEvent::Escape, | ||
| 1751 | "tab" => KeyEvent::Tab, | ||
| 1752 | "space" => KeyEvent::Char(' '), | ||
| 1753 | "bksp" | "backspace" => KeyEvent::Backspace, | ||
| 1754 | "up" => KeyEvent::ArrowUp, | ||
| 1755 | "down" => KeyEvent::ArrowDown, | ||
| 1756 | "left" => KeyEvent::ArrowLeft, | ||
| 1757 | "right" => KeyEvent::ArrowRight, | ||
| 1758 | "home" => KeyEvent::Home, | ||
| 1759 | "end" => KeyEvent::End, | ||
| 1760 | "pgup" => KeyEvent::PageUp, | ||
| 1761 | "pgdn" => KeyEvent::PageDown, | ||
| 1762 | "del" | "delete" => KeyEvent::Delete, | ||
| 1763 | "ctrl-c" => KeyEvent::Ctrl('c'), | ||
| 1764 | "ctrl-d" => KeyEvent::Ctrl('d'), | ||
| 1765 | "ctrl-s" => KeyEvent::Ctrl('s'), | ||
| 1766 | "ctrl-x" => KeyEvent::Ctrl('x'), | ||
| 1767 | _ => { | ||
| 1768 | if let Some(n) = name.strip_prefix('f') { | ||
| 1769 | if let Ok(n) = n.parse::<u8>() { | ||
| 1770 | if (1..=12).contains(&n) { | ||
| 1771 | KeyEvent::F(n) | ||
| 1772 | } else { | ||
| 1773 | return None; | ||
| 1774 | } | ||
| 1775 | } else { | ||
| 1776 | return None; | ||
| 1777 | } | ||
| 1778 | } else if name.chars().count() == 1 { | ||
| 1779 | KeyEvent::Char(name.chars().next().unwrap()) | ||
| 1780 | } else { | ||
| 1781 | return None; | ||
| 1782 | } | ||
| 1783 | } | ||
| 1784 | }) | ||
| 1785 | } | ||
| 1786 | |||
| 1580 | fn cmd_sleep(p: &Parsed) { | 1787 | fn cmd_sleep(p: &Parsed) { |
| 1581 | let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { | 1788 | let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { |
| 1582 | Some(m) if m > 0 => m, | 1789 | Some(m) if m > 0 => m, |
