From fffec0f04dfcc63bef39eb0441cfad7b584da484 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 23:44:41 +0200 Subject: rsnix v0.5.3: fix real-hardware scroll wedge — per-word display push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- CONTEXT.md | 19 ++++++++++++++----- Cargo.lock | 6 +++--- Cargo.toml | 2 +- rsnix/src/gop.rs | 38 ++++++++++++++++++++++++++------------ rsnix/src/sched.rs | 6 +++++- 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f068035..bf1e01f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -113,13 +113,17 @@ Workspace version is `a.b.c` (single version for all crates): Bump the workspace version in the root `Cargo.toml` when a change of that class lands — do not leave the version to drift. Current: -`0.5.2` — the GUI era landed with 0.5.0 (graphics layer + cursor +`0.5.3` — the GUI era landed with 0.5.0 (graphics layer + cursor compositor); 0.5.1 fixed the canvas at 800×600; 0.5.2 added fault diagnostics (screen + serial dumps for every CPU exception) and real-hardware hardening (framebuffer-size clamping, fb-region exclusion -from the allocators, authoritative fb geometry logging). A future -major GUI overhaul (graphics windows beyond the text surface) would be -the `a` bump (1.0.0). +from the allocators, authoritative fb geometry logging); 0.5.3 fixed +the real-hardware scroll wedge — the scroll's full-frame push is now +per-word volatile stores like the boot-time `clear()` instead of one +1.92 MB rep-movsb, which can stall forever on an uncached real-GPU +framebuffer (a stall, not an exception — hence no crash dump). A +future major GUI overhaul (graphics windows beyond the text surface) +would be the `a` bump (1.0.0). ## Project layout @@ -553,7 +557,12 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: shadow too; scrolling is a cached shadow memcpy + one write-only push to the display — the display is never *read* (uncached reads were the killer, since real-hardware GOP framebuffers are - uncacheable/write-combining). + uncacheable/write-combining). **The push is per-word volatile + stores (0.5.3), not one bulk rep-movsb**: a 1.92 MB REP MOVS to an + uncached real-GPU framebuffer wedged the CPU on some hardware (a + stall, not an exception — no dump fired), while the boot-time + `clear()` — the same per-word pattern — always worked. QEMU's + RAM-backed framebuffer never showed it. 2. deferred scroll batching (`vt::PENDING_SCROLLS`): the logical grid scrolls instantly in RAM while the framebuffer shift+push is deferred; N pending scrolls cost one push. Bursts of streaming diff --git a/Cargo.lock b/Cargo.lock index 942456c..65166d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "rsdos" -version = "0.5.2" +version = "0.5.3" dependencies = [ "rsnix", "shell", @@ -19,11 +19,11 @@ dependencies = [ [[package]] name = "rsnix" -version = "0.5.2" +version = "0.5.3" [[package]] name = "shell" -version = "0.5.2" +version = "0.5.3" dependencies = [ "rsdosedit", "rsnix", diff --git a/Cargo.toml b/Cargo.toml index 0aa38e2..6c16784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["rsnix", "shell", "rsdos", "rsdosedit"] [workspace.package] -version = "0.5.2" +version = "0.5.3" edition = "2024" # no_std UEFI kernel: no unwinder available, so panic must abort. 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 @@ /// module is the low-level rasteriser: glyph blits and pixel shifts on the /// linear framebuffer. use crate::font; -use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; +use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; use core::fmt; use core::ptr; -use core::sync::atomic::{AtomicU8, AtomicU64, Ordering}; +use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; /// Framebuffer state captured before ExitBootServices. pub struct Framebuffer { @@ -590,12 +590,16 @@ impl Framebuffer { unsafe { if SHADOW.is_null() { // No shadow (allocation failed): fall back to shifting the - // display directly. - ptr::copy( - self.base.add(n * row_bytes), - self.base, - total - n * row_bytes, - ); + // display directly — per-word volatile stores, never a + // bulk memcpy: a giant REP MOVS to an uncached MMIO + // framebuffer can stall forever on real hardware (the + // boot-time clear() uses the same per-word pattern and + // provably works). + let src = self.base.add(n * row_bytes) as *mut u32; + let n_words = (total - n * row_bytes) / 4; + for i in 0..n_words { + ptr::write_volatile(self.base.add(i * 4) as *mut u32, ptr::read(src.add(i))); + } let last_row = self.base.add(total - n * row_bytes); for i in 0..(n * row_words) { ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); @@ -609,15 +613,25 @@ impl Framebuffer { *SHADOW.add(i) = self.bg; } // Push the shadow to the display: write-only, no reads. - // A bulk copy (rep movsb) writes in 64-byte bursts that - // coalesce well on WC framebuffers — far fewer bus - // transactions than per-pixel stores. + // + // Per-word volatile stores, NOT a bulk rep-movsb: the shadow + // is cached RAM (reads are cheap) and each display store is + // individually posted, exactly like the boot-time clear(). A + // 1.92 MB REP MOVS to an uncached real-GPU framebuffer can + // wedge the CPU — the "fast string" burst path never drains + // on some hardware, the shell stalls mid-instruction, no + // exception fires, and the machine looks hung with a torn + // frame (the white bands). QEMU never shows this because its + // framebuffer is RAM. // // Clamp to the firmware's mapping size; a pitch-padded mode // whose `pitch * height` exceeds the buffer degrades to a // truncated view instead of faulting past the mapping. let push_words = words.min(self.framebuffer_size / 4); - ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, push_words); + let dst = self.base as *mut u32; + for i in 0..push_words { + ptr::write_volatile(dst.add(i), ptr::read(SHADOW.add(i))); + } // The scroll copied the shadow (which never contains the // cursor); re-composite it on top of the pushed frame. 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 { }; let ok = spawn_at(id, entry, name); unlock(); - if ok { Some(id) } else { None } + if ok { + Some(id) + } else { + None + } } unsafe fn reap(id: usize) { -- cgit v1.2.3