summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 17:42:21 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 17:42:21 +0200
commita2ffb8358cf4f4fc7230b5307c651d5badab0860 (patch)
tree5412fa837a6832ca078443e7668a776ee760f75c
parentvt: decode UTF-8, render box-drawing/arrow glyphs procedurally (diff)
downloadrs-dos-a2ffb8358cf4f4fc7230b5307c651d5badab0860.tar.gz
rs-dos-a2ffb8358cf4f4fc7230b5307c651d5badab0860.zip
vt: don't strand stale inverse cursor cells on scroll/clear
The cursor was blitted as an inverse-video cell directly onto the framebuffer, but that inversion was never recorded. RENDERED (the diff grid) only stored the logical cell, so when scrolling shifted the raw pixels up (gop::shift_up), the stale inverse block rode along while the grids said 'blank' — and blank-vs-blank never re-blits. Result: a cursor-shaped block at the end of every scrolled line, surviving even 'clear'. - flush() now mirrors the inverse colours into RENDERED when drawing the cursor (and restores RENDERED when erasing it), and the diff loop skips the visible cursor cell (owned by the cursor pass). Any scroll/clear then sees CELLS != RENDERED at a stale-inverse position and repaints it. - memory.rs heap self-test: snapshot heap state before its own allocations and assert the heap returns to that state, instead of asserting it coalesces to a single empty block. The vt cell grids (allocated earlier in boot) made the old 'free_blocks == 1' check fail; the new check is correct regardless of prior live allocations. Verified in QEMU: hexdump /dev/mem streams hundreds of lines, ^C, screendump shows exactly one inverse cell (the live cursor); 'clear' leaves exactly one (the fresh prompt cursor). heap self-test OK again.
-rw-r--r--CONTEXT.md4
-rw-r--r--rsnix/src/fs.rs2
-rw-r--r--rsnix/src/memory.rs7
-rw-r--r--rsnix/src/vt.rs33
4 files changed, 36 insertions, 10 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 20c60f7..a1ef4bb 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -106,7 +106,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown:
106- `vt.rs` — terminal emulator: cell grid, cursor, ANSI CSI parser (H/J/K, 106- `vt.rs` — terminal emulator: cell grid, cursor, ANSI CSI parser (H/J/K,
107 cursor moves, SGR 16-colour, `?25l/h`), dirty-cell blitting, UTF-8 107 cursor moves, SGR 16-colour, `?25l/h`), dirty-cell blitting, UTF-8
108 decoding (box-drawing `│`/`═`, arrows `←↑→↓`, em dash `—` render 108 decoding (box-drawing `│`/`═`, arrows `←↑→↓`, em dash `—` render
109 procedurally instead of `?`) 109 procedurally instead of `?`). The inverse cursor is mirrored into the
110 rendered grid so scrolling / `clear` can never strand stale inverse
111 cells on the framebuffer.
110- `tty.rs` — line discipline: canonical `read_line` (echo, line buffer, 112- `tty.rs` — line discipline: canonical `read_line` (echo, line buffer,
111 ^C/^D) and raw `read_byte` (terminal byte protocol: arrows → `ESC [ A–D`, 113 ^C/^D) and raw `read_byte` (terminal byte protocol: arrows → `ESC [ A–D`,
112 ^X → `0x18`, …) 114 ^X → `0x18`, …)
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs
index 95a84bf..df9308b 100644
--- a/rsnix/src/fs.rs
+++ b/rsnix/src/fs.rs
@@ -418,7 +418,7 @@ pub fn remove_path(path: &str) -> Result<(), ()> {
418 418
419fn version_text() -> String { 419fn version_text() -> String {
420 alloc::format!( 420 alloc::format!(
421 "rsnix v{} — TempleOS-inspired toy kernel\nUEFI x86_64, identity paging, free-list heap, bitmap pages\n", 421 "rsnix v{} — Official RS-DOS kernel\nUEFI x86_64, identity paging, free-list heap, bitmap pages\n",
422 env!("CARGO_PKG_VERSION") 422 env!("CARGO_PKG_VERSION")
423 ) 423 )
424} 424}
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs
index d33c781..c80edab 100644
--- a/rsnix/src/memory.rs
+++ b/rsnix/src/memory.rs
@@ -436,6 +436,11 @@ pub fn heap_stats() -> HeapStats {
436/// the heap itself (fixed-size stack arrays only). Returns true on success. 436/// the heap itself (fixed-size stack arrays only). Returns true on success.
437pub fn self_test() -> bool { 437pub fn self_test() -> bool {
438 unsafe { 438 unsafe {
439 // Snapshot the heap before we start: earlier boot code (e.g. the
440 // vt cell grids) may legitimately hold live allocations. The test
441 // must return the heap to exactly this state, not to "empty".
442 let before = heap_stats();
443
439 const N: usize = 128; 444 const N: usize = 128;
440 // (ptr, size, align) triplets — fixed array on the stack. 445 // (ptr, size, align) triplets — fixed array on the stack.
441 let mut blocks: [(usize, usize, usize); N] = [(0, 0, 0); N]; 446 let mut blocks: [(usize, usize, usize); N] = [(0, 0, 0); N];
@@ -504,7 +509,7 @@ pub fn self_test() -> bool {
504 ALLOCATOR.dealloc(*ptr as *mut u8, layout); 509 ALLOCATOR.dealloc(*ptr as *mut u8, layout);
505 } 510 }
506 let stats = heap_stats(); 511 let stats = heap_stats();
507 stats.free_blocks == 1 && stats.free_bytes == stats.heap_size 512 stats.free_blocks == before.free_blocks && stats.free_bytes == before.free_bytes
508 } 513 }
509} 514}
510 515
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 3b81d68..4db08fc 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -485,20 +485,29 @@ pub fn clear_all() {
485// --------------------------------------------------------------------------- 485// ---------------------------------------------------------------------------
486 486
487/// Blit every cell whose content differs from what the framebuffer shows, 487/// Blit every cell whose content differs from what the framebuffer shows,
488/// then draw the logical cursor (inverse video) on top. 488/// then draw the logical cursor (inverse video) on top. The cursor's
489/// inverse colours are mirrored into RENDERED so that pixel shifts
490/// (scrolling) or full clears can never leave a stale inverse block that
491/// the diff can't see (blank-vs-blank).
489fn flush() { 492fn flush() {
490 let rows = unsafe { ROWS }; 493 let rows = unsafe { ROWS };
491 let cols = unsafe { COLS }; 494 let cols = unsafe { COLS };
492 let n = rows * cols; 495 let n = rows * cols;
496 let cursor_visible = unsafe { CURSOR_VISIBLE };
497 let (cr, cc) = unsafe { (CURSOR_R, CURSOR_C) };
493 { 498 {
494 let cells = cells(); 499 let cells = cells();
495 let rendered = rendered_cells(); 500 let rendered = rendered_cells();
496 for i in 0..n { 501 for i in 0..n {
502 let row = i / cols;
503 let col = i % cols;
504 // The visible cursor cell is owned by the cursor pass below.
505 if cursor_visible && row == cr && col == cc {
506 continue;
507 }
497 let c = cells[i]; 508 let c = cells[i];
498 let r = rendered[i]; 509 let r = rendered[i];
499 if c != r { 510 if c != r {
500 let row = i / cols;
501 let col = i % cols;
502 let fg = PALETTE[c.fg as usize]; 511 let fg = PALETTE[c.fg as usize];
503 let bg = PALETTE[c.bg as usize]; 512 let bg = PALETTE[c.bg as usize];
504 gop::blit_glyph(col, row, c.ch, fg, bg); 513 gop::blit_glyph(col, row, c.ch, fg, bg);
@@ -510,20 +519,30 @@ fn flush() {
510 unsafe { 519 unsafe {
511 let (lr, lc) = LAST_CURSOR; 520 let (lr, lc) = LAST_CURSOR;
512 let moved = (lr, lc) != (CURSOR_R, CURSOR_C); 521 let moved = (lr, lc) != (CURSOR_R, CURSOR_C);
513 // Erase the previous cursor position (restore its normal colours). 522 // Erase the previous cursor position (restore its normal colours)
523 // and record it in RENDERED so the diff stays in sync.
514 if (moved || !CURSOR_VISIBLE) && lr < rows && lc < cols { 524 if (moved || !CURSOR_VISIBLE) && lr < rows && lc < cols {
515 let c = cells()[idx(lr, lc)]; 525 let i = idx(lr, lc);
526 let c = cells()[i];
516 let fg = PALETTE[c.fg as usize]; 527 let fg = PALETTE[c.fg as usize];
517 let bg = PALETTE[c.bg as usize]; 528 let bg = PALETTE[c.bg as usize];
518 gop::blit_glyph(lc, lr, c.ch, fg, bg); 529 gop::blit_glyph(lc, lr, c.ch, fg, bg);
530 rendered_cells()[i] = c;
519 } 531 }
520 LAST_CURSOR = (CURSOR_R, CURSOR_C); 532 LAST_CURSOR = (CURSOR_R, CURSOR_C);
521 // Draw the cursor as an inverse-video cell. 533 // Draw the cursor as an inverse-video cell, mirroring the inverse
534 // colours into RENDERED (scrolling/clearing then repair it).
522 if CURSOR_VISIBLE && CURSOR_R < rows && CURSOR_C < cols { 535 if CURSOR_VISIBLE && CURSOR_R < rows && CURSOR_C < cols {
523 let c = cells()[idx(CURSOR_R, CURSOR_C)]; 536 let i = idx(CURSOR_R, CURSOR_C);
537 let c = cells()[i];
524 let fg = PALETTE[c.fg as usize]; 538 let fg = PALETTE[c.fg as usize];
525 let bg = PALETTE[c.bg as usize]; 539 let bg = PALETTE[c.bg as usize];
526 gop::blit_glyph(CURSOR_C, CURSOR_R, c.ch, bg, fg); 540 gop::blit_glyph(CURSOR_C, CURSOR_R, c.ch, bg, fg);
541 rendered_cells()[i] = Cell {
542 ch: c.ch,
543 fg: c.bg,
544 bg: c.fg,
545 };
527 } 546 }
528 } 547 }
529} 548}