3fd4403ecb
Beautification pass on the text UI (all pure ASCII so it renders identically on
the UEFI Unicode console and the VGA CP437 console, which truncates chars to
bytes):
- Os::text_columns(): new trait method to query the real console width.
BIOS returns the VGA width; UEFI queries SimpleTextOutput QueryMode for the
current mode's columns (both fall back to 80). Enables true centering.
- draw_header now draws a full-width frame around a centered figlet "RedBear
OS" wordmark, with a centered version/platform subtitle, in brand red/cyan.
Falls back to a plain centered name on consoles narrower than the wordmark.
- Loading progress lines (live / kernel / initfs) gain a fixed-width ASCII
progress bar next to the MiB counters, and turn green on completion.
- A centered green "Booting RedBear OS..." closes the loading screen.
Verified: cargo check passes for x86_64-unknown-uefi (--bin) and
x86-unknown-none (--lib).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
938 lines
30 KiB
Rust
938 lines
30 KiB
Rust
#![no_std]
|
|
#![cfg_attr(any(target_arch = "riscv64", target_os = "uefi"), no_main)]
|
|
|
|
extern crate alloc;
|
|
|
|
#[cfg(any(target_arch = "riscv64", target_os = "uefi"))]
|
|
#[macro_use]
|
|
extern crate uefi_std as std;
|
|
|
|
use alloc::{format, string::String, vec::Vec};
|
|
use core::{
|
|
convert::TryFrom,
|
|
fmt::{self, Write},
|
|
mem, ptr, slice, str,
|
|
};
|
|
use redoxfs::{Disk, Node, TreeData};
|
|
|
|
use self::arch::{paging_create, paging_framebuffer};
|
|
use self::os::{Os, OsHwDesc, OsKey, OsMemoryEntry, OsMemoryKind, OsVideoMode, TextColor};
|
|
|
|
#[macro_use]
|
|
mod os;
|
|
|
|
mod arch;
|
|
mod editor;
|
|
mod logger;
|
|
mod serial_16550;
|
|
|
|
const KIBI: usize = 1024;
|
|
const MIBI: usize = KIBI * KIBI;
|
|
|
|
//TODO: allocate this in a more reasonable manner
|
|
static mut AREAS: [OsMemoryEntry; 1024] = [OsMemoryEntry {
|
|
base: 0,
|
|
size: 0,
|
|
kind: OsMemoryKind::Null,
|
|
}; 1024];
|
|
static mut AREAS_LEN: usize = 0;
|
|
|
|
pub fn area_add(area: OsMemoryEntry) {
|
|
#[allow(static_mut_refs)]
|
|
unsafe {
|
|
for existing_area in &mut AREAS[0..AREAS_LEN] {
|
|
if existing_area.kind == area.kind {
|
|
if existing_area.base.unchecked_add(existing_area.size) == area.base {
|
|
existing_area.size += area.size;
|
|
return;
|
|
}
|
|
if area.base.unchecked_add(area.size) == existing_area.base {
|
|
existing_area.size += area.size;
|
|
existing_area.base = area.base;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
*AREAS.get_mut(AREAS_LEN).expect("AREAS overflowed!") = area;
|
|
AREAS_LEN += 1;
|
|
}
|
|
}
|
|
|
|
pub static mut KERNEL_64BIT: bool = false;
|
|
|
|
pub static mut LIVE_OPT: Option<(u64, &'static [u8])> = None;
|
|
|
|
struct SliceWriter<'a> {
|
|
slice: &'a mut [u8],
|
|
i: usize,
|
|
}
|
|
|
|
impl<'a> Write for SliceWriter<'a> {
|
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
|
for b in s.bytes() {
|
|
if let Some(slice_b) = self.slice.get_mut(self.i) {
|
|
*slice_b = b;
|
|
self.i += 1;
|
|
} else {
|
|
return Err(fmt::Error);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug)]
|
|
#[repr(C, packed(8))]
|
|
pub struct KernelArgs {
|
|
kernel_base: u64,
|
|
kernel_size: u64,
|
|
stack_base: u64,
|
|
stack_size: u64,
|
|
env_base: u64,
|
|
env_size: u64,
|
|
|
|
/// The base pointer to the saved RSDP.
|
|
///
|
|
/// This field can be NULL, and if so, the system has not booted with UEFI or in some other way
|
|
/// retrieved the RSDPs. The kernel or a userspace driver will thus try searching the BIOS
|
|
/// memory instead. On UEFI systems, searching is not guaranteed to actually work though.
|
|
acpi_rsdp_base: u64,
|
|
/// The size of the RSDP region.
|
|
acpi_rsdp_size: u64,
|
|
|
|
areas_base: u64,
|
|
areas_size: u64,
|
|
|
|
bootstrap_base: u64,
|
|
bootstrap_size: u64,
|
|
}
|
|
|
|
fn select_mode(
|
|
os: &impl Os,
|
|
output_i: usize,
|
|
live: &mut bool,
|
|
edit_env: &mut bool,
|
|
) -> Option<OsVideoMode> {
|
|
const DEFAULT_WIDTH: u32 = 1280;
|
|
const DEFAULT_HEIGHT: u32 = 720;
|
|
const AUTOBOOT_SECONDS: usize = 5;
|
|
|
|
let mut all_modes = Vec::new();
|
|
for mode in os.video_modes(output_i) {
|
|
all_modes.push(mode);
|
|
}
|
|
|
|
if all_modes.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
all_modes.sort_by(|a, b| (b.width * b.height).cmp(&(a.width * a.height)));
|
|
|
|
fn categorize(width: u32, height: u32) -> u8 {
|
|
match (width, height) {
|
|
(w, _) if w >= 3840 => 0,
|
|
(w, _) if w >= 2560 => 1,
|
|
(1920, 1080) => 2,
|
|
(1280, 720) => 3,
|
|
(1024, 768) => 4,
|
|
_ => 5,
|
|
}
|
|
}
|
|
|
|
let category_labels = ["4K", "2.5K", "FullHD", "HD", "1024x768", "Other"];
|
|
|
|
struct MenuEntry {
|
|
label: String,
|
|
mode: Option<OsVideoMode>,
|
|
is_more: bool,
|
|
}
|
|
|
|
let mut entries: Vec<MenuEntry> = Vec::new();
|
|
|
|
for cat in 0..5 {
|
|
let mut cat_modes: Vec<&OsVideoMode> = all_modes
|
|
.iter()
|
|
.filter(|m| categorize(m.width, m.height) == cat)
|
|
.collect();
|
|
if cat_modes.is_empty() {
|
|
continue;
|
|
}
|
|
cat_modes.sort_by(|a, b| (b.width * b.height).cmp(&(a.width * a.height)));
|
|
for m in cat_modes {
|
|
let label = if cat == 3 || cat == 2 || cat == 4 {
|
|
format!("{}", category_labels[cat as usize])
|
|
} else {
|
|
format!("{} ({}x{})", category_labels[cat as usize], m.width, m.height)
|
|
};
|
|
entries.push(MenuEntry {
|
|
label,
|
|
mode: Some(*m),
|
|
is_more: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
let obscure_count = all_modes
|
|
.iter()
|
|
.filter(|m| categorize(m.width, m.height) == 5)
|
|
.count();
|
|
if obscure_count > 0 {
|
|
entries.push(MenuEntry {
|
|
label: format!("More resolutions ({} more)", obscure_count),
|
|
mode: None,
|
|
is_more: true,
|
|
});
|
|
}
|
|
|
|
if entries.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Pick the initial highlight: prefer the Red Bear default resolution
|
|
// (1280x720); if the panel does not offer it, fall back to the display's
|
|
// EDID-preferred mode (best_resolution) so real hardware still boots at its
|
|
// native resolution; only then fall back to entries[0] (the largest mode).
|
|
let default_idx = entries
|
|
.iter()
|
|
.position(|e| {
|
|
e.mode
|
|
.map(|m| m.width == DEFAULT_WIDTH && m.height == DEFAULT_HEIGHT)
|
|
.unwrap_or(false)
|
|
})
|
|
.or_else(|| {
|
|
os.best_resolution(output_i).and_then(|(bw, bh)| {
|
|
entries.iter().position(|e| {
|
|
e.mode.map(|m| m.width == bw && m.height == bh).unwrap_or(false)
|
|
})
|
|
})
|
|
})
|
|
.unwrap_or(0);
|
|
|
|
os.set_text_color(TextColor::Cyan);
|
|
println!("Output {}", output_i);
|
|
os.set_text_color(TextColor::Default);
|
|
|
|
let selected_mode = |entries: &[MenuEntry], idx: usize| -> Option<OsVideoMode> {
|
|
entries[idx].mode
|
|
};
|
|
|
|
let mut selected = default_idx;
|
|
let mut countdown = AUTOBOOT_SECONDS;
|
|
let mut countdown_active = true;
|
|
|
|
// Capture the countdown line's row now, so the in-loop countdown updates it
|
|
// in place. Deriving it from list_y with a fixed offset landed one row too
|
|
// low (on the "Use Up/Down" line), leaving this header as a stale second
|
|
// "Autobooting in X seconds" copy above the live one.
|
|
let countdown_y = os.get_text_position().1;
|
|
os.set_text_color(TextColor::Yellow);
|
|
println!(
|
|
" Autobooting in {} seconds (press any key to cancel)",
|
|
countdown
|
|
);
|
|
os.set_text_color(TextColor::Default);
|
|
println!(" Use Up/Down to navigate, Enter to select.");
|
|
let live_y = {
|
|
if *live {
|
|
println!(" [L] Live mode: ON");
|
|
} else {
|
|
println!(" [L] Live mode: OFF");
|
|
}
|
|
os.get_text_position().1
|
|
};
|
|
println!(" [E] Edit boot environment");
|
|
println!();
|
|
|
|
let (list_x, list_y) = os.get_text_position();
|
|
|
|
loop {
|
|
for (i, entry) in entries.iter().enumerate() {
|
|
os.set_text_position(list_x, list_y + i);
|
|
os.set_text_highlight(i == selected);
|
|
let marker = if i == selected { ">" } else { " " };
|
|
let default_tag = if entries[i]
|
|
.mode
|
|
.map(|m| m.width == DEFAULT_WIDTH && m.height == DEFAULT_HEIGHT)
|
|
.unwrap_or(false)
|
|
{
|
|
" [DEFAULT]"
|
|
} else {
|
|
""
|
|
};
|
|
// Pad to a fixed width: the UEFI text console does not clear old
|
|
// characters when a shorter line overwrites a longer one at the
|
|
// same position, so without padding the menu garbles as the
|
|
// highlight/marker/tag change length between frames.
|
|
print!("{:<50}", format!("{} {}{}", marker, entry.label, default_tag));
|
|
}
|
|
os.set_text_highlight(false);
|
|
|
|
os.set_text_position(0, countdown_y);
|
|
if countdown_active {
|
|
os.set_text_color(TextColor::Yellow);
|
|
let msg = format!(
|
|
" Autobooting in {} seconds (press any key to cancel)",
|
|
countdown
|
|
);
|
|
print!("{:<70}", msg);
|
|
os.set_text_color(TextColor::Default);
|
|
} else {
|
|
print!("{:<70}", " Manual selection - press Enter to boot");
|
|
}
|
|
|
|
match if countdown_active {
|
|
os.get_key_timeout(1000)
|
|
} else {
|
|
os.get_key()
|
|
} {
|
|
OsKey::Timeout => {
|
|
if countdown_active {
|
|
if countdown == 0 {
|
|
if let Some(mode) = selected_mode(&entries, selected) {
|
|
return Some(mode);
|
|
}
|
|
countdown_active = false;
|
|
} else {
|
|
countdown -= 1;
|
|
}
|
|
}
|
|
}
|
|
OsKey::Up => {
|
|
countdown_active = false;
|
|
if selected > 0 {
|
|
selected -= 1;
|
|
}
|
|
}
|
|
OsKey::Down => {
|
|
countdown_active = false;
|
|
if selected + 1 < entries.len() {
|
|
selected += 1;
|
|
}
|
|
}
|
|
OsKey::Enter => {
|
|
if entries[selected].is_more {
|
|
return select_obscure(os, &all_modes, live, edit_env);
|
|
}
|
|
if let Some(mode) = selected_mode(&entries, selected) {
|
|
return Some(mode);
|
|
}
|
|
}
|
|
OsKey::Char('l') => {
|
|
countdown_active = false;
|
|
*live = !*live;
|
|
os.set_text_position(0, live_y);
|
|
let msg = if *live {
|
|
" [L] Live mode: ON"
|
|
} else {
|
|
" [L] Live mode: OFF"
|
|
};
|
|
print!("{:<40}", msg);
|
|
}
|
|
OsKey::Char('e') => {
|
|
countdown_active = false;
|
|
if let Some(mode) = selected_mode(&entries, selected) {
|
|
*edit_env = true;
|
|
return Some(mode);
|
|
}
|
|
}
|
|
_ => {
|
|
countdown_active = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn select_obscure(
|
|
os: &impl Os,
|
|
all_modes: &[OsVideoMode],
|
|
live: &mut bool,
|
|
edit_env: &mut bool,
|
|
) -> Option<OsVideoMode> {
|
|
let obscure: Vec<&OsVideoMode> = all_modes
|
|
.iter()
|
|
.filter(|m| match (m.width, m.height) {
|
|
(w, _) if w >= 3840 => false,
|
|
(w, _) if w >= 2560 => false,
|
|
(1920, 1080) => false,
|
|
(1280, 720) => false,
|
|
(1024, 768) => false,
|
|
_ => true,
|
|
})
|
|
.collect();
|
|
|
|
if obscure.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
os.clear_text();
|
|
os.set_text_color(TextColor::Cyan);
|
|
println!("More Resolutions");
|
|
os.set_text_color(TextColor::Default);
|
|
println!(" Use Up/Down to navigate, Enter to select, Backspace to go back");
|
|
println!();
|
|
|
|
let (_, list_y) = os.get_text_position();
|
|
let mut selected = 0;
|
|
|
|
loop {
|
|
for (i, mode) in obscure.iter().enumerate() {
|
|
os.set_text_position(0, list_y + i);
|
|
os.set_text_highlight(i == selected);
|
|
let marker = if i == selected { ">" } else { " " };
|
|
// Pad to a fixed width (see select_mode): prevents UEFI console
|
|
// garbling as the highlight marker moves between rows.
|
|
print!("{:<40}", format!("{} {:>4}x{:<4}", marker, mode.width, mode.height));
|
|
}
|
|
os.set_text_highlight(false);
|
|
|
|
match os.get_key() {
|
|
OsKey::Up => {
|
|
if selected > 0 {
|
|
selected -= 1;
|
|
}
|
|
}
|
|
OsKey::Down => {
|
|
if selected + 1 < obscure.len() {
|
|
selected += 1;
|
|
}
|
|
}
|
|
OsKey::Backspace => {
|
|
return None;
|
|
}
|
|
OsKey::Enter => {
|
|
return Some(*obscure[selected]);
|
|
}
|
|
OsKey::Char('l') => {
|
|
*live = !*live;
|
|
}
|
|
OsKey::Char('e') => {
|
|
*edit_env = true;
|
|
return Some(*obscure[selected]);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
fn redoxfs<O: Os>(os: &O) -> (redoxfs::FileSystem<O::D>, Option<&'static [u8]>) {
|
|
let attempts = 10;
|
|
for attempt in 0..=attempts {
|
|
let mut password_opt = None;
|
|
if attempt > 0 {
|
|
print!("\rRedoxFS password ({}/{}): ", attempt, attempts);
|
|
|
|
let mut password = String::new();
|
|
|
|
loop {
|
|
match os.get_key() {
|
|
OsKey::Backspace | OsKey::Delete => {
|
|
if !password.is_empty() {
|
|
print!("\x08 \x08");
|
|
password.pop();
|
|
}
|
|
}
|
|
OsKey::Char(c) => {
|
|
print!("*");
|
|
password.push(c)
|
|
}
|
|
OsKey::Enter => break,
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
// Erase password information
|
|
while os.get_text_position().0 > 0 {
|
|
print!("\x08 \x08");
|
|
}
|
|
|
|
if !password.is_empty() {
|
|
password_opt = Some(password);
|
|
}
|
|
}
|
|
match os.filesystem(password_opt.as_ref().map(|x| x.as_bytes())) {
|
|
Ok(fs) => {
|
|
return (
|
|
fs,
|
|
password_opt.map(|password| {
|
|
// Copy password to page aligned memory
|
|
let password_size = password.len();
|
|
let password_base = os.alloc_zeroed_page_aligned(password_size);
|
|
|
|
area_add(OsMemoryEntry {
|
|
base: password_base as u64,
|
|
size: password_size as u64,
|
|
kind: OsMemoryKind::Reserved,
|
|
});
|
|
|
|
unsafe {
|
|
ptr::copy(password.as_ptr(), password_base, password_size);
|
|
slice::from_raw_parts(password_base, password_size)
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
Err(err) => match err.errno {
|
|
// Incorrect password, try again
|
|
syscall::ENOKEY => (),
|
|
_ => {
|
|
panic!("Failed to open RedoxFS: {}", err);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
panic!("RedoxFS out of unlock attempts");
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
enum Filetype {
|
|
Elf,
|
|
Initfs,
|
|
}
|
|
fn load_to_memory<O: Os>(
|
|
os: &O,
|
|
fs: &mut redoxfs::FileSystem<O::D>,
|
|
path: &str,
|
|
filetype: Filetype,
|
|
) -> &'static mut [u8] {
|
|
fs.tx(|tx| {
|
|
let mut node = None;
|
|
for component in path.split('/') {
|
|
node = Some(
|
|
tx.find_node(
|
|
node.map_or(redoxfs::TreePtr::root(), |node: TreeData<Node>| node.ptr()),
|
|
component,
|
|
)
|
|
.unwrap_or_else(|err| panic!("Failed to find {component}: {err}")),
|
|
);
|
|
}
|
|
let node = node.unwrap();
|
|
|
|
let size = node.data().size();
|
|
|
|
print!(" {}: {} 0/{} MiB", path, progress_bar(0, size), size / MIBI as u64);
|
|
|
|
let ptr = os.alloc_zeroed_page_aligned(size as usize);
|
|
if ptr.is_null() {
|
|
panic!("Failed to allocate memory for {}", path);
|
|
}
|
|
|
|
let slice = unsafe { slice::from_raw_parts_mut(ptr, size as usize) };
|
|
|
|
let mut i = 0;
|
|
for chunk in slice.chunks_mut(MIBI) {
|
|
print!("\r {}: {} {}/{} MiB", path, progress_bar(i, size), i / MIBI as u64, size / MIBI as u64);
|
|
i += tx
|
|
.read_node_inner(&node, i, chunk)
|
|
.unwrap_or_else(|err| panic!("Failed to read `{}` file: {}", path, err))
|
|
as u64;
|
|
}
|
|
os.set_text_color(TextColor::Green);
|
|
println!("\r {}: {} {}/{} MiB", path, progress_bar(size, size), size / MIBI as u64, size / MIBI as u64);
|
|
os.set_text_color(TextColor::Default);
|
|
|
|
if filetype == Filetype::Elf {
|
|
let magic = &slice[..4];
|
|
if magic != b"\x7FELF" {
|
|
panic!("{} has invalid magic number {:#X?}", path, magic);
|
|
}
|
|
} else if filetype == Filetype::Initfs {
|
|
let magic = &slice[..8];
|
|
if magic != b"RedoxFtw" {
|
|
panic!("{} has invalid magic number {:#X?}", path, magic);
|
|
}
|
|
}
|
|
|
|
Ok(slice)
|
|
})
|
|
.unwrap_or_else(|err| {
|
|
panic!(
|
|
"RedoxFS transaction failed while loading `{}`: {}",
|
|
path, err
|
|
)
|
|
})
|
|
}
|
|
|
|
fn elf_entry(data: &[u8]) -> (u64, bool) {
|
|
match (data[4], data[5]) {
|
|
// 32-bit, little endian
|
|
(1, 1) => (
|
|
u32::from_le_bytes(
|
|
<[u8; 4]>::try_from(&data[0x18..0x18 + 4]).expect("conversion cannot fail"),
|
|
) as u64,
|
|
false,
|
|
),
|
|
// 32-bit, big endian
|
|
(1, 2) => (
|
|
u32::from_be_bytes(
|
|
<[u8; 4]>::try_from(&data[0x18..0x18 + 4]).expect("conversion cannot fail"),
|
|
) as u64,
|
|
false,
|
|
),
|
|
// 64-bit, little endian
|
|
(2, 1) => (
|
|
u64::from_le_bytes(
|
|
<[u8; 8]>::try_from(&data[0x18..0x18 + 8]).expect("conversion cannot fail"),
|
|
),
|
|
true,
|
|
),
|
|
// 64-bit, big endian
|
|
(2, 2) => (
|
|
u64::from_be_bytes(
|
|
<[u8; 8]>::try_from(&data[0x18..0x18 + 8]).expect("conversion cannot fail"),
|
|
),
|
|
true,
|
|
),
|
|
(ei_class, ei_data) => {
|
|
panic!("Unsupported ELF EI_CLASS {} EI_DATA {}", ei_class, ei_data);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ASCII wordmark (figlet "standard"), pure ASCII so it renders identically on
|
|
// the UEFI Unicode console and the VGA CP437 text console (which truncates each
|
|
// char to a byte). Keep this ASCII-only.
|
|
const WORDMARK: &[&str] = &[
|
|
" ____ _ ____ ___ ____",
|
|
"| _ \\ ___ __| | __ ) ___ __ _ _ __ / _ \\/ ___|",
|
|
"| |_) / _ \\/ _` | _ \\ / _ \\/ _` | '__| | | | \\___ \\",
|
|
"| _ < __/ (_| | |_) | __/ (_| | | | |_| |___) |",
|
|
"|_| \\_\\___|\\__,_|____/ \\___|\\__,_|_| \\___/|____/",
|
|
];
|
|
const WORDMARK_W: usize = 53;
|
|
|
|
/// Return `text` centered within `width` columns, padded with spaces on both
|
|
/// sides to exactly `width` characters (truncated if it does not fit).
|
|
fn center_in(width: usize, text: &str) -> String {
|
|
let tlen = text.chars().count();
|
|
if tlen >= width {
|
|
return text.chars().take(width).collect();
|
|
}
|
|
let pad = width - tlen;
|
|
let left = pad / 2;
|
|
let mut s = String::with_capacity(width);
|
|
for _ in 0..left {
|
|
s.push(' ');
|
|
}
|
|
s.push_str(text);
|
|
for _ in 0..(pad - left) {
|
|
s.push(' ');
|
|
}
|
|
s
|
|
}
|
|
|
|
/// A fixed-width ASCII progress bar like `[##### ]` for `cur`/`total`.
|
|
fn progress_bar(cur: u64, total: u64) -> String {
|
|
const BAR_W: usize = 20;
|
|
let filled = if total == 0 {
|
|
BAR_W
|
|
} else {
|
|
((core::cmp::min(cur, total) as u128 * BAR_W as u128) / total as u128) as usize
|
|
};
|
|
let mut s = String::with_capacity(BAR_W + 2);
|
|
s.push('[');
|
|
for i in 0..BAR_W {
|
|
s.push(if i < filled { '#' } else { ' ' });
|
|
}
|
|
s.push(']');
|
|
s
|
|
}
|
|
|
|
/// Clear the screen and draw the Red Bear OS bootloader header: a full-width
|
|
/// frame around the centered ASCII wordmark, with a centered version/platform
|
|
/// subtitle below.
|
|
///
|
|
/// Called at the start of each phase (filesystem, resolution menu, loading) so
|
|
/// every phase renders on a clean, consistently-branded screen and no phase
|
|
/// overprints another. Previously only the second-and-later video outputs were
|
|
/// cleared, so on a single display the menu was drawn under the header and the
|
|
/// loading progress was drawn on top of the still-visible menu (e.g. "live:"
|
|
/// landing on the "Autobooting" line).
|
|
fn draw_header(os: &impl Os) {
|
|
let w = os.text_columns().max(20);
|
|
os.clear_text();
|
|
os.set_text_position(0, 0);
|
|
|
|
os.set_text_color(TextColor::Red);
|
|
if w >= WORDMARK_W + 2 {
|
|
let inner = w - 2;
|
|
let border = format!("+{}+", "=".repeat(inner));
|
|
println!("{}", border);
|
|
for line in WORDMARK {
|
|
println!("|{}|", center_in(inner, line));
|
|
}
|
|
println!("{}", border);
|
|
} else {
|
|
// Narrow console: skip the wordmark, just center the name.
|
|
println!("{}", center_in(w, "RedBear OS"));
|
|
}
|
|
|
|
os.set_text_color(TextColor::Cyan);
|
|
let subtitle = format!("Bootloader {} ({})", env!("CARGO_PKG_VERSION"), os.name());
|
|
println!("{}", center_in(w, &subtitle));
|
|
os.set_text_color(TextColor::Default);
|
|
println!();
|
|
}
|
|
|
|
/// Print the RedoxFS identity line (uuid + size) below the current cursor.
|
|
fn print_fs_line<D: Disk>(fs: &redoxfs::FileSystem<D>) {
|
|
print!(" RedoxFS ");
|
|
for i in 0..fs.header.uuid().len() {
|
|
if i == 4 || i == 6 || i == 8 || i == 10 {
|
|
print!("-");
|
|
}
|
|
print!("{:>02x}", fs.header.uuid()[i]);
|
|
}
|
|
println!(": {} MiB", fs.header.size() / MIBI as u64);
|
|
}
|
|
|
|
fn main(os: &impl Os) -> (usize, u64, KernelArgs) {
|
|
// Phase 1: open the filesystem. redoxfs() prompts for a password on this
|
|
// header screen if the disk is encrypted.
|
|
draw_header(os);
|
|
|
|
let hwdesc = os.hwdesc();
|
|
let (acpi_rsdp_base, acpi_rsdp_size) = match hwdesc {
|
|
OsHwDesc::Acpi(base, size) => (base, size),
|
|
OsHwDesc::DeviceTree(base, size) => (base, size),
|
|
OsHwDesc::NotFound => (0, 0),
|
|
};
|
|
|
|
let (mut fs, password_opt) = redoxfs(os);
|
|
|
|
// Phase 2: resolution selection — a clean, titled screen per video output.
|
|
let mut mode_opts = Vec::new();
|
|
let mut live = cfg!(feature = "live");
|
|
let mut edit_env = false;
|
|
for output_i in 0..os.video_outputs() {
|
|
draw_header(os);
|
|
mode_opts.push(select_mode(os, output_i, &mut live, &mut edit_env));
|
|
}
|
|
|
|
let stack_size = 128 * KIBI;
|
|
let stack_base = os.alloc_zeroed_page_aligned(stack_size);
|
|
if stack_base.is_null() {
|
|
panic!("Failed to allocate memory for stack");
|
|
}
|
|
|
|
// Phase 3: load kernel + initfs (and optionally preload the live image) on a
|
|
// fresh titled screen, so loading progress never overprints the menu.
|
|
draw_header(os);
|
|
print_fs_line(&fs);
|
|
|
|
let live_opt = if live {
|
|
let size = fs.header.size();
|
|
|
|
print!(" live: {} 0/{} MiB", progress_bar(0, size), size / MIBI as u64);
|
|
|
|
let live_size = match usize::try_from(size) {
|
|
Ok(live_size) => live_size,
|
|
Err(_) => {
|
|
println!("\r live: disabled (image too large for bootloader address space)");
|
|
live = false;
|
|
0
|
|
}
|
|
};
|
|
|
|
let ptr = if live {
|
|
os.alloc_zeroed_page_aligned(live_size)
|
|
} else {
|
|
ptr::null_mut()
|
|
};
|
|
if live && ptr.is_null() {
|
|
println!(
|
|
"\r live: disabled (unable to allocate {} MiB upfront)",
|
|
size / MIBI as u64
|
|
);
|
|
live = false;
|
|
}
|
|
|
|
let live = if live {
|
|
Some(unsafe { slice::from_raw_parts_mut(ptr, live_size) })
|
|
} else {
|
|
println!(" Continuing without live preload");
|
|
None
|
|
};
|
|
|
|
if let Some(live) = live {
|
|
let mut i = 0;
|
|
for chunk in live.chunks_mut(MIBI) {
|
|
print!("\r live: {} {}/{} MiB", progress_bar(i, size), i / MIBI as u64, size / MIBI as u64);
|
|
i += unsafe {
|
|
fs.disk
|
|
.read_at(fs.block + i / redoxfs::BLOCK_SIZE, chunk)
|
|
.expect("Failed to read live disk") as u64
|
|
};
|
|
}
|
|
os.set_text_color(TextColor::Green);
|
|
println!("\r live: {} {}/{} MiB", progress_bar(size, size), size / MIBI as u64, size / MIBI as u64);
|
|
os.set_text_color(TextColor::Default);
|
|
|
|
println!(" Switching to live disk");
|
|
unsafe {
|
|
LIVE_OPT = Some((fs.block, slice::from_raw_parts_mut(ptr, live_size)));
|
|
}
|
|
|
|
area_add(OsMemoryEntry {
|
|
base: live.as_ptr() as u64,
|
|
size: live.len() as u64,
|
|
kind: OsMemoryKind::Reserved,
|
|
});
|
|
|
|
Some(live)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let (kernel, kernel_entry) = {
|
|
let kernel = load_to_memory(os, &mut fs, "usr/lib/boot/kernel", Filetype::Elf);
|
|
let (kernel_entry, kernel_64bit) = elf_entry(kernel);
|
|
unsafe {
|
|
KERNEL_64BIT = kernel_64bit;
|
|
}
|
|
(kernel, kernel_entry)
|
|
};
|
|
|
|
let (bootstrap_size, bootstrap_base) = {
|
|
let initfs_slice = load_to_memory(os, &mut fs, "usr/lib/boot/initfs", Filetype::Initfs);
|
|
|
|
let memory = unsafe {
|
|
let total_size = initfs_slice.len().next_multiple_of(4096);
|
|
let ptr = os.alloc_zeroed_page_aligned(total_size);
|
|
assert!(!ptr.is_null(), "failed to allocate bootstrap+initfs memory");
|
|
core::slice::from_raw_parts_mut(ptr, total_size)
|
|
};
|
|
memory[..initfs_slice.len()].copy_from_slice(initfs_slice);
|
|
|
|
(memory.len() as u64, memory.as_mut_ptr() as u64)
|
|
};
|
|
|
|
let page_phys = unsafe { paging_create(os, kernel.as_ptr() as u64, kernel.len() as u64) }
|
|
.expect("Failed to set up paging");
|
|
|
|
let max_env_size = 64 * KIBI;
|
|
let mut env_size = max_env_size;
|
|
let env_base = os.alloc_zeroed_page_aligned(env_size);
|
|
if env_base.is_null() {
|
|
panic!("Failed to allocate memory for stack");
|
|
}
|
|
|
|
{
|
|
let mut w = SliceWriter {
|
|
slice: unsafe { slice::from_raw_parts_mut(env_base, max_env_size) },
|
|
i: 0,
|
|
};
|
|
|
|
match hwdesc {
|
|
OsHwDesc::Acpi(addr, size) => {
|
|
writeln!(w, "RSDP_ADDR={addr:016x}").unwrap();
|
|
writeln!(w, "RSDP_SIZE={size:016x}").unwrap();
|
|
}
|
|
OsHwDesc::DeviceTree(addr, size) => {
|
|
writeln!(w, "DTB_ADDR={addr:016x}").unwrap();
|
|
writeln!(w, "DTB_SIZE={size:016x}").unwrap();
|
|
}
|
|
OsHwDesc::NotFound => {}
|
|
}
|
|
|
|
if let Some(live) = live_opt {
|
|
writeln!(w, "DISK_LIVE_ADDR={:016x}", live.as_ptr() as usize).unwrap();
|
|
writeln!(w, "DISK_LIVE_SIZE={:016x}", live.len()).unwrap();
|
|
writeln!(w, "REDOXFS_BLOCK={:016x}", 0).unwrap();
|
|
} else {
|
|
writeln!(w, "REDOXFS_BLOCK={:016x}", fs.block).unwrap();
|
|
}
|
|
write!(w, "REDOXFS_UUID=").unwrap();
|
|
for i in 0..fs.header.uuid().len() {
|
|
if i == 4 || i == 6 || i == 8 || i == 10 {
|
|
write!(w, "-").unwrap();
|
|
}
|
|
|
|
write!(w, "{:>02x}", fs.header.uuid()[i]).unwrap();
|
|
}
|
|
writeln!(w).unwrap();
|
|
if let Some(password) = password_opt {
|
|
writeln!(
|
|
w,
|
|
"REDOXFS_PASSWORD_ADDR={:016x}",
|
|
password.as_ptr() as usize
|
|
)
|
|
.unwrap();
|
|
writeln!(w, "REDOXFS_PASSWORD_SIZE={:016x}", password.len()).unwrap();
|
|
}
|
|
|
|
#[cfg(target_arch = "riscv64")]
|
|
{
|
|
let boot_hartid = os::efi_get_boot_hartid()
|
|
.expect("Could not retrieve boot hart id from EFI implementation!");
|
|
writeln!(w, "BOOT_HART_ID={:016x}", boot_hartid).unwrap();
|
|
}
|
|
if edit_env {
|
|
editor::edit_env(os, env_base, &mut w.i, max_env_size);
|
|
}
|
|
|
|
for output_i in 0..os.video_outputs() {
|
|
if let Some(mut mode) = mode_opts[output_i] {
|
|
// Set mode to get updated values
|
|
os.set_video_mode(output_i, &mut mode);
|
|
|
|
if output_i == 0 {
|
|
let virt = unsafe {
|
|
paging_framebuffer(
|
|
os,
|
|
page_phys,
|
|
mode.base,
|
|
(mode.stride * mode.height * 4) as u64,
|
|
)
|
|
}
|
|
.expect("Failed to map framebuffer");
|
|
|
|
writeln!(w, "FRAMEBUFFER_ADDR={:016x}", mode.base).unwrap();
|
|
writeln!(w, "FRAMEBUFFER_VIRT={virt:016x}").unwrap();
|
|
writeln!(w, "FRAMEBUFFER_WIDTH={:016x}", mode.width).unwrap();
|
|
writeln!(w, "FRAMEBUFFER_HEIGHT={:016x}", mode.height).unwrap();
|
|
writeln!(w, "FRAMEBUFFER_STRIDE={:016x}", mode.stride).unwrap();
|
|
} else {
|
|
writeln!(
|
|
w,
|
|
"FRAMEBUFFER{}={:#x},{},{},{}",
|
|
output_i, mode.base, mode.width, mode.height, mode.stride,
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
env_size = w.i;
|
|
}
|
|
|
|
os.set_text_color(TextColor::Green);
|
|
println!();
|
|
println!(
|
|
"{}",
|
|
center_in(os.text_columns().max(20), "Booting RedBear OS...")
|
|
);
|
|
os.set_text_color(TextColor::Default);
|
|
|
|
#[allow(static_mut_refs)]
|
|
(
|
|
page_phys,
|
|
kernel_entry,
|
|
KernelArgs {
|
|
kernel_base: kernel.as_ptr() as u64,
|
|
kernel_size: kernel.len() as u64,
|
|
stack_base: stack_base as u64,
|
|
stack_size: stack_size as u64,
|
|
env_base: env_base as u64,
|
|
env_size: env_size as u64,
|
|
acpi_rsdp_base,
|
|
acpi_rsdp_size,
|
|
areas_base: unsafe { AREAS.as_ptr() as u64 },
|
|
areas_size: unsafe { (AREAS.len() * mem::size_of::<OsMemoryEntry>()) as u64 },
|
|
bootstrap_base,
|
|
bootstrap_size,
|
|
},
|
|
)
|
|
}
|