diff --git a/Makefile b/Makefile index db47930..7fe07d0 100644 --- a/Makefile +++ b/Makefile @@ -390,10 +390,12 @@ kernel: $(BUILD)/kernel/kernel # ----------------------------------------------------------------------- UEFI_CFLAGS = --target=x86_64-unknown-windows -ffreestanding -fshort-wchar -mno-red-zone -fno-stack-protector -Wall -Wextra -O1 -I./include -$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/schboot.h +$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/schboot.h \ + boot/common/ui.c boot/common/ui.h @mkdir -p $(BUILD)/uefi clang $(UEFI_CFLAGS) -c boot/uefi/main.c -o $(BUILD)/uefi/main.obj - lld-link -subsystem:efi_application -entry:EfiMain -nodefaultlib $(BUILD)/uefi/main.obj -out:$@ + clang $(UEFI_CFLAGS) -c boot/common/ui.c -o $(BUILD)/uefi/ui.obj + lld-link -subsystem:efi_application -entry:EfiMain -nodefaultlib $(BUILD)/uefi/main.obj $(BUILD)/uefi/ui.obj -out:$@ uefi: $(BUILD)/uefi/BOOTX64.EFI diff --git a/boot/common/ui.c b/boot/common/ui.c new file mode 100644 index 0000000..ee42f34 --- /dev/null +++ b/boot/common/ui.c @@ -0,0 +1,262 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* + * The loader's user interface. See ui.h for why this can be shared at all. + * + * The look is deliberately quiet: a dark ground, the mark centred, one thin + * bar beneath it, and no text that is not load-bearing. A loader is not a + * place to be interesting -- it is looked at for four seconds by someone who + * wants to be somewhere else, and the useful thing it can do is stay still and + * tell them it is working. + */ + +#include "ui.h" +#include "../../include/theme.h" +#include "../../include/font.h" +#include "../../include/logo.h" + +/* "Draw the glyph, leave the background alone." Not a colour anyone can mean: + * the palette is 24-bit, so the high byte is free for a sentinel. */ +#define SCH_BG_NONE 0xFF000000u + +static struct ui_fb FB; +static int ACTIVE; + +/* Progress state. Kept here rather than passed around because the caller ticks + * it from inside a read loop that has no business carrying UI state. */ +static ui_u32 PROG_TOTAL, PROG_DONE; +static int PROG_X, PROG_Y, PROG_W, PROG_H; +static int PROG_LAST_PX; /* pixels already filled */ +static int PROG_ACTIVE; + +/* Layout constants. One place, because a splash that is laid out by scattered + * literals is one nobody dares move. */ +#define LOGO_GAP 28 /* logo bottom to product name */ +#define NAME_GAP 22 /* product name to the bar */ +#define SUB_GAP 18 /* bar to the subtitle under it */ +#define BAR_W_FRAC 4 /* bar is width/4 ... */ +#define BAR_W_MIN 220 /* ... but never narrower than this */ +#define BAR_H 4 /* thin: it is a hint, not a gauge */ +#define GLYPH_W 8 +#define GLYPH_H 16 + +void +ui_init(const struct ui_fb *fb) +{ + ACTIVE = 0; + if (!fb || !fb->base || !fb->width || !fb->height) + return; + FB = *fb; + if (!FB.stride) + FB.stride = FB.width * 4; + ACTIVE = 1; +} + +int ui_active(void) { return ACTIVE; } +ui_u32 ui_width(void) { return ACTIVE ? FB.width : 0; } +ui_u32 ui_height(void) { return ACTIVE ? FB.height : 0; } + +static void +put(int x, int y, ui_u32 colour) +{ + if (!ACTIVE || x < 0 || y < 0 || (ui_u32)x >= FB.width || (ui_u32)y >= FB.height) + return; + *(ui_u32 *)(FB.base + (ui_u64)y * FB.stride + (ui_u64)x * 4) = colour; +} + +void +ui_fill_rect(int x, int y, int w, int h, ui_u32 colour) +{ + if (!ACTIVE) + return; + for (int r = 0; r < h; r++) { + int yy = y + r; + if (yy < 0 || (ui_u32)yy >= FB.height) + continue; + /* One row at a time through a row pointer: put() per pixel costs a + * bounds check and a multiply for every one of them, and this is used + * to clear a whole screen. */ + ui_u32 *row = (ui_u32 *)(FB.base + (ui_u64)yy * FB.stride); + for (int c = 0; c < w; c++) { + int xx = x + c; + if (xx >= 0 && (ui_u32)xx < FB.width) + row[xx] = colour; + } + } +} + +void +ui_clear(void) +{ + ui_fill_rect(0, 0, (int)FB.width, (int)FB.height, SCH_BG); +} + +static void +glyph(int x, int y, ui_u8 c, ui_u32 colour, ui_u32 bg) +{ + for (int row = 0; row < GLYPH_H; row++) { + ui_u8 bits = font_data[c][row]; + for (int col = 0; col < GLYPH_W; col++) { + if (bits & (0x80 >> col)) + put(x + col, y + row, colour); + else if (bg != SCH_BG_NONE) + put(x + col, y + row, bg); + } + } +} + +void +ui_text(int x, int y, const char *s, ui_u32 colour, ui_u32 bg) +{ + if (!ACTIVE || !s) + return; + for (int i = 0; s[i]; i++) + glyph(x + i * GLYPH_W, y, (ui_u8)s[i], colour, bg); +} + +static int +slen(const char *s) +{ + int n = 0; + while (s && s[n]) + n++; + return n; +} + +void +ui_text_centered(int y, const char *s, ui_u32 colour) +{ + if (!ACTIVE) + return; + int w = slen(s) * GLYPH_W; + ui_text(((int)FB.width - w) / 2, y, s, colour, SCH_BG_NONE); +} + +void +ui_logo(int y) +{ + if (!ACTIVE) + return; + + int x0 = ((int)FB.width - LOGO_W) / 2; + + /* The logo is a 4-bit coverage mask, not a bitmap: each nibble says how + * much of that pixel the mark covers, and the colour is blended against the + * background by that amount. Drawing it as on/off would give a mark with + * stepped edges that looks wrong at every size. */ + for (int y1 = 0; y1 < LOGO_H; y1++) { + for (int x1 = 0; x1 < LOGO_W; x1++) { + unsigned idx = (unsigned)y1 * LOGO_W + (unsigned)x1; + unsigned cov = (logo_mask[idx >> 1] >> ((idx & 1) ? 0 : 4)) & 0xF; + if (!cov) + continue; + + ui_u32 fg = SCH_FG, bg = SCH_BG; + ui_u32 r = (((fg >> 16) & 0xFF) * cov + ((bg >> 16) & 0xFF) * (15 - cov)) / 15; + ui_u32 g = (((fg >> 8) & 0xFF) * cov + ((bg >> 8) & 0xFF) * (15 - cov)) / 15; + ui_u32 b = (((fg ) & 0xFF) * cov + ((bg ) & 0xFF) * (15 - cov)) / 15; + put(x0 + x1, y + y1, (r << 16) | (g << 8) | b); + } + } +} + +/* Where the bar goes. Derived, not stored, so nothing can set it and then have + * the screen change underneath it. */ +static void +bar_geometry(void) +{ + PROG_W = (int)FB.width / BAR_W_FRAC; + if (PROG_W < BAR_W_MIN) + PROG_W = BAR_W_MIN; + if (PROG_W > (int)FB.width - 80) + PROG_W = (int)FB.width - 80; + PROG_H = BAR_H; + PROG_X = ((int)FB.width - PROG_W) / 2; + + /* Vertically: the whole group is centred, and the bar sits below the logo + * and the name. Anchoring to the centre rather than to the top is what + * keeps the arrangement identical from 800x600 up to 1600x1200. */ + int group_h = LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP + PROG_H; + int top = ((int)FB.height - group_h) / 2; + PROG_Y = top + LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP; +} + +void +ui_splash(const char *product, const char *subtitle) +{ + if (!ACTIVE) + return; + + ui_clear(); + bar_geometry(); + + int group_h = LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP + PROG_H; + int top = ((int)FB.height - group_h) / 2; + + ui_logo(top); + ui_text_centered(top + LOGO_H + LOGO_GAP, product, SCH_FG); + + if (subtitle) + ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM); +} + +void +ui_progress_begin(const char *label, ui_u32 total) +{ + if (!ACTIVE) + return; + + bar_geometry(); + PROG_TOTAL = total ? total : 1; + PROG_DONE = 0; + PROG_LAST_PX = 0; + PROG_ACTIVE = 1; + + /* The unfilled track. Drawn once: repainting it on every tick is what makes + * a progress bar flicker. */ + ui_fill_rect(PROG_X, PROG_Y, PROG_W, PROG_H, SCH_ACCENT_DIM); + if (label) + ui_progress_label(label); +} + +void +ui_progress_label(const char *label) +{ + if (!ACTIVE || !label) + return; + + /* Clear the line first. A shorter label after a longer one would otherwise + * leave the tail of the old one behind. */ + int y = PROG_Y + PROG_H + SUB_GAP; + ui_fill_rect(0, y, (int)FB.width, GLYPH_H, SCH_BG); + ui_text_centered(y, label, SCH_DIM); +} + +void +ui_progress_tick(ui_u32 done) +{ + if (!ACTIVE || !PROG_ACTIVE) + return; + + if (done > PROG_TOTAL) + done = PROG_TOTAL; + PROG_DONE = done; + + int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL); + + /* Only the newly filled part. Filling from zero each time is O(width) per + * tick and visibly slower than the thing it is reporting on. */ + if (px > PROG_LAST_PX) { + ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y, px - PROG_LAST_PX, PROG_H, + SCH_ACCENT); + PROG_LAST_PX = px; + } +} + +void +ui_progress_end(void) +{ + if (!ACTIVE || !PROG_ACTIVE) + return; + ui_progress_tick(PROG_TOTAL); + PROG_ACTIVE = 0; +} diff --git a/boot/common/ui.h b/boot/common/ui.h new file mode 100644 index 0000000..15d58de --- /dev/null +++ b/boot/common/ui.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* + * The loader's user interface, shared by both firmwares. + * + * This is the first piece of the two loaders that is genuinely common code + * rather than a contract they both honour. It can be, because the difference + * between BIOS and UEFI is entirely in how you *obtain* a framebuffer -- VBE in + * real mode, or GOP -- and not in what you do with one afterwards. By the time + * there is a base address, a size and a stride, there is nothing firmware- + * specific left to say. + * + * Everything here draws and nothing here decides. No disk, no memory map, no + * mode setting: a caller hands over a framebuffer and asks for pixels. That is + * what makes it compilable into a 16/32/64-bit BIOS loader and into a PE/COFF + * UEFI application without either of them leaking into it. + * + * Types are spelled out rather than taken from a header, because the two + * callers do not agree on one: the BIOS side has uchar/uint32 from the kernel + * tree, the UEFI side has UINT8/UINT32 from the EFI headers, and this file has + * to compile inside both. + */ +#ifndef SCHOS_BOOT_UI_H +#define SCHOS_BOOT_UI_H + +typedef unsigned char ui_u8; +typedef unsigned short ui_u16; +typedef unsigned int ui_u32; +typedef unsigned long long ui_u64; + +/* A linear framebuffer, 32 bits per pixel. Both firmwares can produce one and + * neither can produce anything this code could use if they could not. */ +struct ui_fb { + ui_u8 *base; + ui_u32 width; + ui_u32 height; + ui_u32 stride; /* bytes per row, from the driver -- never width*4 */ +}; + +/* Hand over the framebuffer. Passing a null base, or a zero size, leaves the + * UI inert: every call below then does nothing, so a caller with no display + * does not need to guard each one. */ +void ui_init(const struct ui_fb *fb); +int ui_active(void); + +ui_u32 ui_width(void); +ui_u32 ui_height(void); + +void ui_clear(void); +void ui_fill_rect(int x, int y, int w, int h, ui_u32 colour); +void ui_text(int x, int y, const char *s, ui_u32 colour, ui_u32 bg); +void ui_text_centered(int y, const char *s, ui_u32 colour); + +/* The logo, centred horizontally, top-left at the given y. */ +void ui_logo(int y); + +/* The splash: logo, product name, and the progress bar beneath it. + * + * Laid out from the centre outwards rather than from the top down, so it looks + * the same at 800x600 and at 1280x1024 instead of drifting up the screen as the + * mode grows. + */ +void ui_splash(const char *product, const char *subtitle); + +/* The progress bar. `total` is whatever unit the caller counts in; ticks are + * cumulative, and the bar only ever moves forward. */ +void ui_progress_begin(const char *label, ui_u32 total); +void ui_progress_label(const char *label); +void ui_progress_tick(ui_u32 done); +void ui_progress_end(void); + +#endif /* SCHOS_BOOT_UI_H */ diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 239a1db..e8747cf 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -26,6 +26,7 @@ #include "efi.h" #include "../../include/schboot.h" +#include "../common/ui.h" // Where the kernel expects to find things comes from schboot.h, included // above: the hand-off is one contract and both loaders write the same one. @@ -182,6 +183,22 @@ load_elf(EFI_FILE_PROTOCOL *file) UINT64 lowest = ~0ULL, highest = 0; + // Two passes' worth of information from one: the loop below both loads and + // reports, so the bar tracks bytes actually written rather than a segment + // count that says nothing about how long each takes. + UINT64 total = 0, done = 0; + for (UINT16 i = 0; i < eh.e_phnum; i++) { + struct elf64_phdr ph; + UINTN m = sizeof ph; + if (EFI_ERROR(file->SetPosition(file, eh.e_phoff + (UINT64)i * eh.e_phentsize))) + break; + if (EFI_ERROR(file->Read(file, &m, &ph)) || m != sizeof ph) + break; + if (ph.p_type == PT_LOAD) + total += ph.p_memsz; + } + ui_progress_begin("Loading SchOS", (ui_u32)(total ? total : 1)); + for (UINT16 i = 0; i < eh.e_phnum; i++) { struct elf64_phdr ph; n = sizeof ph; @@ -223,6 +240,9 @@ load_elf(EFI_FILE_PROTOCOL *file) zero((void *)(UINTN)(ph.p_paddr + ph.p_filesz), (UINTN)(ph.p_memsz - ph.p_filesz)); + done += ph.p_memsz; + ui_progress_tick((ui_u32)done); + if (ph.p_paddr < lowest) lowest = ph.p_paddr; if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz; } @@ -446,6 +466,17 @@ setup_framebuffer(void) *(UINT8 *)(vbe + 25) = (UINT8)bpp; *(UINT32 *)(vbe + 40) = fb; + // The same UI the BIOS loader draws, on the framebuffer this one just + // obtained. Everything above this point is firmware-specific; nothing below + // it is, which is the whole reason it can be shared. + struct ui_fb uifb; + uifb.base = (ui_u8 *)(UINTN)fb; + uifb.width = info->HorizontalResolution; + uifb.height = info->VerticalResolution; + uifb.stride = stride; + ui_init(&uifb); + ui_splash("SchOS", "Starting up"); + print(L"schos: framebuffer "); print_hex(fb); print(L" "); print_hex(info->HorizontalResolution); print(L"x"); print_hex(info->VerticalResolution); @@ -536,14 +567,22 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) ST->ConOut->ClearScreen(ST->ConOut); print(L"SchOS UEFI loader\r\n"); + // The display comes first, before the kernel is read. + // + // It used to be the other way round, which meant the resolution prompt + // appeared after the slowest step and the progress bar was drawn into a + // framebuffer that did not exist yet -- every tick was silently discarded + // and then the splash painted over the result. Setting the mode first also + // puts the menu where a person expects it: at the start. + UINT32 fb = setup_framebuffer(); + EFI_FILE_PROTOCOL *file = open_kernel(image); UINT64 entry = load_elf(file); + ui_progress_end(); file->Close(file); print(L"schos: kernel entry "); print_hex(entry); print(L"\r\n"); - UINT32 fb = setup_framebuffer(); - // Read while boot services still exist and the system table is guaranteed // valid; written to low memory further down, after the firmware has let go // of it, for the same reason the e820 map is.