diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 21:30:48 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 21:30:48 +0200 |
| commit | d9c47d52039be7a61d3e9d3652f73322fb6f47ba (patch) | |
| tree | 6e195f0819c97378ec5e0a3a049365d5e1f59ea5 | |
| parent | rsnix v0.5.0: graphics layer + cursor compositor (diff) | |
| download | rs-dos-d9c47d52039be7a61d3e9d3652f73322fb6f47ba.tar.gz rs-dos-d9c47d52039be7a61d3e9d3652f73322fb6f47ba.zip | |
rsnix v0.5.1: force the GUI canvas to 800x600 via GOP SetMode
The kernel used to inherit whatever GOP mode the firmware picked
(QEMU/OVMF gave 1280x800). Now every surface derives from a fixed
800x600 canvas:
- uefi: typed EfiGopQueryMode/SetMode/GetMode fn pointers in
GraphicsOutputProtocol (were raw *mut c_void, never called).
- gop::select_gop_mode: scan the mode list for an exact 800x600
(32-bit BGR/RGB only, the driver assumes 4 Bpp), SetMode it; fall
back to the smallest mode that fits the canvas, then to the
firmware's choice. SetMode may move both the mode info pointer and
the framebuffer base, so both are re-read after the switch. Boot
log: 'gop: mode 3 selected — 800x600 (was 1280x800)'.
- tests: relative assertions are resolution-agnostic; test-gfx and
test-mouse had a boot-log race (shell line prints before the cursor
task line) — now wait for the cursor line instead of grepping once.
All four suites pass at 800x600. 0 build warnings.
| -rw-r--r-- | CONTEXT.md | 16 | ||||
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 87 | ||||
| -rw-r--r-- | rsnix/src/uefi.rs | 22 | ||||
| -rwxr-xr-x | scripts/test-gfx.sh | 2 | ||||
| -rwxr-xr-x | scripts/test-mouse.sh | 4 |
7 files changed, 124 insertions, 15 deletions
| @@ -113,10 +113,10 @@ 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.0` — the GUI era landed (graphics layer + cursor compositor) as a | 116 | `0.5.1` — the GUI era landed with 0.5.0 (graphics layer + cursor |
| 117 | `b` bump from the 0.4.0 input foundation; a future major GUI overhaul | 117 | compositor) and the canvas was fixed at 800×600 as a `c` bump; a |
| 118 | (graphics windows beyond the text surface) would be the `a` bump | 118 | future major GUI overhaul (graphics windows beyond the text surface) |
| 119 | (1.0.0). | 119 | would be the `a` bump (1.0.0). |
| 120 | 120 | ||
| 121 | ## Project layout | 121 | ## Project layout |
| 122 | 122 | ||
| @@ -515,6 +515,14 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 515 | the cursor tip is white on the display but absent from the shadow | 515 | the cursor tip is white on the display but absent from the shadow |
| 516 | (overlay purity), and prints PASS/FAIL per stage. | 516 | (overlay purity), and prints PASS/FAIL per stage. |
| 517 | 517 | ||
| 518 | - **Canvas resolution fixed at 800×600** — `gop::init` scans the GOP | ||
| 519 | mode list and calls `SetMode` for an exact 800×600 (BGR/RGB 32-bit; | ||
| 520 | QEMU std-vga lists it as mode 3, real GPUs list it as a standard | ||
| 521 | mode), falling back to the smallest mode that fits, then to the | ||
| 522 | firmware's choice. Everything derives from it: the vt text grid, | ||
| 523 | the cursor clamp, the demo strip. `SetMode` may move the info | ||
| 524 | pointer and framebuffer base, so they are re-read after the switch. | ||
| 525 | |||
| 518 | Verified in QEMU (`make test-gfx`): `fill_rect/shadow: OK`, | 526 | Verified in QEMU (`make test-gfx`): `fill_rect/shadow: OK`, |
| 519 | `display push: OK`, `cursor overlay: OK`, `cursor not in shadow: | 527 | `display push: OK`, `cursor overlay: OK`, `cursor not in shadow: |
| 520 | OK`, `gfx check: PASS`; the demo draws; the cursor tracks injected | 528 | OK`, `gfx check: PASS`; the demo draws; the cursor tracks injected |
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.0" | 7 | version = "0.5.1" |
| 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.0" | 22 | version = "0.5.1" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.0" | 26 | version = "0.5.1" |
| 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.0" | 6 | version = "0.5.1" |
| 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 9dedcb8..dad4505 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -4,7 +4,7 @@ | |||
| 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::{GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; | 7 | use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, 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; |
| @@ -37,8 +37,93 @@ static mut FB: Option<Framebuffer> = None; | |||
| 37 | static mut SHADOW: *mut u32 = core::ptr::null_mut(); | 37 | static mut SHADOW: *mut u32 = core::ptr::null_mut(); |
| 38 | static mut SHADOW_WORDS: usize = 0; | 38 | static mut SHADOW_WORDS: usize = 0; |
| 39 | 39 | ||
| 40 | /// The GUI canvas. Every display surface assumes this resolution: | ||
| 41 | /// `vt` derives its text grid from it, the compositor's cursor clamps | ||
| 42 | /// to it, and the future window manager lays out windows on it. We | ||
| 43 | /// force it through GOP `SetMode` at boot — QEMU's std-vga offers it, | ||
| 44 | /// and real GPUs list it as a standard mode — falling back to the | ||
| 45 | /// smallest mode that fits, then to whatever the firmware chose. | ||
| 46 | const WANTED_W: usize = 800; | ||
| 47 | const WANTED_H: usize = 600; | ||
| 48 | |||
| 49 | /// Pick a GOP mode: prefer an exact 800×600 (BGR/RGB 32-bit), else the | ||
| 50 | /// smallest mode that fits the canvas, else leave the firmware's choice | ||
| 51 | /// alone. Call before reading the framebuffer — `SetMode` may move | ||
| 52 | /// both the mode info pointer and the framebuffer base. | ||
| 53 | fn select_gop_mode(gop: &mut GraphicsOutputProtocol) { | ||
| 54 | let mode = unsafe { &*gop.mode }; | ||
| 55 | let cur = mode.mode; | ||
| 56 | let info = unsafe { &*mode.info }; | ||
| 57 | let cur_w = info.horizontal_resolution as usize; | ||
| 58 | let cur_h = info.vertical_resolution as usize; | ||
| 59 | if cur_w == WANTED_W && cur_h == WANTED_H { | ||
| 60 | crate::serial_println!("gop: mode {} — already {}x{}", cur, cur_w, cur_h); | ||
| 61 | return; | ||
| 62 | } | ||
| 63 | |||
| 64 | let mut best: Option<(u32, usize, usize)> = None; // (mode#, w, h) | ||
| 65 | for i in 0..mode.max_mode { | ||
| 66 | let mut size: usize = 0; | ||
| 67 | let mut info_ptr: *mut GopModeInfo = core::ptr::null_mut(); | ||
| 68 | let st = unsafe { (gop.query_mode)(gop, i, &mut size, &mut info_ptr) }; | ||
| 69 | if st != EFI_SUCCESS || info_ptr.is_null() { | ||
| 70 | continue; | ||
| 71 | } | ||
| 72 | let mi = unsafe { &*info_ptr }; | ||
| 73 | if mi.pixel_format != PIXEL_BGR && mi.pixel_format != PIXEL_RGB { | ||
| 74 | continue; // 32-bit BGR/RGB only (the driver assumes 4 Bpp) | ||
| 75 | } | ||
| 76 | let w = mi.horizontal_resolution as usize; | ||
| 77 | let h = mi.vertical_resolution as usize; | ||
| 78 | if w == WANTED_W && h == WANTED_H { | ||
| 79 | best = Some((i, w, h)); | ||
| 80 | break; | ||
| 81 | } | ||
| 82 | if w >= WANTED_W && h >= WANTED_H { | ||
| 83 | match best { | ||
| 84 | Some((_, bw, bh)) if bw * bh <= w * h => {} | ||
| 85 | _ => best = Some((i, w, h)), | ||
| 86 | } | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | match best { | ||
| 91 | Some((n, w, h)) => { | ||
| 92 | let st = unsafe { (gop.set_mode)(gop, n) }; | ||
| 93 | if st != EFI_SUCCESS { | ||
| 94 | crate::serial_println!( | ||
| 95 | "gop: SetMode({}) failed 0x{:x} — keeping current {}x{}", | ||
| 96 | n, | ||
| 97 | st, | ||
| 98 | cur_w, | ||
| 99 | cur_h | ||
| 100 | ); | ||
| 101 | return; | ||
| 102 | } | ||
| 103 | crate::serial_println!( | ||
| 104 | "gop: mode {} selected — {}x{} (was {}x{})", | ||
| 105 | n, | ||
| 106 | w, | ||
| 107 | h, | ||
| 108 | cur_w, | ||
| 109 | cur_h | ||
| 110 | ); | ||
| 111 | } | ||
| 112 | None => crate::serial_println!( | ||
| 113 | "gop: no mode ≥ {}x{} — keeping current {}x{}", | ||
| 114 | WANTED_W, | ||
| 115 | WANTED_H, | ||
| 116 | cur_w, | ||
| 117 | cur_h | ||
| 118 | ), | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 40 | /// Initialise the framebuffer from GOP. Call *before* ExitBootServices. | 122 | /// Initialise the framebuffer from GOP. Call *before* ExitBootServices. |
| 41 | pub fn init(gop: &mut GraphicsOutputProtocol) { | 123 | pub fn init(gop: &mut GraphicsOutputProtocol) { |
| 124 | // Force the 800×600 canvas first; SetMode may move the info | ||
| 125 | // pointer and framebuffer base, so everything is re-read after. | ||
| 126 | select_gop_mode(gop); | ||
| 42 | let mode = unsafe { &*gop.mode }; | 127 | let mode = unsafe { &*gop.mode }; |
| 43 | let info = unsafe { &*mode.info }; | 128 | let info = unsafe { &*mode.info }; |
| 44 | let base = mode.framebuffer_base as *mut u8; | 129 | let base = mode.framebuffer_base as *mut u8; |
diff --git a/rsnix/src/uefi.rs b/rsnix/src/uefi.rs index f947c90..c311ad9 100644 --- a/rsnix/src/uefi.rs +++ b/rsnix/src/uefi.rs | |||
| @@ -321,12 +321,28 @@ pub struct GopMode { | |||
| 321 | 321 | ||
| 322 | #[repr(C)] | 322 | #[repr(C)] |
| 323 | pub struct GraphicsOutputProtocol { | 323 | pub struct GraphicsOutputProtocol { |
| 324 | pub query_mode: *mut c_void, | 324 | pub query_mode: EfiGopQueryMode, |
| 325 | pub set_mode: *mut c_void, | 325 | pub set_mode: EfiGopSetMode, |
| 326 | pub get_current_mode: *mut c_void, | 326 | pub get_current_mode: EfiGopGetMode, |
| 327 | pub mode: *mut GopMode, | 327 | pub mode: *mut GopMode, |
| 328 | } | 328 | } |
| 329 | 329 | ||
| 330 | pub type EfiGopQueryMode = unsafe extern "efiapi" fn( | ||
| 331 | *mut GraphicsOutputProtocol, | ||
| 332 | u32, // mode number | ||
| 333 | *mut usize, // out: size of info | ||
| 334 | *mut *mut GopModeInfo, // out: info pointer (firmware-owned) | ||
| 335 | ) -> Status; | ||
| 336 | |||
| 337 | pub type EfiGopSetMode = unsafe extern "efiapi" fn(*mut GraphicsOutputProtocol, u32) -> Status; | ||
| 338 | |||
| 339 | pub type EfiGopGetMode = unsafe extern "efiapi" fn( | ||
| 340 | *mut GraphicsOutputProtocol, | ||
| 341 | *mut u32, // out: current mode | ||
| 342 | *mut u32, // out: size of info | ||
| 343 | *mut *mut GopModeInfo, // out: info pointer | ||
| 344 | ) -> Status; | ||
| 345 | |||
| 330 | // --------------------------------------------------------------------------- | 346 | // --------------------------------------------------------------------------- |
| 331 | // System table | 347 | // System table |
| 332 | // --------------------------------------------------------------------------- | 348 | // --------------------------------------------------------------------------- |
diff --git a/scripts/test-gfx.sh b/scripts/test-gfx.sh index a6e4016..7bee4ee 100755 --- a/scripts/test-gfx.sh +++ b/scripts/test-gfx.sh | |||
| @@ -85,7 +85,7 @@ rm -f "$SOCK" "$LOG" | |||
| 85 | echo $! >"$QPID" | 85 | echo $! >"$QPID" |
| 86 | 86 | ||
| 87 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | 87 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" |
| 88 | grep -aq "cursor: task " "$LOG" || fail "cursor task did not spawn" | 88 | wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" |
| 89 | 89 | ||
| 90 | echo "==> 4/5 in-kernel pixel self-test (gfx check)" | 90 | echo "==> 4/5 in-kernel pixel self-test (gfx check)" |
| 91 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | 91 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ |
diff --git a/scripts/test-mouse.sh b/scripts/test-mouse.sh index e58d474..ea58f91 100755 --- a/scripts/test-mouse.sh +++ b/scripts/test-mouse.sh | |||
| @@ -95,7 +95,7 @@ echo $! >"$QPID" | |||
| 95 | 95 | ||
| 96 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | 96 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" |
| 97 | grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" | 97 | grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" |
| 98 | grep -aq "cursor: task " "$LOG" || fail "cursor task did not spawn" | 98 | wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" |
| 99 | 99 | ||
| 100 | echo "==> 4/6 cursor position tracks injected movement" | 100 | echo "==> 4/6 cursor position tracks injected movement" |
| 101 | INIT=$(cursor_state) | 101 | INIT=$(cursor_state) |
| @@ -153,7 +153,7 @@ qemu_hmp "sendkey ctrl-c" | |||
| 153 | grep -aq "mouse: routing events to task 3" "$LOG" || fail "mouse 3 did not arm routing" | 153 | grep -aq "mouse: routing events to task 3" "$LOG" || fail "mouse 3 did not arm routing" |
| 154 | grep -aq "msgrecv\[3\]: Mouse(+" "$LOG" || fail "task 3 queue did not receive the routed Mouse event" | 154 | grep -aq "msgrecv\[3\]: Mouse(+" "$LOG" || fail "task 3 queue did not receive the routed Mouse event" |
| 155 | # Watch line: x/y/buttons format (any occurrence after watching started). | 155 | # Watch line: x/y/buttons format (any occurrence after watching started). |
| 156 | grep -aq "mouse: x=$((X3 + 3)) y=$((Y3 + 2)) buttons=---" "$LOG" || fail "watch did not print the moved cursor (x=$((X3+3)) y=$((Y3+2)))" | 156 | grep -aq "mouse: x=$((X3 + 3)) y=$((Y3 + 2)) buttons=---" "$LOG" || fail "watch did not print the moved cursor (x=$((X3 + 3)) y=$((Y3 + 2)))" |
| 157 | grep -aq "mouse: stopped" "$LOG" || fail "^C did not stop the mouse watch" | 157 | grep -aq "mouse: stopped" "$LOG" || fail "^C did not stop the mouse watch" |
| 158 | 158 | ||
| 159 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "poweroff" | 159 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "poweroff" |
