summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--rsdos/src/main.rs9
-rw-r--r--rsnix/src/gop.rs158
-rw-r--r--rsnix/src/memory.rs37
-rwxr-xr-xscripts/build-image-release.sh99
4 files changed, 188 insertions, 115 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index aa116e4..2717dff 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -162,7 +162,13 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut
162 let (rows, cols) = rsnix::vt::size(); 162 let (rows, cols) = rsnix::vt::size();
163 rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); 163 rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols);
164 let (fw, fh) = rsnix::gop::pixel_dims(); 164 let (fw, fh) = rsnix::gop::pixel_dims();
165 rsnix::gop::boot_marker(&alloc::format!("rsnix: gop {}x{} vt {}x{} ok", fw, fh, cols, rows)); 165 rsnix::gop::boot_marker(&alloc::format!(
166 "rsnix: gop {}x{} vt {}x{} ok",
167 fw,
168 fh,
169 cols,
170 rows
171 ));
166 172
167 // ExitBootServices — we now own the machine 173 // ExitBootServices — we now own the machine
168 if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { 174 if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS {
@@ -212,7 +218,6 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut
212 } else { 218 } else {
213 rsnix::serial_println!("pages: init FAILED"); 219 rsnix::serial_println!("pages: init FAILED");
214 } 220 }
215 rsnix::gop::init_shadow();
216 rsnix::gop::boot_marker("rsnix: shadow ok"); 221 rsnix::gop::boot_marker("rsnix: shadow ok");
217 if rsnix::memory::page_self_test() { 222 if rsnix::memory::page_self_test() {
218 rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); 223 rsnix::serial_println!("pages: self-test OK (alloc/free/runs)");
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index 90c24bf..d37468f 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -80,8 +80,7 @@ const STAGE_MAX: usize = 96;
80/// The last boot stage string (for the panic band). 80/// The last boot stage string (for the panic band).
81pub fn last_boot_stage() -> alloc::string::String { 81pub fn last_boot_stage() -> alloc::string::String {
82 unsafe { 82 unsafe {
83 alloc::string::String::from_utf8_lossy(&LAST_BOOT_STAGE[..LAST_BOOT_STAGE_LEN]) 83 alloc::string::String::from_utf8_lossy(&LAST_BOOT_STAGE[..LAST_BOOT_STAGE_LEN]).into_owned()
84 .into_owned()
85 } 84 }
86} 85}
87 86
@@ -210,7 +209,7 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
210 let bgr = info.pixel_format == PIXEL_BGR; 209 let bgr = info.pixel_format == PIXEL_BGR;
211 let _ = fg; 210 let _ = fg;
212 211
213 let fb = Framebuffer { 212 let fbf = Framebuffer {
214 base, 213 base,
215 width, 214 width,
216 height, 215 height,
@@ -221,14 +220,8 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
221 bgr, 220 bgr,
222 }; 221 };
223 222
224 // The RAM backbuffer is allocated *after* ExitBootServices by 223 // Re-stash the post-SetMode region (base can move) for the
225 // `init_shadow()` (main.rs) — via the fb-aware page allocator so it 224 // allocators, and log the authoritative geometry for diagnostics.
226 // can never land on the framebuffer (see init_shadow). Nothing is
227 // drawn between fb.clear() here and init_shadow(), so the blank
228 // shadow is in sync with the cleared display.
229
230 // Re-stash the post-SetMode region (base can move) for the page
231 // allocator, and log the authoritative geometry for diagnostics.
232 unsafe { 225 unsafe {
233 FB_REGION_BASE = mode.framebuffer_base as usize; 226 FB_REGION_BASE = mode.framebuffer_base as usize;
234 FB_REGION_SIZE = mode.framebuffer_size; 227 FB_REGION_SIZE = mode.framebuffer_size;
@@ -244,104 +237,65 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
244 bgr 237 bgr
245 ); 238 );
246 239
247 fb.clear(); 240 fbf.clear();
248 241
249 unsafe { 242 unsafe {
250 FB = Some(fb); 243 FB = Some(fbf);
251 } 244 }
252} 245
253 246 // The RAM backbuffer is the *first* heap allocation: allocated here
254/// Allocate the RAM backbuffer. Must run *after* 247 // (pre-ExitBootServices, before vt::init builds its grids) so no
255/// `memory::init_page_allocator` (which excludes the GOP framebuffer 248 // other heap object can collide with it — the v0.5.3 layout that
256/// pages per-page), and before any text is painted. 249 // provably boots on real hardware. The heap free list excludes the
257/// 250 // fb region (init_heap splits at the fb hole) and we additionally
258/// The heap (pre-ExitBootServices) is excluded from the fb region only 251 // reject any range overlapping the fb, so the shadow can never
259/// by a coarse overlap subtraction that still leaves one contiguous 252 // alias the display (an aliased shadow turns every scroll into a
260/// free block *spanning* the fb hole — a 1.9 MB shadow allocation could 253 // 1.8 MB MMIO→MMIO memcpy on an uncached real fb — a wedge).
261/// therefore land *on* the framebuffer on machines where firmware 254 // Allocation failure degrades to no-shadow mode, never panics.
262/// reports it as ConventionalMemory. A shadow aliased over the display
263/// makes every scroll a 1.8 MB MMIO→MMIO memcpy, which can wedge a
264/// real uncached framebuffer (a stall, not a fault — hence no dump).
265/// The page allocator marks those pages used, so the shadow can never
266/// land there. Heap allocation is kept only as a fallback, with an
267/// explicit overlap rejection.
268pub fn init_shadow() {
269 unsafe { 255 unsafe {
270 let Some(fb) = fb().as_ref() else { 256 if let Some(fbinfo) = fb().as_ref() {
271 return; 257 let words = (fbinfo.pitch * fbinfo.height) / 4;
272 }; 258 if words > 0 {
273 let words = (fb.pitch * fb.height) / 4; 259 let bytes = words * 4;
274 if words == 0 { 260 #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic
275 return; 261 let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new();
276 } 262 if v.try_reserve_exact(words).is_ok() {
277 let bytes = words * 4; 263 v.resize(words, 0); // can't fail after the reserve
278 let pages = bytes.div_ceil(crate::memory::PAGE_SIZE); 264 let p = v.as_ptr() as usize;
279 let mut origin = "none"; 265 let overlaps = FB_REGION_SIZE > 0
280 266 && p < FB_REGION_BASE + FB_REGION_SIZE
281 // Primary: fb-aware page allocator (fb pages already marked 267 && p + bytes > FB_REGION_BASE;
282 // used by init_page_allocator). Defensively refuse the range if 268 if !overlaps {
283 // it ever overlaps the fb region anyway. 269 SHADOW = v.as_ptr() as *mut u32;
284 if let Some(p) = crate::memory::alloc_pages(pages) { 270 SHADOW_WORDS = words;
285 let overlaps = FB_REGION_SIZE > 0 271 for i in 0..words {
286 && p < FB_REGION_BASE + FB_REGION_SIZE 272 *SHADOW.add(i) = fbinfo.bg;
287 && p + bytes > FB_REGION_BASE; 273 }
288 if !overlaps { 274 core::mem::forget(v); // shadow owns the memory now
289 SHADOW = p as *mut u32; 275 }
290 SHADOW_WORDS = words;
291 origin = "page-alloc";
292 for i in 0..words {
293 *SHADOW.add(i) = fb.bg;
294 } 276 }
295 } 277 let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE);
296 } 278 let (shadow, swords) = (SHADOW as usize, SHADOW_WORDS);
297 if origin == "none" { 279 crate::serial_println!(
298 // Fallback: heap — must never panic here: an OOM would halt 280 "gop: shadow @ {:#x} words={} fb={:#x}..{:#x} origin=heap",
299 // the kernel before the shell banner, a blank screen on real 281 shadow,
300 // hardware (the old `vec![0; words]` could abort). On any 282 swords,
301 // failure we degrade to no-shadow mode instead. 283 rbase,
302 #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic 284 rbase + rsize,
303 let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new(); 285 );
304 if v.try_reserve_exact(words).is_ok() { 286 if SHADOW.is_null() {
305 v.resize(words, 0); // can't fail after the reserve 287 let msg =
306 let p = v.as_ptr() as usize; 288 "gop: WARNING — shadow rejected (fb overlap); no-shadow fallback";
307 let overlaps = FB_REGION_SIZE > 0 289 crate::serial_println!("{}", msg);
308 && p < FB_REGION_BASE + FB_REGION_SIZE 290 let lines: alloc::vec::Vec<&str> = msg
309 && p + bytes > FB_REGION_BASE; 291 .as_bytes()
310 if !overlaps { 292 .chunks(100)
311 SHADOW = v.as_ptr() as *mut u32; 293 .map(|c| core::str::from_utf8(c).unwrap_or("?"))
312 SHADOW_WORDS = words; 294 .collect();
313 origin = "heap"; 295 dump_lines(&lines);
314 core::mem::forget(v); // shadow owns the memory now
315 } 296 }
316 } 297 }
317 } 298 }
318 let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE);
319 let (shadow, swords) = (SHADOW as usize, SHADOW_WORDS);
320 crate::serial_println!(
321 "gop: shadow @ {:#x} words={} fb={:#x}..{:#x} origin={}",
322 shadow,
323 swords,
324 rbase,
325 rbase + rsize,
326 origin
327 );
328 // Real hardware has no serial console: surface a degraded shadow
329 // (heap fallback or rejected allocation) as a visible warning
330 // band so a boot on hardware shows it on the screen.
331 if origin != "page-alloc" {
332 let msg = if SHADOW.is_null() {
333 "gop: WARNING — shadow allocation rejected (fb overlap); no-shadow fallback"
334 } else {
335 "gop: WARNING — shadow from heap (page allocator unavailable)"
336 };
337 crate::serial_println!("{}", msg);
338 let lines: alloc::vec::Vec<&str> = msg
339 .as_bytes()
340 .chunks(100)
341 .map(|c| core::str::from_utf8(c).unwrap_or("?"))
342 .collect();
343 dump_lines(&lines);
344 }
345 } 299 }
346} 300}
347 301
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs
index a947c53..605125a 100644
--- a/rsnix/src/memory.rs
+++ b/rsnix/src/memory.rs
@@ -366,18 +366,33 @@ fn init_heap() {
366 if d.kind == MEMORY_TYPE_CONVENTIONAL { 366 if d.kind == MEMORY_TYPE_CONVENTIONAL {
367 let s = d.physical_start as usize; 367 let s = d.physical_start as usize;
368 let e = s + d.number_of_pages as usize * 4096; 368 let e = s + d.number_of_pages as usize * 4096;
369 // Subtract any overlap with the framebuffer region. 369 // Split at the framebuffer hole so the free list NEVER
370 let (cs, ce) = match fb { 370 // contains fb pages: a coarse size subtraction leaves one
371 Some((b, sz)) if e > b && s < b + sz => (s.max(b), e.min(b + sz)), 371 // block *spanning* the hole, letting a large allocation
372 _ => (e, s), // no overlap → empty range 372 // (e.g. the 1.9 MB display shadow) straddle the display.
373 }; 373 let mut lo = s;
374 let mut sz = e - s; 374 if let Some((b, sz)) = fb
375 if ce > cs { 375 && e > b
376 sz -= ce - cs; 376 && s < b + sz
377 {
378 let h0 = b.max(s);
379 let h1 = (b + sz).min(e);
380 if h0 > lo {
381 // Segment before the hole.
382 let seg = h0 - lo;
383 if seg > best_size {
384 best_start = lo;
385 best_size = seg;
386 }
387 }
388 lo = h1;
377 } 389 }
378 if sz > best_size { 390 if e > lo {
379 best_start = s; 391 let seg = e - lo;
380 best_size = sz; 392 if seg > best_size {
393 best_start = lo;
394 best_size = seg;
395 }
381 } 396 }
382 } 397 }
383 } 398 }
diff --git a/scripts/build-image-release.sh b/scripts/build-image-release.sh
new file mode 100755
index 0000000..32f8f91
--- /dev/null
+++ b/scripts/build-image-release.sh
@@ -0,0 +1,99 @@
1#!/usr/bin/env bash
2#
3# scripts/build-image.sh — assemble a bootable rs-dos disk image (UEFI/GPT)
4#
5# Layout:
6# Partition 1: ESP (FAT32) — rsboot bootloader + kernel
7#
8# Output:
9# rs-dos.img (or custom path) — bootable raw disk image for UEFI
10#
11# Usage:
12# ./scripts/build-image.sh [output.img]
13#
14set -euo pipefail
15
16SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
18
19cd "$PROJECT_DIR"
20
21BUILD_DIR="$PROJECT_DIR/build"
22ESP_DIR="$PROJECT_DIR/esp"
23
24IMAGE="${1:-$PROJECT_DIR/rs-dos.img}"
25TARGET=x86_64-unknown-uefi
26PROFILE=release
27OUT_DIR="target/${TARGET}/${PROFILE}"
28
29BOOTLOADER="$BUILD_DIR/bootloader/BOOTX64.EFI"
30KERNEL="$OUT_DIR/rsdos.efi"
31BZIMAGE="$ESP_DIR/bzImage"
32INITRD="$ESP_DIR/initramfs.cpio.gz"
33BOOT_EFI="$ESP_DIR/EFI/BOOT/BOOTX64.EFI"
34
35ESP_START=2048 # 1 MiB offset (standard alignment)
36ESP_SIZE_MB=64
37
38IMAGE_SIZE_MB=$((ESP_SIZE_MB + 2)) # ESP + GPT gap + backup
39
40# ---- checks ----
41for cmd in sfdisk mkfs.vfat mcopy truncate dd; do
42 if ! command -v "$cmd" &>/dev/null; then
43 echo "Error: $cmd not found"
44 exit 1
45 fi
46done
47
48# ---- build kernel ----
49echo "[1/4] Building kernel..."
50cargo build --release --package rsdos --target x86_64-unknown-uefi ${CARGO_FLAGS:-}
51
52# ---- build bootloader ----
53echo "[2/4] Building bootloader..."
54if [ ! -f "$BOOTLOADER" ]; then
55 "$SCRIPT_DIR/build-bootloader.sh"
56fi
57
58# ---- assemble ESP ----
59echo "[3/4] Assembling ESP..."
60mkdir -p "$ESP_DIR/EFI/BOOT"
61cp "$KERNEL" "$BZIMAGE"
62cp "$BOOTLOADER" "$BOOT_EFI"
63if [ ! -f "$INITRD" ]; then
64 printf '' | gzip >"$INITRD"
65fi
66
67# ---- create disk image ----
68echo "[4/4] Writing disk image..."
69rm -f "$IMAGE"
70truncate -s "${IMAGE_SIZE_MB}M" "$IMAGE"
71
72# GPT partition table: single EFI System partition
73sfdisk --quiet "$IMAGE" <<EOF
74label: gpt
75type=U, start=$ESP_START, size=+
76EOF
77
78# Build FAT32 image with ESP contents
79ESP_IMG=$(mktemp)
80truncate -s "${ESP_SIZE_MB}M" "$ESP_IMG"
81mkfs.vfat -F 32 -n "RS-DOS" "$ESP_IMG" >/dev/null 2>&1
82
83# Copy files into FAT
84mmd -i "$ESP_IMG" ::/EFI 2>/dev/null || true
85mmd -i "$ESP_IMG" ::/EFI/BOOT 2>/dev/null || true
86mcopy -i "$ESP_IMG" "$BOOT_EFI" ::/EFI/BOOT/BOOTX64.EFI
87mcopy -i "$ESP_IMG" "$BZIMAGE" ::/bzImage 2>/dev/null || true
88mcopy -i "$ESP_IMG" "$INITRD" ::/initramfs.cpio.gz 2>/dev/null || true
89
90# Inject FAT into image at partition offset
91dd if="$ESP_IMG" of="$IMAGE" bs=512 seek="$ESP_START" conv=notrunc 2>/dev/null
92rm -f "$ESP_IMG"
93
94echo ""
95echo "=== Done: $IMAGE ==="
96ls -lh "$IMAGE"
97echo ""
98echo "Boot with: ./scripts/boot-image.sh"
99echo "USB: sudo dd if=$IMAGE of=/dev/sdX bs=4M status=progress"