From 819778f292ee4e1294603b98662e083e49e23463 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 19:52:05 +0200 Subject: rsnix v0.5.0: graphics layer + cursor compositor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first visible GUI surface: real rect primitives, a single shadow→display push path, and an on-screen mouse cursor that can never race the text surface. gop: - fill_rect + push_rect(x,y,w,h): every raster op rasterises into the RAM shadow, then push_rect copies the rect to the display (write-only, still never read) and composites the cursor on top. blit_glyph and scroll shifts now route through it. - cursor overlay: 12x16 pointer (const-fn built from column lists) drawn white with a black offset outline; position in one AtomicU64 (x:high 32, y:low 32) — unpack masks the low word (a bare 'as usize' cast is a no-op on 64-bit). The cursor never enters the shadow, so erasing is just a shadow push of the old rect. - cursor_redraw after scroll shifts and once at spawn (the vt diff never re-blits idle cells). cursor task (task 2, spawned at boot when a mouse is present): - owns the PS/2 ring; polls every 10 ms, moves the cursor (clamped), tracks buttons, and forwards every event to a task's queue when 'mouse ' armed it (the window-manager input path). - /dev/mouse is now a position device (x/y/buttons snapshot); 'mouse' watches the cursor live (prints on change); 'cursor' prints one-shot state; 'gfx' draws a status-bar demo; 'gfx check' is an in-kernel pixel self-test (shadow readback, display readback, cursor overlay purity) — PASS/FAIL per stage. Tests (headless QEMU + monitor injection): - make test-gfx (new): all four compositor checks PASS. - make test-mouse (rewritten): cursor tracks mouse_move deltas, buttons ride along, /dev/mouse snapshot, live watch, real-event queue routing. - make test-msg: task numbering shifted (cursor is task 2; msgrecv is task 3) — updated, still PASS. - make test-disk regression re-passes. 0 build warnings. --- rsnix/src/cursor.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 rsnix/src/cursor.rs (limited to 'rsnix/src/cursor.rs') diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs new file mode 100644 index 0000000..18c9a1d --- /dev/null +++ b/rsnix/src/cursor.rs @@ -0,0 +1,71 @@ +// --------------------------------------------------------------------------- +// cursor — the mouse cursor task. +// +// One background task owns the PS/2 mouse: it polls the packet ring every +// 10 ms, moves the on-screen cursor (gop's display overlay), tracks the +// button state, and optionally *forwards* every event to another task's +// message queue (`mouse ` sets the target) — the input-routing half +// of the future window manager. +// +// The cursor itself never touches the text surface: `gop::push_rect` +// composites it on top of every display write, so text scrolling and +// cursor movement cannot corrupt each other (see gop.rs). +// --------------------------------------------------------------------------- +use core::sync::atomic::{AtomicUsize, Ordering}; + +use crate::ps2::MouseEvent; +use crate::sched::TaskMsg; + +/// Forward target task id, or `usize::MAX` = none. +static FORWARD_TARGET: AtomicUsize = AtomicUsize::new(usize::MAX); + +/// Spawn the cursor task. Call after `sched::init`; only makes sense +/// when the PS/2 mouse came up at boot. The cursor starts centred. +pub fn spawn() -> Option { + let (w, h) = crate::gop::pixel_dims(); + crate::gop::cursor_set(w / 2 - 6, h / 2 - 8); + // Draw it immediately: the vt diff only re-blits changed cells, so an + // idle centre cell would never push (and thus never composite) it. + crate::gop::cursor_redraw(); + let id = crate::sched::spawn(task_main, "cursor")?; + crate::serial_println!( + "cursor: task {} — mouse poll + display overlay (forward target: none)", + id + ); + Some(id) +} + +/// Route decoded mouse events to a task's queue (the shell's `mouse +/// `). `None` stops forwarding. +pub fn set_forward(target: Option) { + FORWARD_TARGET.store(target.unwrap_or(usize::MAX), Ordering::Relaxed); +} + +fn forward_target() -> Option { + match FORWARD_TARGET.load(Ordering::Relaxed) { + usize::MAX => None, + id => Some(id), + } +} + +fn task_main() { + loop { + while let Some(ev) = crate::ps2::try_poll_mouse() { + crate::gop::cursor_move(ev.dx, ev.dy); + crate::gop::cursor_set_buttons(btn_bits(&ev)); + if let Some(id) = forward_target() + && crate::sched::send(id, TaskMsg::Mouse(ev)).is_err() + { + // The listener died mid-stream: stop routing (the + // cursor keeps tracking) rather than spam errors. + set_forward(None); + crate::serial_println!("cursor: forward target task {} gone — stopped routing", id); + } + } + crate::sched::sleep_ms(10); + } +} + +fn btn_bits(ev: &MouseEvent) -> u8 { + (ev.left as u8) | ((ev.right as u8) << 1) | ((ev.middle as u8) << 2) +} -- cgit v1.2.3