From 7bd3459f4f8bae69509202e5aa0dbd04a42c656f Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 17 Aug 2026 20:28:48 +0200 Subject: vt → per-instance Terminal rendering into a pixel buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor vt.rs into a per-instance Terminal struct (owned cell/rendered grids, cursor, ANSI parser, scroll state), rendering glyphs into a caller-owned pixel buffer with dirty-region blitting - Add gop buffer primitives (buffer_glyph_px, buffer_shift_up) and keep the legacy vt:: API as thin wrappers around the home terminal - Remove the now-obsolete scroll hook and shadow scroll path - Home terminal renders into its own buffer; window_id stays None until the WM integration lands --- rsnix/src/vt.rs | 1207 ++++++++++++++++++++++++------------------------------- 1 file changed, 524 insertions(+), 683 deletions(-) (limited to 'rsnix/src/vt.rs') diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 4ef568d..90264f6 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs @@ -1,23 +1,16 @@ -//! Minimal ANSI terminal emulator over the GOP framebuffer. +//! Minimal ANSI terminal emulator, now per-instance. //! -//! `vt` owns the text screen: a cell grid (char + fg/bg colour), a logical -//! cursor, scrolling, and a small CSI parser. It renders to the framebuffer -//! through `gop`, blitting only cells whose content changed since the last -//! flush. +//! A `Terminal` owns a cell grid (char + fg/bg colour), a logical cursor, +//! scrolling, and a small CSI parser, and renders into a caller-owned pixel +//! buffer (the terminal window's framebuffer). Only cells whose content +//! changed since the last flush are re-blitted. //! //! This is the kernel's "terminal". Plain text (shell output, typed echo) //! flows through here in *ground* state; full-screen apps (rsdosedit) write //! ANSI escape sequences that the CSI parser interprets — cursor addressing, //! erase, 16-colour SGR, cursor show/hide. -//! -//! Known perf note: scrolling shifts the whole framebuffer one row up (a -//! ~3 MB copy at 1024×768), which dominates under QEMU without KVM. A ring -//! buffer / deferred-pan approach is the future optimisation; the cell grid -//! itself never re-rasterises on scroll — only the exposed bottom row is -//! blitted. use crate::gop; -use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; @@ -54,28 +47,6 @@ const BLANK: Cell = Cell { bg: 0, }; -// ---- screen state ---------------------------------------------------------- - -static mut CELLS: *mut Vec = core::ptr::null_mut(); -/// What is currently on the physical framebuffer (used to diff blits). -static mut RENDERED: *mut Vec = core::ptr::null_mut(); -static mut ROWS: usize = 0; -static mut COLS: usize = 0; - -// ---- cursor ---------------------------------------------------------------- - -static mut CURSOR_R: usize = 0; -static mut CURSOR_C: usize = 0; -static mut LAST_CURSOR: (usize, usize) = (0, 0); -static mut CURSOR_VISIBLE: bool = true; - -// ---- SGR state ------------------------------------------------------------- - -static mut SGR_FG: u8 = 7; -static mut SGR_BG: u8 = 0; - -// ---- ANSI parser state ----------------------------------------------------- - #[derive(Clone, Copy, PartialEq, Eq)] enum Parser { Ground, @@ -83,792 +54,662 @@ enum Parser { Csi, } -static mut PARSER: Parser = Parser::Ground; -static mut PRIVATE: bool = false; -static mut PARAMS: [usize; 4] = [0; 4]; -static mut NPARAMS: usize = 0; -static mut CUR: usize = 0; - -// --------------------------------------------------------------------------- -// init / dimensions -// --------------------------------------------------------------------------- +/// ANSI SGR colour index (0–7) → VGA palette index. +const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; -/// Allocate the cell grid. Call once after `gop::init()`. -pub fn init() { - let (cols, rows) = gop::dims(); - if rows == 0 || cols == 0 { - // No usable framebuffer geometry: GOP is missing entirely or the - // active mode reports 0x0. Without a display there is nothing - // the shell can do, so stop with a visible explanation instead - // of building an empty grid and panicking at the first cell - // write (a blank screen on real hardware, which has no serial - // console). - let lines = [ - "FATAL: no framebuffer geometry", - "GOP missing or active mode is 0x0", - "see the 'gop:' boot marker / serial log", - ]; - crate::gop::dump_lines(&lines); - crate::serial_println!("FATAL: no framebuffer geometry (GOP missing or 0x0 mode)"); - loop { - unsafe { - core::arch::asm!("cli; hlt"); - } +const SCROLL_BATCH: usize = 16; +const TICK_LIMIT: u64 = 4; + +/// One text terminal: a cell grid + cursor + ANSI parser, rendered into a +/// pixel buffer (`w`×`h`, row-major `u32`). +pub struct Terminal { + cells: Vec, + rendered: Vec, + rows: usize, + cols: usize, + cursor_r: usize, + cursor_c: usize, + last_cursor: (usize, usize), + cursor_visible: bool, + sgr_fg: u8, + sgr_bg: u8, + parser: Parser, + private: bool, + params: [usize; 4], + nparams: usize, + cur: usize, + pending_scrolls: usize, + last_sync_tick: u64, + buffer: Vec, + w: usize, + h: usize, + window_id: Option, +} + +fn union_rect( + d: &mut Option<(usize, usize, usize, usize)>, + x: usize, + y: usize, + w: usize, + h: usize, +) { + *d = match *d { + Some((dx, dy, dw, dh)) => { + let nx = dx.min(x); + let ny = dy.min(y); + let nx2 = (dx + dw).max(x + w); + let ny2 = (dy + dh).max(y + h); + Some((nx, ny, nx2 - nx, ny2 - ny)) + } + None => Some((x, y, w, h)), + }; +} + +impl Terminal { + /// Build a terminal with a fresh `w`×`h` pixel buffer. + pub fn new(w: usize, h: usize) -> Terminal { + let cols = w / 8; + let rows = h / 16; + Terminal { + cells: vec![BLANK; rows * cols], + rendered: vec![BLANK; rows * cols], + rows, + cols, + cursor_r: 1, + cursor_c: 0, + last_cursor: (1, 0), + cursor_visible: true, + sgr_fg: 7, + sgr_bg: 0, + parser: Parser::Ground, + private: false, + params: [0; 4], + nparams: 0, + cur: 0, + pending_scrolls: 0, + last_sync_tick: 0, + buffer: vec![0; w * h], + w, + h, + window_id: None, } } - unsafe { - ROWS = rows; - COLS = cols; - CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); - RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); - // Home below row 0: row 0 is the boot-marker status line, so the - // terminal (shell + klog screen echo) must never write to it. - CURSOR_R = 1; - CURSOR_C = 0; - LAST_CURSOR = (1, 0); - CURSOR_VISIBLE = true; - SGR_FG = 7; - SGR_BG = 0; - PARSER = Parser::Ground; - NPARAMS = 0; - PRIVATE = false; + + /// Attach this terminal to a WM window (so `flush` composites it). + pub fn set_window(&mut self, id: usize) { + self.window_id = Some(id); } -} -/// True once the terminal is initialised (grids live). The klog screen -/// echo checks this so serial lines printed before vt::init (very early -/// boot) are skipped instead of hitting a null grid. -pub fn ready() -> bool { - unsafe { ROWS != 0 && !CELLS.is_null() } -} + pub fn ready(&self) -> bool { + self.rows != 0 + } -/// (rows, cols) — matches the editor's expected (rows, cols) ordering. -pub fn size() -> (usize, usize) { - unsafe { (ROWS, COLS) } -} + pub fn size(&self) -> (usize, usize) { + (self.rows, self.cols) + } -/// Re-render the console's cells into the shadow for a pixel region, so the -/// window manager can restore the console (layer 0) beneath a window that -/// moved, closed, or was minimized. Does not push — the caller pushes once. -pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { - unsafe { - if CELLS.is_null() { - return; - } - let (cols, rows) = (COLS, ROWS); - let c0 = x / 8; - let c1 = (x + w).div_ceil(8).min(cols); - let r0 = y / 16; - let r1 = (y + h).div_ceil(16).min(rows); - let cells = &*CELLS; - for r in r0..r1 { - for c in c0..c1 { - let cell = cells[r * cols + c]; - let fg = PALETTE[cell.fg as usize]; - let bg = PALETTE[cell.bg as usize]; - crate::gop::shadow_glyph_px(c * 8, r * 16, cell.ch, fg, bg); - } - } + pub fn rows(&self) -> usize { + self.rows } -} -/// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) -/// from the live cells grid and the rendered grid, for the given text -/// row/col. Returns (raw_cell, raw_rendered). -pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { - unsafe { - if CELLS.is_null() || RENDERED.is_null() { + pub fn cols(&self) -> usize { + self.cols + } + + /// Raw (ch|fg<<16|bg<<24) values of a cell and its rendered mirror, for + /// the `heapdbg` diagnostic. + pub fn cell_raw(&self, r: usize, c: usize) -> (u32, u32) { + if r >= self.rows || c >= self.cols { return (0xFFFF_FFFF, 0xFFFF_FFFF); } - let raw = |v: &Vec| -> u32 { - if r < ROWS && c < COLS { - let cell = v[r * COLS + c]; - cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) - } else { - 0xFFFF_FFFE - } + let raw = |cell: &Cell| -> u32 { + cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) }; - ( - raw(&*(CELLS as *const Vec)), - raw(&*(RENDERED as *const Vec)), - ) + let i = self.idx(r, c); + (raw(&self.cells[i]), raw(&self.rendered[i])) } -} -/// Shell-entry self-check: dump the vt grid's heap structs so a real- -/// hardware boot shows whether the cells VecData / data arrays are sane -/// at shell entry (the OptiPlex's first banner write #GPs when the -/// cells buffer holds firmware 0xAA fill instead of real pointers). -pub fn debug_verify() { - unsafe { - let total = ROWS * COLS; - let msg = |m: &str| { - crate::serial_println!("vt: {}", m); - }; - if CELLS.is_null() || RENDERED.is_null() { - msg("verify: cells/renderd NULL — vt::init did not run"); + pub fn set_cursor_visible(&mut self, v: bool) { + self.sync_scroll(); + self.cursor_visible = v; + self.flush(); + } + + /// Push deferred scrolls: shift the pixel buffer, mirror the rendered + /// grid, and re-blit the exposed rows. + pub fn sync_scroll(&mut self) { + let n = self.pending_scrolls; + if n == 0 { return; } - let c = &*(CELLS as *const Vec); - let r = &*(RENDERED as *const Vec); - msg(&alloc::format!( - "verify: cells ptr={:#x} cap={:#x} len={:#x} rendered ptr={:#x} cap={:#x} len={:#x} want={:#x}", - c.as_ptr() as usize, - c.capacity(), - c.len(), - r.as_ptr() as usize, - r.capacity(), - r.len(), - total - )); - // The cells array must cover rows*cols and be inside the heap. - let heap_ok = crate::memory::heap_range() - .map(|(hs, he)| { - let p = c.as_ptr() as usize; - let bytes = c.capacity().saturating_mul(core::mem::size_of::()); - p >= hs && p + bytes <= he - }) - .unwrap_or(false); - // Content check: at shell entry nothing has been printed yet, so - // every cell must still hold the grid's init fill. NOTE: that - // fill is BLANK (space + fg 7 + bg 0 = raw 0x70020), NOT zero — - // `vec![BLANK; n]` fills the grid with BLANK, and all boot - // output to this point is serial-only (boot markers are a - // direct-FB blit, never the vt grid). The v0.5.7 check - // compared against 0 and therefore tripped on every healthy - // boot (the "0x70020 at grid(0,0)" mystery — the grid's own - // init value, with the probe loop's early `break` leaving the - // other three probe slots as zero-initialized). A stale cells - // data pointer that happens to be in-heap still passes the - // range checks (len/cap look fine) but its "cells" read back as - // the underlying memory's leftover fill. NOTE: the OptiPlex - // dump value 0x00AAAAAA is the VGA GRAY text colour - // (rgb(170,170,170) = PALETTE[7] = fg 7), i.e. rendered shadow - // pixels — the memory read as heap held display text, not a - // firmware scrub. BLANK (0x70020) vs 0x00AAAAAA is cleanly - // distinguishable. - let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); - let mut content_ok = true; - let mut rcontent_ok = true; - let mut c_probes_raw: [u32; 4] = [0; 4]; - let mut r_probes_raw: [u32; 4] = [0; 4]; - if heap_ok && c.len() == total && total > 0 { - // Sample the first cell, the last cell, and two interior - // cells — reading the whole grid here would be ~60 KiB of - // checks at every boot for no real benefit. All four - // probes are recorded unconditionally (no early break) so - // a serial log shows every value. - let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; - for (pi, &p) in probes.iter().enumerate() { - let cell = *c.as_ptr().add(p); - let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); - c_probes_raw[pi] = raw; - if raw != blank_raw { - content_ok = false; - } - } - // Same probe on the rendered grid: it is a *separate* - // allocation, so its content discriminates "only the cells - // VecData's ptr field is stale" (rendered ok) from "both - // grids' memory was clobbered / both VecData ptrs are - // stale" (rendered garbage too). - for (pi, &p) in probes.iter().enumerate() { - let cell = *r.as_ptr().add(p); - let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); - r_probes_raw[pi] = raw; - if raw != blank_raw { - rcontent_ok = false; - } - } + let rows = self.rows; + let cols = self.cols; + gop::buffer_shift_up(self.buffer.as_mut_ptr(), self.w, self.h, n * 16); + // Mirror the pushed content in RENDERED (it describes the buffer). + let total = rows * cols; + self.rendered.copy_within(n * cols..total, 0); + for i in (total - n * cols)..total { + self.rendered[i] = BLANK; } - // Full-grid summary (cheap: a few thousand reads, serial-only). - // Definitive health signal on QEMU: healthy = every cell is - // BLANK (nz == blanks == total); the probes above are just the - // tripwire, this catches a partial clobber that the four - // samples happen to miss. - let (mut c_nz, mut c_blank) = (0usize, 0usize); - let (mut r_nz, mut r_blank) = (0usize, 0usize); - if heap_ok && c.len() == total && r.len() == total && total > 0 { - for i in 0..total { - let cell = *c.as_ptr().add(i); - let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); - if raw != 0 { - c_nz += 1; - } - if raw == blank_raw { - c_blank += 1; - } - let cell = *r.as_ptr().add(i); - let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); - if raw != 0 { - r_nz += 1; + self.pending_scrolls = 0; + self.last_sync_tick = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); + self.flush(); + } + + /// Re-blit a region of this terminal's buffer into the shadow (used by + /// the window manager to restore the console layer beneath a window). + pub fn repaint_region(&self, x: usize, y: usize, w: usize, h: usize) { + let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; + gop::shadow_blit(x, y, w, h, src, self.w); + } + + /// Write raw bytes (ANSI-capable), decoding UTF-8 for drawable glyphs. + pub fn write_bytes(&mut self, b: &[u8]) { + let on = irq_off(); + let mut i = 0; + while i < b.len() { + let byte = b[i]; + if byte < 0x80 { + self.put_byte(byte); + i += 1; + continue; + } + match core::str::from_utf8(&b[i..]) { + Ok(s) => { + let c = s.chars().next().unwrap_or('?'); + self.write_char(c); + i += c.len_utf8(); } - if raw == blank_raw { - r_blank += 1; + Err(e) => { + let valid = e.valid_up_to(); + if valid > 0 { + let s = unsafe { core::str::from_utf8_unchecked(&b[i..i + valid]) }; + let c = s.chars().next().unwrap_or('?'); + self.write_char(c); + i += c.len_utf8(); + } else { + self.put_byte(b'?'); + i += 1; + } } } } - msg(&alloc::format!( - "verify: grids c nz={} blank={} r nz={} blank={} (of {})", - c_nz, - c_blank, - r_nz, - r_blank, - total - )); - if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { - msg("verify: FAILED — cells/rendered not sane at shell entry"); - msg(&alloc::format!( - "verify: probes c=[{:#x},{:#x},{:#x},{:#x}] r=[{:#x},{:#x},{:#x},{:#x}]", - c_probes_raw[0], - c_probes_raw[1], - c_probes_raw[2], - c_probes_raw[3], - r_probes_raw[0], - r_probes_raw[1], - r_probes_raw[2], - r_probes_raw[3] - )); - // NOTE: debug markers are serial-only now; the crash family - // was resolved (EBS-retry free-list rebuild) and the screen - // must stay clean for users. - crate::serial_println!( - "vt: c {:#x} cap={:#x} {} | r {:#x} {}", - c.as_ptr() as usize, - c.capacity(), - if content_ok { "ok" } else { "GARBAGE" }, - r.as_ptr() as usize, - if rcontent_ok { "ok" } else { "GARBAGE" } - ); - } else { - msg("verify: ok"); - crate::serial_println!( - "vt: c {:#x} cap={:#x} ok | r {:#x} ok", - c.as_ptr() as usize, - c.capacity(), - r.as_ptr() as usize - ); - } + self.flush(); + irq_restore(on); } -} -pub fn set_cursor_visible(v: bool) { - sync_scroll(); - unsafe { - CURSOR_VISIBLE = v; + /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). + pub fn write_str(&mut self, s: &str) { + let on = irq_off(); + for c in s.chars() { + self.write_char(c); + } + self.flush(); + irq_restore(on); } - flush(); -} -// --------------------------------------------------------------------------- -// cell access -// --------------------------------------------------------------------------- - -fn idx(r: usize, c: usize) -> usize { - r * unsafe { COLS } + c -} - -fn cells() -> &'static mut Vec { - unsafe { &mut *CELLS } -} - -fn rendered_cells() -> &'static mut Vec { - unsafe { &mut *RENDERED } -} + /// Write a single ASCII byte / char. + pub fn put_char(&mut self, b: u8) { + let on = irq_off(); + self.put_byte(b); + self.flush(); + irq_restore(on); + } -fn cell(r: usize, c: usize) -> &'static mut Cell { - &mut cells()[idx(r, c)] -} + /// Clear the whole screen, reset colours, home the cursor. + pub fn clear_all(&mut self) { + self.sgr_fg = 7; + self.sgr_bg = 0; + self.sync_scroll(); + self.erase_all(); + self.cursor_r = 0; + self.cursor_c = 0; + self.flush(); + } -fn params() -> &'static mut [usize; 4] { - unsafe { &mut *core::ptr::addr_of_mut!(PARAMS) } -} + // -- internals ---------------------------------------------------------- -fn put_cell(r: usize, c: usize, ch: u16) { - let cell = cell(r, c); - cell.ch = ch; - cell.fg = unsafe { SGR_FG }; - cell.bg = unsafe { SGR_BG }; -} + fn idx(&self, r: usize, c: usize) -> usize { + r * self.cols + c + } -// --------------------------------------------------------------------------- -// writing — the ANSI state machine -// --------------------------------------------------------------------------- + fn put_cell(&mut self, r: usize, c: usize, ch: u16) { + let i = self.idx(r, c); + self.cells[i].ch = ch; + self.cells[i].fg = self.sgr_fg; + self.cells[i].bg = self.sgr_bg; + } -/// Feed one byte through the terminal state machine. -fn put_byte(b: u8) { - unsafe { - match PARSER { + fn put_byte(&mut self, b: u8) { + match self.parser { Parser::Ground => match b { - 0x1b => PARSER = Parser::Escape, - 0x0d => CURSOR_C = 0, - 0x0a => newline(), + 0x1b => self.parser = Parser::Escape, + 0x0d => self.cursor_c = 0, + 0x0a => self.newline(), 0x08 => { - // Backspace: move cursor back and erase. - if CURSOR_C > 0 { - CURSOR_C -= 1; - } else if CURSOR_R > 0 { - CURSOR_R -= 1; - CURSOR_C = COLS - 1; + if self.cursor_c > 0 { + self.cursor_c -= 1; + } else if self.cursor_r > 0 { + self.cursor_r -= 1; + self.cursor_c = self.cols - 1; } else { return; } - let c = cell(CURSOR_R, CURSOR_C); - *c = BLANK; + let i = self.idx(self.cursor_r, self.cursor_c); + self.cells[i] = BLANK; } 0x09 => { - let n = 8 - (CURSOR_C % 8); - CURSOR_C = (CURSOR_C + n).min(COLS - 1); + let n = 8 - (self.cursor_c % 8); + self.cursor_c = (self.cursor_c + n).min(self.cols - 1); } - 0x00..=0x1f => {} // other control chars: ignored - _ => put_printable(b as u16), + 0x00..=0x1f => {} + _ => self.put_printable(b as u16), }, Parser::Escape => match b { b'[' => { - PRIVATE = false; - NPARAMS = 0; - CUR = 0; - PARSER = Parser::Csi; + self.private = false; + self.nparams = 0; + self.cur = 0; + self.parser = Parser::Csi; } - _ => PARSER = Parser::Ground, // unsupported escape — drop + _ => self.parser = Parser::Ground, }, Parser::Csi => match b { - b'?' => PRIVATE = true, + b'?' => self.private = true, b'0'..=b'9' => { - CUR = CUR.saturating_mul(10).saturating_add((b - b'0') as usize); + self.cur = self + .cur + .saturating_mul(10) + .saturating_add((b - b'0') as usize); } b';' => { - if NPARAMS < params().len() { - PARAMS[NPARAMS] = CUR; - NPARAMS += 1; + if self.nparams < self.params.len() { + self.params[self.nparams] = self.cur; + self.nparams += 1; } - CUR = 0; + self.cur = 0; } _ => { - if NPARAMS < params().len() { - PARAMS[NPARAMS] = CUR; - NPARAMS += 1; + if self.nparams < self.params.len() { + self.params[self.nparams] = self.cur; + self.nparams += 1; } - PARSER = Parser::Ground; - csi_final(b); + self.parser = Parser::Ground; + self.csi_final(b); } }, } } -} -fn param(i: usize, default: usize) -> usize { - unsafe { if i < NPARAMS { PARAMS[i] } else { default } } -} + fn param(&self, i: usize, default: usize) -> usize { + if i < self.nparams { + self.params[i] + } else { + default + } + } -/// Place a printable glyph at the cursor and advance (wrapping/scrolling). -/// `ch` is the character code: ASCII or a Unicode codepoint that the -/// rasteriser can draw (box-drawing / arrows are rendered procedurally). -fn put_printable(ch: u16) { - unsafe { - put_cell(CURSOR_R, CURSOR_C, ch); - CURSOR_C += 1; - if CURSOR_C >= COLS { - CURSOR_C = 0; - newline(); + fn put_printable(&mut self, ch: u16) { + self.put_cell(self.cursor_r, self.cursor_c, ch); + self.cursor_c += 1; + if self.cursor_c >= self.cols { + self.cursor_c = 0; + self.newline(); } } -} -/// Dispatch one character to the terminal: ASCII goes through the byte -/// state machine (control/escape handling), printable non-ASCII goes -/// straight to the glyph path (replaced by '?' when not drawable). -fn write_char(c: char) { - let u = c as u32; - if u < 0x80 { - put_byte(u as u8); - } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { - put_printable(c as u16); - } else { - put_byte(b'?'); + fn write_char(&mut self, c: char) { + let u = c as u32; + if u < 0x80 { + self.put_byte(u as u8); + } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { + self.put_printable(c as u16); + } else { + self.put_byte(b'?'); + } } -} -fn csi_final(b: u8) { - // A CSI sequence means a full-screen app is actively rendering — - // push any deferred scrolls so it draws over a current framebuffer - // (streaming shell output never emits CSI, so it stays batched). - if unsafe { PENDING_SCROLLS } > 0 { - sync_scroll(); - } - match b { - // Cursor position (1-based; 0 means 1) - b'H' | b'f' => { - let r = param(0, 1).max(1).saturating_sub(1); - let c = param(1, 1).max(1).saturating_sub(1); - unsafe { - CURSOR_R = r.min(ROWS - 1); - CURSOR_C = c.min(COLS - 1); - } + fn csi_final(&mut self, b: u8) { + if self.pending_scrolls > 0 { + self.sync_scroll(); } - // Cursor up / down / forward / back - b'A' => unsafe { CURSOR_R = CURSOR_R.saturating_sub(param(0, 1).max(1)) }, - b'B' => unsafe { CURSOR_R = (CURSOR_R + param(0, 1).max(1)).min(ROWS - 1) }, - b'C' => unsafe { CURSOR_C = (CURSOR_C + param(0, 1).max(1)).min(COLS - 1) }, - b'D' => unsafe { CURSOR_C = CURSOR_C.saturating_sub(param(0, 1).max(1)) }, - // Erase in display: 0 below, 1 above, 2 all - b'J' => match param(0, 0) { - 0 => { - erase_eol(); - unsafe { - for r in (CURSOR_R + 1)..ROWS { - erase_row(r); + match b { + b'H' | b'f' => { + let r = self.param(0, 1).max(1).saturating_sub(1); + let c = self.param(1, 1).max(1).saturating_sub(1); + self.cursor_r = r.min(self.rows - 1); + self.cursor_c = c.min(self.cols - 1); + } + b'A' => self.cursor_r = self.cursor_r.saturating_sub(self.param(0, 1).max(1)), + b'B' => self.cursor_r = (self.cursor_r + self.param(0, 1).max(1)).min(self.rows - 1), + b'C' => self.cursor_c = (self.cursor_c + self.param(0, 1).max(1)).min(self.cols - 1), + b'D' => self.cursor_c = self.cursor_c.saturating_sub(self.param(0, 1).max(1)), + b'J' => match self.param(0, 0) { + 0 => { + self.erase_eol(); + for r in (self.cursor_r + 1)..self.rows { + self.erase_row(r); } } - } - 1 => { - unsafe { - for r in 0..CURSOR_R { - erase_row(r); + 1 => { + for r in 0..self.cursor_r { + self.erase_row(r); } + self.erase_bol(); } - erase_bol(); - } - _ => erase_all(), - }, - // Erase in line: 0 to EOL, 1 from BOL, 2 whole line - b'K' => match param(0, 0) { - 1 => erase_bol(), - 2 => { - let r = unsafe { CURSOR_R }; - erase_row(r); - } - _ => erase_eol(), - }, - // SGR — colours / attributes - b'm' => { - let n = unsafe { NPARAMS }; - if n == 0 { - apply_sgr(0); - } else { - for i in 0..n { - apply_sgr(unsafe { PARAMS[i] }); + _ => self.erase_all(), + }, + b'K' => match self.param(0, 0) { + 0 => self.erase_eol(), + 1 => self.erase_bol(), + _ => self.erase_row(self.cursor_r), + }, + b'm' => { + let n = self.nparams; + if n == 0 { + self.apply_sgr(0); + } else { + for i in 0..n { + self.apply_sgr(self.params[i]); + } } } - } - // Private mode: 25 = cursor show/hide (h = show, l = hide) - b'l' | b'h' if unsafe { PRIVATE } => { - for i in 0..unsafe { NPARAMS } { - if unsafe { PARAMS[i] } == 25 { - unsafe { CURSOR_VISIBLE = b == b'h' }; + b'l' | b'h' if self.private => { + for i in 0..self.nparams { + if self.params[i] == 25 { + self.cursor_visible = b == b'h'; + } } } + _ => {} } - _ => {} // unknown / ignored } -} - -/// ANSI SGR colour index (0–7) → VGA palette index. The two orderings -/// differ: ANSI 1=red, 3=yellow, 4=blue, 6=cyan; VGA 1=blue, 3=cyan, -/// 4=red, 6=brown. Terminals translate ANSI→VGA. -const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; -fn apply_sgr(p: usize) { - unsafe { + fn apply_sgr(&mut self, p: usize) { match p { 0 => { - SGR_FG = 7; - SGR_BG = 0; + self.sgr_fg = 7; + self.sgr_bg = 0; } 1 => { - if SGR_FG < 8 { - SGR_FG += 8; // bold ≈ bright + if self.sgr_fg < 8 { + self.sgr_fg += 8; } } - 30..=37 => SGR_FG = ANSI_TO_VGA[p - 30], - 40..=47 => SGR_BG = ANSI_TO_VGA[p - 40], - 90..=97 => SGR_FG = ANSI_TO_VGA[p - 90] + 8, - 100..=107 => SGR_BG = ANSI_TO_VGA[p - 100] + 8, - 39 => SGR_FG = 7, - 49 => SGR_BG = 0, + 30..=37 => self.sgr_fg = ANSI_TO_VGA[p - 30], + 40..=47 => self.sgr_bg = ANSI_TO_VGA[p - 40], + 90..=97 => self.sgr_fg = ANSI_TO_VGA[p - 90] + 8, + 100..=107 => self.sgr_bg = ANSI_TO_VGA[p - 100] + 8, + 39 => self.sgr_fg = 7, + 49 => self.sgr_bg = 0, _ => {} } } -} -// --------------------------------------------------------------------------- -// erase / scroll helpers -// --------------------------------------------------------------------------- - -fn erase_eol() { - let r = unsafe { CURSOR_R }; - let c = unsafe { CURSOR_C }; - for col in c..unsafe { COLS } { - *cell(r, col) = BLANK; + fn erase_eol(&mut self) { + let r = self.cursor_r; + for col in self.cursor_c..self.cols { + let i = self.idx(r, col); + self.cells[i] = BLANK; + } } -} -fn erase_bol() { - let r = unsafe { CURSOR_R }; - let c = unsafe { CURSOR_C }; - for col in 0..=c { - *cell(r, col) = BLANK; + fn erase_bol(&mut self) { + let r = self.cursor_r; + for col in 0..=self.cursor_c { + let i = self.idx(r, col); + self.cells[i] = BLANK; + } } -} -fn erase_row(r: usize) { - for col in 0..unsafe { COLS } { - *cell(r, col) = BLANK; + fn erase_row(&mut self, r: usize) { + for col in 0..self.cols { + let i = self.idx(r, col); + self.cells[i] = BLANK; + } } -} -fn erase_all() { - for r in 0..unsafe { ROWS } { - erase_row(r); + fn erase_all(&mut self) { + for r in 0..self.rows { + self.erase_row(r); + } } -} -/// Advance cursor to the next line (col 0), scrolling if at the bottom. -fn newline() { - unsafe { - CURSOR_C = 0; - if CURSOR_R + 1 < ROWS { - CURSOR_R += 1; + fn newline(&mut self) { + self.cursor_c = 0; + if self.cursor_r + 1 < self.rows { + self.cursor_r += 1; } else { - scroll_up(); + self.scroll_up(); } } -} - -/// Deferred scrolls: the logical grid scrolls instantly (RAM), but the -/// framebuffer shift+push is batched. N pending scrolls cost one push, -/// so a burst of streaming output no longer writes the whole framebuffer -/// once per line. `flush` renders nothing while scrolls are pending; -/// `sync_scroll` pushes the whole batch and lets the normal diff re-blit -/// only the newly-exposed rows. -const SCROLL_BATCH: usize = 16; -static mut PENDING_SCROLLS: usize = 0; -static mut LAST_SYNC_TICK: u64 = 0; -/// Shift the logical grid up one row; defer the framebuffer shift. -fn scroll_up() { - let n = unsafe { ROWS * COLS }; - let cols = unsafe { COLS }; - cells().copy_within(cols..n, 0); - for c in 0..cols { - cells()[n - cols + c] = BLANK; - } - unsafe { - PENDING_SCROLLS += 1; - // Batch: sync when enough scrolls piled up, or when the stream - // pauses (~40 ms at the 100 Hz timer) so interactive output still - // appears promptly. - const TICK_LIMIT: u64 = 4; + fn scroll_up(&mut self) { + let n = self.rows * self.cols; + let cols = self.cols; + self.cells.copy_within(cols..n, 0); + for c in 0..cols { + self.cells[n - cols + c] = BLANK; + } + self.pending_scrolls += 1; let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); - if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { - sync_scroll(); + if self.pending_scrolls >= SCROLL_BATCH + || now.wrapping_sub(self.last_sync_tick) > TICK_LIMIT + { + self.sync_scroll(); } } -} -/// Push deferred scrolls to the display in one batch: shift the shadow -/// (and the rendered grid, which mirrors it) up by the pending count and -/// do a single write-only push, then let the normal flush diff re-blit -/// the newly-exposed rows and redraw the cursor. -pub fn sync_scroll() { - unsafe { - let n = PENDING_SCROLLS; - if n == 0 { + fn flush(&mut self) { + if self.pending_scrolls > 0 { return; } - let rows = ROWS; - let cols = COLS; - gop::shift_up_n(n); - // Mirror the pushed content in RENDERED (it describes the fb). - let total = rows * cols; - rendered_cells().copy_within(n * cols..total, 0); - for i in (total - n * cols)..total { - rendered_cells()[i] = BLANK; + let rows = self.rows; + let cols = self.cols; + let n = rows * cols; + let cursor_visible = self.cursor_visible; + let (cr, cc) = (self.cursor_r, self.cursor_c); + let mut dirty: Option<(usize, usize, usize, usize)> = None; + + // Diff cells vs rendered, re-blitting changed cells into the buffer. + for i in 0..n { + let row = i / cols; + let col = i % cols; + if cursor_visible && row == cr && col == cc { + continue; + } + let c = self.cells[i]; + let r = self.rendered[i]; + if c != r { + let fg = PALETTE[c.fg as usize]; + let bg = PALETTE[c.bg as usize]; + gop::buffer_glyph_px( + self.buffer.as_mut_ptr(), + self.w, + self.h, + col * 8, + row * 16, + c.ch, + fg, + bg, + ); + self.rendered[i] = c; + union_rect(&mut dirty, col * 8, row * 16, 8, 16); + } + } + + // Cursor: erase the previous position, draw the new one (inverse). + let (lr, lc) = self.last_cursor; + let moved = (lr, lc) != (cr, cc); + if (moved || !cursor_visible) && lr < rows && lc < cols { + let i = self.idx(lr, lc); + let c = self.cells[i]; + let fg = PALETTE[c.fg as usize]; + let bg = PALETTE[c.bg as usize]; + gop::buffer_glyph_px( + self.buffer.as_mut_ptr(), + self.w, + self.h, + lc * 8, + lr * 16, + c.ch, + fg, + bg, + ); + self.rendered[i] = c; + union_rect(&mut dirty, lc * 8, lr * 16, 8, 16); + } + if cursor_visible && cr < rows && cc < cols { + let i = self.idx(cr, cc); + let c = self.cells[i]; + let fg = PALETTE[c.bg as usize]; + let bg = PALETTE[c.fg as usize]; + gop::buffer_glyph_px( + self.buffer.as_mut_ptr(), + self.w, + self.h, + cc * 8, + cr * 16, + c.ch, + fg, + bg, + ); + self.rendered[i] = Cell { + ch: c.ch, + fg: c.bg, + bg: c.fg, + }; + union_rect(&mut dirty, cc * 8, cr * 16, 8, 16); + } + self.last_cursor = (cr, cc); + + // Display: composite the attached window, or push just the dirty + // region of the buffer (not the whole screen). + match self.window_id { + Some(id) => crate::wm::flush(id), + None => { + if let Some((x, y, w, h)) = dirty { + let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; + gop::shadow_blit(x, y, w, h, src, self.w); + gop::push_rect(x, y, w, h); + } + } } - PENDING_SCROLLS = 0; - LAST_SYNC_TICK = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); - flush(); - gop::call_scroll_hook(); } } // --------------------------------------------------------------------------- -// public writing API +// The home terminal + the legacy `vt::` API (thin wrappers). // --------------------------------------------------------------------------- -/// Disable interrupts unless they are already off (nested-safe). Returns -/// whether they were on, so `irq_restore` re-enables exactly when needed. -/// The terminal write path uses this so a klog screen-echo (which can run -/// in ISR context) can never tear a shell write, and a task-context write -/// can never be split by a tick. -fn irq_off() -> bool { - let rflags: u64; +static mut HOME: Option = None; + +fn home() -> &'static mut Terminal { unsafe { - core::arch::asm!( - "pushfq; pop {}", - out(reg) rflags, - options(nostack, preserves_flags) - ); - if rflags & (1 << 9) != 0 { - core::arch::asm!("cli", options(nostack, preserves_flags)); - true - } else { - false - } + let slot = &mut *core::ptr::addr_of_mut!(HOME); + slot.as_mut().expect("vt: not initialised") } } -fn irq_restore(was_on: bool) { - if was_on { - unsafe { - core::arch::asm!("sti", options(nostack, preserves_flags)); +/// Initialise the home terminal over a full-screen pixel buffer. Call once +/// after `gop::init()`. +pub fn init() { + let (w, h) = gop::pixel_dims(); + if w == 0 || h == 0 { + let lines = [ + "FATAL: no framebuffer geometry", + "GOP missing or active mode is 0x0", + "see the 'gop:' boot marker / serial log", + ]; + crate::gop::dump_lines(&lines); + crate::serial_println!("FATAL: no framebuffer geometry (GOP missing or 0x0 mode)"); + loop { + unsafe { + core::arch::asm!("cli; hlt"); + } } } + unsafe { + HOME = Some(Terminal::new(w, h)); + } } -/// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 -/// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; -/// undecodable bytes become '?'. -pub fn write_bytes(b: &[u8]) { - let on = irq_off(); - let mut i = 0; - while i < b.len() { - let byte = b[i]; - if byte < 0x80 { - put_byte(byte); - i += 1; - continue; - } - // Decode the next UTF-8 character starting at i. - match core::str::from_utf8(&b[i..]) { - Ok(s) => { - let c = s.chars().next().unwrap_or('?'); - write_char(c); - i += c.len_utf8(); - } - Err(e) => { - let valid = e.valid_up_to(); - if valid > 0 { - let s = unsafe { core::str::from_utf8_unchecked(&b[i..i + valid]) }; - let c = s.chars().next().unwrap_or('?'); - write_char(c); - i += c.len_utf8(); - } else { - put_byte(b'?'); - i += 1; - } - } +/// The home terminal instance (for the WM/boot to attach to a window). +pub fn home_terminal() -> &'static mut Terminal { + home() +} + +pub fn ready() -> bool { + unsafe { + let slot = &*core::ptr::addr_of!(HOME); + match slot { + Some(t) => t.ready(), + None => false, } } - flush(); - irq_restore(on); } -/// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). -/// Atomic with respect to interrupts: a klog screen-echo in ISR context -/// cannot interleave with a shell write, and a tick cannot split a write. +pub fn size() -> (usize, usize) { + home().size() +} + +pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { + home().cell_raw(r, c) +} + +pub fn set_cursor_visible(v: bool) { + home().set_cursor_visible(v); +} + +pub fn sync_scroll() { + home().sync_scroll(); +} + +pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { + home().repaint_region(x, y, w, h); +} + +pub fn write_bytes(b: &[u8]) { + home().write_bytes(b); +} + pub fn write_str(s: &str) { - let on = irq_off(); - for c in s.chars() { - write_char(c); - } - flush(); - irq_restore(on); + home().write_str(s); } -/// Write a single ASCII byte / char. pub fn put_char(b: u8) { - let on = irq_off(); - put_byte(b); - flush(); - irq_restore(on); + home().put_char(b); } -/// Clear the whole screen, reset colours, home the cursor. pub fn clear_all() { - unsafe { - SGR_FG = 7; - SGR_BG = 0; - sync_scroll(); - erase_all(); - CURSOR_R = 0; - CURSOR_C = 0; - } - flush(); + home().clear_all(); } // --------------------------------------------------------------------------- -// rendering +// Interrupt-flag helpers (atomic terminal writes). // --------------------------------------------------------------------------- -/// Blit every cell whose content differs from what the framebuffer shows, -/// then draw the logical cursor (inverse video) on top. The cursor's -/// inverse colours are mirrored into RENDERED so that pixel shifts -/// (scrolling) or full clears can never leave a stale inverse block that -/// the diff can't see (blank-vs-blank). -fn flush() { +fn irq_off() -> bool { + let rflags: u64; unsafe { - // Deferred scrolls pending: the framebuffer lags the logical grid. - // Render nothing until `sync_scroll` pushes the batch — the diff - // would otherwise see every cell as changed and re-rasterise the - // whole screen once per line. - if PENDING_SCROLLS > 0 { - return; - } - } - let rows = unsafe { ROWS }; - let cols = unsafe { COLS }; - let n = rows * cols; - let cursor_visible = unsafe { CURSOR_VISIBLE }; - let (cr, cc) = unsafe { (CURSOR_R, CURSOR_C) }; - { - let cells = cells(); - let rendered = rendered_cells(); - for i in 0..n { - let row = i / cols; - let col = i % cols; - // The visible cursor cell is owned by the cursor pass below. - if cursor_visible && row == cr && col == cc { - continue; - } - let c = cells[i]; - let r = rendered[i]; - if c != r { - let fg = PALETTE[c.fg as usize]; - let bg = PALETTE[c.bg as usize]; - gop::blit_glyph(col, row, c.ch, fg, bg); - rendered[i] = c; - } + core::arch::asm!( + "pushfq; pop {}", + out(reg) rflags, + options(nostack, preserves_flags) + ); + if rflags & (1 << 9) != 0 { + core::arch::asm!("cli", options(nostack, preserves_flags)); + true + } else { + false } } +} - unsafe { - let (lr, lc) = LAST_CURSOR; - let moved = (lr, lc) != (CURSOR_R, CURSOR_C); - // Erase the previous cursor position (restore its normal colours) - // and record it in RENDERED so the diff stays in sync. - if (moved || !CURSOR_VISIBLE) && lr < rows && lc < cols { - let i = idx(lr, lc); - let c = cells()[i]; - let fg = PALETTE[c.fg as usize]; - let bg = PALETTE[c.bg as usize]; - gop::blit_glyph(lc, lr, c.ch, fg, bg); - rendered_cells()[i] = c; - } - LAST_CURSOR = (CURSOR_R, CURSOR_C); - // Draw the cursor as an inverse-video cell, mirroring the inverse - // colours into RENDERED (scrolling/clearing then repair it). - if CURSOR_VISIBLE && CURSOR_R < rows && CURSOR_C < cols { - let i = idx(CURSOR_R, CURSOR_C); - let c = cells()[i]; - let fg = PALETTE[c.fg as usize]; - let bg = PALETTE[c.bg as usize]; - gop::blit_glyph(CURSOR_C, CURSOR_R, c.ch, bg, fg); - rendered_cells()[i] = Cell { - ch: c.ch, - fg: c.bg, - bg: c.fg, - }; +fn irq_restore(was_on: bool) { + if was_on { + unsafe { + core::arch::asm!("sti", options(nostack, preserves_flags)); } } } -- cgit v1.2.3