summaryrefslogtreecommitdiffstats
path: root/rsnix
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 23:44:41 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 23:44:41 +0200
commitfffec0f04dfcc63bef39eb0441cfad7b584da484 (patch)
tree394dc1897e04d0aac400061494023d1d16a89dd2 /rsnix
parentrsnix v0.5.2: fault diagnostics + real-hardware scroll-crash hardening (diff)
downloadrs-dos-fffec0f04dfcc63bef39eb0441cfad7b584da484.tar.gz
rs-dos-fffec0f04dfcc63bef39eb0441cfad7b584da484.zip
rsnix v0.5.3: fix real-hardware scroll wedge — per-word display push
The real-hardware crash (2 lines of white + freeze, no CPU exception even with the 0.5.2 fault dumps) pointed away from a fault and at a stall: the scroll's full-frame push used one 1.92 MB rep-movsb to the framebuffer. On QEMU the framebuffer is RAM, so it flies; on a real GPU the framebuffer is uncached MMIO and the CPU's fast-string burst path can stall forever — the shell wedges mid-instruction, the cursor task's own writes wedge too, no exception fires (MMIO writes don't fault), and the screen shows the torn partial push (the white bands). The boot-time clear() always worked on the same hardware and uses per-word volatile stores, so the scroll push now uses the identical pattern: shadow reads (cached RAM, cheap) + individually-posted display stores. Interruptible between stores, never a bulk burst, and functionally identical on QEMU (all 5 suites re-pass). Also converts the no-shadow fallback shift to the same per-word pattern. CONTEXT.md updated (scroll-cost note + versioning).
Diffstat (limited to 'rsnix')
-rw-r--r--rsnix/src/gop.rs38
-rw-r--r--rsnix/src/sched.rs6
2 files changed, 31 insertions, 13 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index 47a2208..439dfb1 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -4,13 +4,13 @@
4/// module is the low-level rasteriser: glyph blits and pixel shifts on the 4/// module is the low-level rasteriser: glyph blits and pixel shifts on the
5/// linear framebuffer. 5/// linear framebuffer.
6use crate::font; 6use crate::font;
7use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; 7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB};
8use alloc::boxed::Box; 8use alloc::boxed::Box;
9use alloc::vec; 9use alloc::vec;
10use alloc::vec::Vec; 10use alloc::vec::Vec;
11use core::fmt; 11use core::fmt;
12use core::ptr; 12use core::ptr;
13use core::sync::atomic::{AtomicU8, AtomicU64, Ordering}; 13use core::sync::atomic::{AtomicU64, AtomicU8, Ordering};
14 14
15/// Framebuffer state captured before ExitBootServices. 15/// Framebuffer state captured before ExitBootServices.
16pub struct Framebuffer { 16pub struct Framebuffer {
@@ -590,12 +590,16 @@ impl Framebuffer {
590 unsafe { 590 unsafe {
591 if SHADOW.is_null() { 591 if SHADOW.is_null() {
592 // No shadow (allocation failed): fall back to shifting the 592 // No shadow (allocation failed): fall back to shifting the
593 // display directly. 593 // display directly — per-word volatile stores, never a
594 ptr::copy( 594 // bulk memcpy: a giant REP MOVS to an uncached MMIO
595 self.base.add(n * row_bytes), 595 // framebuffer can stall forever on real hardware (the
596 self.base, 596 // boot-time clear() uses the same per-word pattern and
597 total - n * row_bytes, 597 // provably works).
598 ); 598 let src = self.base.add(n * row_bytes) as *mut u32;
599 let n_words = (total - n * row_bytes) / 4;
600 for i in 0..n_words {
601 ptr::write_volatile(self.base.add(i * 4) as *mut u32, ptr::read(src.add(i)));
602 }
599 let last_row = self.base.add(total - n * row_bytes); 603 let last_row = self.base.add(total - n * row_bytes);
600 for i in 0..(n * row_words) { 604 for i in 0..(n * row_words) {
601 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); 605 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg);
@@ -609,15 +613,25 @@ impl Framebuffer {
609 *SHADOW.add(i) = self.bg; 613 *SHADOW.add(i) = self.bg;
610 } 614 }
611 // Push the shadow to the display: write-only, no reads. 615 // Push the shadow to the display: write-only, no reads.
612 // A bulk copy (rep movsb) writes in 64-byte bursts that 616 //
613 // coalesce well on WC framebuffers — far fewer bus 617 // Per-word volatile stores, NOT a bulk rep-movsb: the shadow
614 // transactions than per-pixel stores. 618 // is cached RAM (reads are cheap) and each display store is
619 // individually posted, exactly like the boot-time clear(). A
620 // 1.92 MB REP MOVS to an uncached real-GPU framebuffer can
621 // wedge the CPU — the "fast string" burst path never drains
622 // on some hardware, the shell stalls mid-instruction, no
623 // exception fires, and the machine looks hung with a torn
624 // frame (the white bands). QEMU never shows this because its
625 // framebuffer is RAM.
615 // 626 //
616 // Clamp to the firmware's mapping size; a pitch-padded mode 627 // Clamp to the firmware's mapping size; a pitch-padded mode
617 // whose `pitch * height` exceeds the buffer degrades to a 628 // whose `pitch * height` exceeds the buffer degrades to a
618 // truncated view instead of faulting past the mapping. 629 // truncated view instead of faulting past the mapping.
619 let push_words = words.min(self.framebuffer_size / 4); 630 let push_words = words.min(self.framebuffer_size / 4);
620 ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, push_words); 631 let dst = self.base as *mut u32;
632 for i in 0..push_words {
633 ptr::write_volatile(dst.add(i), ptr::read(SHADOW.add(i)));
634 }
621 // The scroll copied the shadow (which never contains the 635 // The scroll copied the shadow (which never contains the
622 // cursor); re-composite it on top of the pushed frame. 636 // cursor); re-composite it on top of the pushed frame.
623 crate::gop::cursor_redraw(); 637 crate::gop::cursor_redraw();
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs
index cfce58f..f48e210 100644
--- a/rsnix/src/sched.rs
+++ b/rsnix/src/sched.rs
@@ -282,7 +282,11 @@ pub fn spawn(entry: fn(), name: &str) -> Option<usize> {
282 }; 282 };
283 let ok = spawn_at(id, entry, name); 283 let ok = spawn_at(id, entry, name);
284 unlock(); 284 unlock();
285 if ok { Some(id) } else { None } 285 if ok {
286 Some(id)
287 } else {
288 None
289 }
286} 290}
287 291
288unsafe fn reap(id: usize) { 292unsafe fn reap(id: usize) {