summaryrefslogtreecommitdiffstats
path: root/rsnix
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 19:34:40 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 19:34:40 +0200
commit9b9758e5ee2564b04685eadf2b9ce23dd1060b86 (patch)
tree57fce9e715510ec70df70dc399b4440f3480fd77 /rsnix
parentfix pi-lens warnings: unwraps, shell SC2046, dup code (fmtbuf/scripts/hexdump... (diff)
downloadrs-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.rs38
-rw-r--r--rsnix/src/interrupts.rs68
-rw-r--r--rsnix/src/ps2.rs183
-rw-r--r--rsnix/src/sched.rs199
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
51enum NodeData { 52enum 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.
720fn 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
710pub fn self_test() -> bool { 748pub 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.
160struct KeyBuf { 163struct 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
166unsafe impl Sync for KeyBuf {} 169unsafe impl Sync for ByteRing {}
167 170
168impl KeyBuf { 171impl 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
208static KEYBUF: KeyBuf = KeyBuf::new(); 211static KEYBUF: ByteRing = ByteRing::new();
212static 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`.
412pub 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;
16const MOD_CAPS: u8 = 0x40; 16const MOD_CAPS: u8 = 0x40;
17 17
18/// A decoded key event. 18/// A decoded key event.
19#[derive(Clone, Copy, Debug)]
19pub enum KeyEvent { 20pub 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).
410const CMD_AUX_ENABLE: u8 = 0xA8; // enable the auxiliary (second) port
411const CMD_READ_CFG: u8 = 0x20; // read Configuration Byte → port 0x60
412const CMD_WRITE_CFG: u8 = 0x60; // write Configuration Byte ← port 0x60
413const 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.
418const CFG_AUX_IRQ: u8 = 0x02; // bit 1 — IRQ12 (mouse)
419const CFG_AUX_CLOCK: u8 = 0x20; // bit 5 — auxiliary port clock enable
420
421// Mouse device commands (sent via CMD_SEND_TO_AUX).
422const AUX_SET_DEFAULTS: u8 = 0xF6; // 100 Hz sample, 4 counts/mm, 1:1
423const AUX_ENABLE: u8 = 0xF4; // start data reporting (stream mode)
424const 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)]
432pub 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.
442struct MouseState {
443 phase: u8, // 0 = awaiting start byte, 1 = await X, 2 = await Y
444 pkt: [u8; 3],
445}
446
447static mut MOUSE: MouseState = MouseState {
448 phase: 0,
449 pkt: [0; 3],
450};
451static 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.
454pub 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.
465pub 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.
505fn 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.
531pub 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.
544fn 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.
578fn 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
42const NAME_MAX: usize = 24; 42const NAME_MAX: usize = 24;
43const PANIC_MAX: usize = 64; 43const 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.
52const 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)]
59pub 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).
67pub 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)]
46pub enum TaskState { 103pub 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
70const TASK_NONE: Task = Task { 134const 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
83static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS]; 151static 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.
412pub 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.
435pub 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.
446pub 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).
461unsafe 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).
475unsafe 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`).
489pub 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.
498pub 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);