diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 23:44:41 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 23:44:41 +0200 |
| commit | fffec0f04dfcc63bef39eb0441cfad7b584da484 (patch) | |
| tree | 394dc1897e04d0aac400061494023d1d16a89dd2 | |
| parent | rsnix v0.5.2: fault diagnostics + real-hardware scroll-crash hardening (diff) | |
| download | rs-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).
| -rw-r--r-- | CONTEXT.md | 19 | ||||
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 38 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 6 |
5 files changed, 49 insertions, 22 deletions
| @@ -113,13 +113,17 @@ Workspace version is `a.b.c` (single version for all crates): | |||
| 113 | 113 | ||
| 114 | Bump the workspace version in the root `Cargo.toml` when a change of | 114 | Bump the workspace version in the root `Cargo.toml` when a change of |
| 115 | that class lands — do not leave the version to drift. Current: | 115 | that class lands — do not leave the version to drift. Current: |
| 116 | `0.5.2` — the GUI era landed with 0.5.0 (graphics layer + cursor | 116 | `0.5.3` — the GUI era landed with 0.5.0 (graphics layer + cursor |
| 117 | compositor); 0.5.1 fixed the canvas at 800×600; 0.5.2 added fault | 117 | compositor); 0.5.1 fixed the canvas at 800×600; 0.5.2 added fault |
| 118 | diagnostics (screen + serial dumps for every CPU exception) and | 118 | diagnostics (screen + serial dumps for every CPU exception) and |
| 119 | real-hardware hardening (framebuffer-size clamping, fb-region exclusion | 119 | real-hardware hardening (framebuffer-size clamping, fb-region exclusion |
| 120 | from the allocators, authoritative fb geometry logging). A future | 120 | from the allocators, authoritative fb geometry logging); 0.5.3 fixed |
| 121 | major GUI overhaul (graphics windows beyond the text surface) would be | 121 | the real-hardware scroll wedge — the scroll's full-frame push is now |
| 122 | the `a` bump (1.0.0). | 122 | per-word volatile stores like the boot-time `clear()` instead of one |
| 123 | 1.92 MB rep-movsb, which can stall forever on an uncached real-GPU | ||
| 124 | framebuffer (a stall, not an exception — hence no crash dump). A | ||
| 125 | future major GUI overhaul (graphics windows beyond the text surface) | ||
| 126 | would be the `a` bump (1.0.0). | ||
| 123 | 127 | ||
| 124 | ## Project layout | 128 | ## Project layout |
| 125 | 129 | ||
| @@ -553,7 +557,12 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 553 | shadow too; scrolling is a cached shadow memcpy + one write-only | 557 | shadow too; scrolling is a cached shadow memcpy + one write-only |
| 554 | push to the display — the display is never *read* (uncached reads | 558 | push to the display — the display is never *read* (uncached reads |
| 555 | were the killer, since real-hardware GOP framebuffers are | 559 | were the killer, since real-hardware GOP framebuffers are |
| 556 | uncacheable/write-combining). | 560 | uncacheable/write-combining). **The push is per-word volatile |
| 561 | stores (0.5.3), not one bulk rep-movsb**: a 1.92 MB REP MOVS to an | ||
| 562 | uncached real-GPU framebuffer wedged the CPU on some hardware (a | ||
| 563 | stall, not an exception — no dump fired), while the boot-time | ||
| 564 | `clear()` — the same per-word pattern — always worked. QEMU's | ||
| 565 | RAM-backed framebuffer never showed it. | ||
| 557 | 2. deferred scroll batching (`vt::PENDING_SCROLLS`): the logical grid | 566 | 2. deferred scroll batching (`vt::PENDING_SCROLLS`): the logical grid |
| 558 | scrolls instantly in RAM while the framebuffer shift+push is | 567 | scrolls instantly in RAM while the framebuffer shift+push is |
| 559 | deferred; N pending scrolls cost one push. Bursts of streaming | 568 | deferred; N pending scrolls cost one push. Bursts of streaming |
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.2" | 7 | version = "0.5.3" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.5.2" | 22 | version = "0.5.3" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.2" | 26 | version = "0.5.3" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.2" | 6 | version = "0.5.3" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # 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 @@ | |||
| 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. |
| 6 | use crate::font; | 6 | use crate::font; |
| 7 | use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; | 7 | use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; |
| 8 | use alloc::boxed::Box; | 8 | use alloc::boxed::Box; |
| 9 | use alloc::vec; | 9 | use alloc::vec; |
| 10 | use alloc::vec::Vec; | 10 | use alloc::vec::Vec; |
| 11 | use core::fmt; | 11 | use core::fmt; |
| 12 | use core::ptr; | 12 | use core::ptr; |
| 13 | use core::sync::atomic::{AtomicU8, AtomicU64, Ordering}; | 13 | use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; |
| 14 | 14 | ||
| 15 | /// Framebuffer state captured before ExitBootServices. | 15 | /// Framebuffer state captured before ExitBootServices. |
| 16 | pub struct Framebuffer { | 16 | pub 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 | ||
| 288 | unsafe fn reap(id: usize) { | 292 | unsafe fn reap(id: usize) { |
