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 /rsnix/src | |
| 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.
Diffstat (limited to '')
| -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 |
4 files changed, 471 insertions, 17 deletions
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); |
