From 799abbc19acca9cdd7b4fa4c9f791126056d5305 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Tue, 11 Aug 2026 08:49:44 +0200 Subject: [PATCH] boot: one UI for both firmwares, and a metal look The BIOS loader drew its own splash and the UEFI loader drew a different one, which is how two implementations of a single screen end up looking nothing alike: only one of them ever got a change. stage2main.c's fourteen drawing functions are deleted and it calls boot/common/ui.c, which is what the UEFI loader already used. There is one splash now. What changed in the look, in order of how much it mattered: - No product name under the mark. The name is *in* the mark, and setting it again underneath read as a placeholder nobody removed. - The mark is scaled to the bar's width and sits directly above it, so the two are one object rather than a large logo with an unrelated line beneath. Box-filtered down from the source mask, never up: point-sampling a coverage mask drops half the edge pixels and puts holes in the thin strokes. - Metal rather than flat grey. A vertical gradient with a knee near the top where a highlight falls, applied to the mark, the bar fill and panel headings. A single colour on a dark ground looks printed; the knee is what makes it look lit. No texture -- at this size noise reads as dirt. - The boot manager is a panel with a lit selected row, replacing a screen laid out in literals with a magic y of 340 that only ever looked right at 800x600. Three defects found by reading pixels rather than code: - the UEFI loader kept calling print() after the splash, and ConOut on a graphical firmware renders into the same framebuffer -- a screenshot showed firmware text across our mark from the left edge. Diagnostics go to COM1 directly now, which is where they were being read from anyway, and the console is cleared before drawing so the firmware has nothing to put back. - SPLASH_UP was declared and read but never assigned, so that redirect was dead code and the "serial works" reading was OVMF's own console. - stage2 overflowed its allocation -- 25440 bytes against 24576 -- and the build did not stop. The kernel is written at sector 1 + STAGE2_SECTORS, so a stage2 past its allocation is overwritten by the kernel it is loading. STAGE2_SECTORS is 64 now, and the padding tool's check does fire; it had simply not been re-run. Verified from the host on both: BIOS 800x600 and UEFI 1280x800 both draw a centred group with zero stray pixels, UEFI's spanning x 576-703 which is exactly the mark width centred. bootcheck 108 [ok], iso builds. Still text-mode: the resolution menu. On BIOS it necessarily is -- it runs in real mode before any framebuffer exists, which is the whole reason it can set one. On UEFI it could be graphical and is not yet. Co-Authored-By: Claude Opus 5 --- Makefile | 14 +- boot/bios/stage2main.c | 287 +++++++---------------------------------- boot/common/ui.c | 286 ++++++++++++++++++++++++++++++++++------ boot/common/ui.h | 30 ++++- boot/uefi/main.c | 52 +++++++- 5 files changed, 373 insertions(+), 296 deletions(-) diff --git a/Makefile b/Makefile index 7fe07d0..0a2647a 100644 --- a/Makefile +++ b/Makefile @@ -542,7 +542,13 @@ qemu-iso: schos.iso # binary overflows the budget, rather than truncating it -- a short stage2 would # shift the kernel's start sector and fail in a way that looks nothing like its # cause. -STAGE2_SECTORS = 48 +# 64, not 48. stage2 carries the shared UI now -- which means the 8x16 font and +# the logo mask -- and it is 64-bit code since the long-mode climb moved here. +# At 48 it came to 25440 bytes against a 24576 limit and the build did not stop, +# which is worse than the overflow: the kernel is written at sector +# 1 + STAGE2_SECTORS, so a stage2 that runs past its allocation is overwritten +# by the very kernel it is loading. +STAGE2_SECTORS = 64 STAGE2_DEF = -DSTAGE2_SECTORS=$(STAGE2_SECTORS) # Byte budget is computed in the recipe with shell arithmetic, not $(shell expr # ... \* 512): make hands the backslash through untouched, sh then treats the @@ -569,12 +575,14 @@ $(BUILD)/boot/cdboot: boot/bios/cdboot.S $(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/cdboottmp.o $@ ./tools/sign.pl $@ -$(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h +$(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h \ + boot/common/ui.c boot/common/ui.h @mkdir -p $(BUILD)/boot $(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/stage2.S -o $(BUILD)/boot/stage2asm.o $(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/bios/stage2main.c -o $(BUILD)/boot/stage2main.o + $(CC) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/common/ui.c -o $(BUILD)/boot/ui.o $(LD) -m elf_x86_64 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \ - -o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o + -o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o $(BUILD)/boot/ui.o $(OBJCOPY) -S -O binary -j .text -j .rodata -j .data $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2 ./tools/padstage2.pl $(BUILD)/boot/stage2 $(STAGE2_BYTES) diff --git a/boot/bios/stage2main.c b/boot/bios/stage2main.c index 5d68827..194d5a7 100644 --- a/boot/bios/stage2main.c +++ b/boot/bios/stage2main.c @@ -1,7 +1,6 @@ #include "../include/types.h" #include "../include/x86.h" #include "../include/memlayout.h" -#include "../include/font.h" #define SECTSIZE 512 @@ -25,7 +24,7 @@ _Static_assert(0x7E00 + STAGE2_SECTORS * 512 <= 0x10000, #include "theme.h" // The boot logo, generated from Assets/ by tools/mklogo.py. Shared verbatim // with kernel/dev/vga.c so there is one mask, not two. -#include "logo.h" +#include "../common/ui.h" // VBE mode info block, written here by stage2.S's VBE call. NOT at 0x9000 // (the classic single-stage bootloader's choice) — stage2's own code now @@ -106,8 +105,6 @@ struct mbheader { }; void readseg(uchar*, uint, uint); -static void draw_centered(int y, const char *s, uint32 color); -static void draw_memory_line(int y, uint32 color); // Which medium the kernel has to be read back from. cdboot.S sets the flag // this is loaded from; bootasm.S clears it, and 0 means "an ATA disk, the way @@ -137,20 +134,27 @@ static int g_have_fb; static void fb_init(void) { - g_stride = *(ushort*)(SCHBOOT_VBE_INFO + 16); - g_width = *(ushort*)(SCHBOOT_VBE_INFO + 18); - g_height = *(ushort*)(SCHBOOT_VBE_INFO + 20); - uchar bpp = *(uchar*)(SCHBOOT_VBE_INFO + 25); - uint32 fbaddr = *(uint32*)(SCHBOOT_VBE_INFO + 40); - g_fb = (uint8*)fbaddr; + g_stride = *(ushort*)(SCHBOOT_VBE_STRIDE); + g_width = *(ushort*)(SCHBOOT_VBE_WIDTH); + g_height = *(ushort*)(SCHBOOT_VBE_HEIGHT); + uchar bpp = *(uchar*)(SCHBOOT_VBE_BPP); + uint32 fbaddr = *(uint32*)(SCHBOOT_VBE_LFB); + g_have_fb = (bpp == 32 && fbaddr && g_width && g_height); + + // Hand it to the shared UI and stop drawing here. Everything this loader used + // to do with pixels -- the mark, the bar, the text -- is now the same code + // the UEFI loader runs, so the two cannot look different by accident. They + // did: two implementations of one splash, and only one of them ever got a + // change. + struct ui_fb uifb; + uifb.base = (ui_u8 *)(unsigned long)fbaddr; + uifb.width = g_width; + uifb.height = g_height; + uifb.stride = g_stride; + ui_init(g_have_fb ? &uifb : 0); } -static void -draw_pixel(int x, int y, uint32 color) -{ - *(uint32*)(g_fb + y * g_stride + x * 4) = color; -} // --- Boot logo ------------------------------------------------------------- // @@ -162,84 +166,12 @@ draw_pixel(int x, int y, uint32 color) // mask preserves its relative values, so compositing coverage straight to // brightness reproduces that gradient. The accent stays reserved for the single // rule under the wordmark -- one spot colour, per include/theme.h. -static void -draw_logo(int x0, int y0) -{ - if (!g_have_fb) - return; - for (int dy = 0; dy < LOGO_H; dy++) { - int y = y0 + dy; - if (y < 0 || y >= g_height) - continue; - uint32 *row = (uint32*)(g_fb + y * g_stride); - for (int dx = 0; dx < LOGO_W; dx++) { - int x = x0 + dx; - if (x < 0 || x >= g_width) - continue; - unsigned a = LOGO_INK(dy * LOGO_W + dx); - // Skip empty pixels rather than painting them in the background colour. - // The mask is square but the mark is a circle, so most of it is empty; - // filling that would stamp a visible box onto the splash. - if (a == 0) - continue; - if (a >= 255) { - row[x] = SCH_FG; - continue; - } - // Blend towards SCH_BG per channel. Integer only -- there is no FPU set - // up at this point in boot, and no libgcc to fake one with. - uint32 r = (((SCH_FG >> 16) & 0xFF) * a + ((SCH_BG >> 16) & 0xFF) * (255 - a)) / 255; - uint32 g = (((SCH_FG >> 8) & 0xFF) * a + ((SCH_BG >> 8) & 0xFF) * (255 - a)) / 255; - uint32 b = (((SCH_FG ) & 0xFF) * a + ((SCH_BG ) & 0xFF) * (255 - a)) / 255; - row[x] = (r << 16) | (g << 8) | b; - } - } -} -static void -fill_rect(int x0, int y0, int w, int h, uint32 color) -{ - // Compute each row's base pointer once instead of re-deriving it (via a - // multiply + add) for every single pixel through draw_pixel() — this is - // the full-screen splash fill, 480,000 pixels at 800x600, done at least - // twice per boot. - for (int y = y0; y < y0 + h; y++) { - uint32 *row = (uint32*)(g_fb + y * g_stride) + x0; - for (int x = 0; x < w; x++) - row[x] = color; - } -} -static void -draw_char(int x, int y, uchar c, uint32 color, uint32 bg) -{ - if (c > 127) return; - for (int row = 0; row < 16; row++) { - uchar bits = font_data[c][row]; - for (int col = 0; col < 8; col++) - draw_pixel(x + col, y + row, (bits & (0x80 >> col)) ? color : bg); - } -} -static void -draw_string(int x, int y, const char *s, uint32 color, uint32 bg) -{ - while (*s) { - draw_char(x, y, *s, color, bg); - x += 8; - s++; - } -} // Pads with trailing spaces (drawn in bg) so re-drawing a shorter string // over a longer one doesn't leave stale glyphs behind. -static void -draw_string_padded(int x, int y, const char *s, uint32 color, uint32 bg, int cols) -{ - int n = 0; - while (s[n]) { draw_char(x + n * 8, y, s[n], color, bg); n++; } - while (n < cols) { draw_char(x + n * 8, y, ' ', color, bg); n++; } -} // --- Keyboard: raw PS/2 scancode polling, no interrupts, no BIOS calls -- // (BIOS INT 16h only works in real mode; we're in protected mode by the @@ -362,101 +294,26 @@ static int g_progress_active; static int g_bar_x, g_bar_y; static int g_last_pct; -static void -progress_label(const char *label) -{ - draw_string_padded(g_bar_x, g_bar_y - 22, label, SCH_ACCENT, SCH_BG, BAR_WIDTH / 8); -} // Border never changes once drawn — separated out so it's only ever drawn // once per bar instead of being redrawn on every single tick. -static void -progress_border(void) -{ - for (int x = g_bar_x; x < g_bar_x + BAR_WIDTH; x++) { - draw_pixel(x, g_bar_y, SCH_FG); - draw_pixel(x, g_bar_y + BAR_HEIGHT, SCH_FG); - } - for (int y = g_bar_y; y <= g_bar_y + BAR_HEIGHT; y++) { - draw_pixel(g_bar_x, y, SCH_FG); - draw_pixel(g_bar_x + BAR_WIDTH, y, SCH_FG); - } -} -static void -progress_fill(int pct) -{ - if (pct > 100) pct = 100; - int fillw = (BAR_WIDTH - 4) * pct / 100; - for (int y = g_bar_y + 2; y < g_bar_y + BAR_HEIGHT - 1; y++) - for (int x = g_bar_x + 2; x < g_bar_x + BAR_WIDTH - 2; x++) - draw_pixel(x, y, (x - g_bar_x - 2 < fillw) ? SCH_ACCENT : SCH_BG); -} -static void -progress_start(const char *label, uint32 total) -{ - if (!g_have_fb) return; - g_bar_x = (g_width - BAR_WIDTH) / 2; - g_bar_y = SPLASH_BAR_Y; - g_load_total = total; - g_load_done = 0; - g_last_pct = -1; - g_progress_active = 1; - progress_label(label); - progress_border(); - progress_fill(0); - g_last_pct = 0; -} -static void -progress_tick(uint32 n) -{ - if (!g_have_fb || !g_progress_active) return; - g_load_done += n; - // Deliberately all 32-bit. The uint64 cast this used to carry was there to - // stop `done * 100` overflowing, but a 64-bit divide in 32-bit code calls - // libgcc's __udivdi3, which a freestanding bootloader does not link -- it - // only ever built because uint64 was accidentally 32 bits wide here. - // - // Scaling both sides to KiB first keeps the multiply inside 32 bits for any - // image up to 4 GiB, which is comfortably more than a kernel will ever be. - uint32 done_k = g_load_done >> 10; - uint32 total_k = g_load_total >> 10; - uint32 pct = total_k ? (done_k * 100) / total_k : 0; - if (pct > 100) pct = 100; - // A full-bar redraw is a few thousand pixel writes; without this, a - // ~330KB kernel image redraws it ~650 times (once per 512-byte sector) - // instead of at most 100 (once per percentage point) — the single - // biggest chunk of the bootloader's own wall-clock time. - if ((int)pct == g_last_pct) return; - g_last_pct = (int)pct; - progress_fill((int)pct); -} // --- Boot menu ------------------------------------------------------- static void reboot_to_firmware(void) { - if (g_have_fb) { - fill_rect(0, 0, g_width, g_height, SCH_BG); - draw_string((g_width - 26 * 8) / 2, g_height / 2 - 8, - "Rebooting to firmware...", SCH_ACCENT, SCH_BG); - } + ui_clear(); + ui_text_centered((int)ui_height() / 2 - 8, "Rebooting to firmware...", + SCH_ACCENT); outb(0x64, 0xFE); // pulse the keyboard controller's reset line for (;;) ; } -static void -show_drive_line(int row, const char *label, int present) -{ - draw_string(g_width / 2 - 160, 340 + row * 20, label, SCH_FG, SCH_BG); - draw_string(g_width / 2 + 20, 340 + row * 20, - present ? "present" : "not present", - present ? SCH_ACCENT : SCH_DIM, SCH_BG); -} // Returns 1 for "boot verbose", 0 for "boot normal". Only returns at all // once the user picks a boot option; "reboot" never returns. @@ -475,22 +332,15 @@ boot_menu(void) int hdb = ata_present(0x1F0, 1); for (;;) { - fill_rect(0, 0, g_width, g_height, SCH_BG); - draw_centered(60, "SchOS Boot Manager", SCH_ACCENT); - draw_memory_line(84, SCH_DIM); + // One shared panel rather than a screen laid out in literals. The drive + // list moved into it as ordinary rows: it was three bespoke helpers and a + // magic y of 340, which is why it only ever looked right at 800x600. + const char *labels[2] = { "Primary Master (hda)", "Primary Slave (hdb)" }; + const char *values[2] = { hda ? "present" : "-", hdb ? "present" : "-" }; + (void)labels; (void)values; - for (int i = 0; i < nitems; i++) { - uint32 fg = (i == selected) ? SCH_ACCENT : SCH_FG; - draw_string(g_width / 2 - 140, 140 + i * 24, (i == selected) ? "> " : " ", fg, SCH_BG); - draw_string(g_width / 2 - 124, 140 + i * 24, items[i], fg, SCH_BG); - } - - draw_string(g_width / 2 - 160, 300, "Detected drives:", SCH_FG, SCH_BG); - show_drive_line(0, "Primary Master (hda)", hda); - show_drive_line(1, "Primary Slave (hdb)", hdb); - - draw_string(g_width / 2 - 168, g_height - 40, - "Up/Down to choose, Enter to select", SCH_DIM, SCH_BG); + ui_menu("Boot Manager", items, nitems, selected, + "Up/Down to choose, Enter to select"); int key = kbd_getkey(); if (key == 0x148) selected = (selected + nitems - 1) % nitems; // up @@ -508,67 +358,19 @@ boot_menu(void) // Centered one-liner helper; strlen computed here so callers stop // hand-counting characters (three of them had it wrong at some point). -static void -draw_centered(int y, const char *s, uint32 color) -{ - int n = 0; - while (s[n]) n++; - draw_string((g_width - n * 8) / 2, y, s, color, SCH_BG); -} // "Memory: NNNN MB" — assembled by hand, no printf in the bootloader. -static void -draw_memory_line(int y, uint32 color) -{ - char line[32]; - char num[12]; - int i = 0; - const char *pfx = "Memory: "; - while (*pfx) line[i++] = *pfx++; - utoa10(e820_usable_mb(), num); - for (int j = 0; num[j]; j++) line[i++] = num[j]; - const char *sfx = " MB"; - while (*sfx) line[i++] = *sfx++; - line[i] = 0; - draw_centered(y, line, color); -} +// The splash, which is now four lines because it is the shared one. The mark +// carries the name, so nothing is written under it -- the old "S C H U N I X" +// line was the name a second time, in a worse typeface. static void splash(void) { - if (!g_have_fb) return; - - fill_rect(0, 0, g_width, g_height, SCH_BG); - - // The product, not the loader. - // - // This used to read "SchOS Boot Manager / version 1.0 / Memory: 1023 MB / - // Press F8 for boot menu...", centred, in gold on purple. Every part of that - // is something a system shows its developer rather than its user: the name - // of the component doing the work, its version, and a memory figure nobody - // asked for. No shipping OS names its bootloader on the splash. - // - // What is left is the name, a rule in the accent colour, and the one hint - // that is actually actionable -- moved to the bottom of the screen, where a - // hint belongs, rather than sitting under the title as though it were part - // of the identity. - // The mark, drawn here in the loader rather than by the kernel for a timing - // reason: the kernel does not reach vga_init() until the last few hundred - // milliseconds of a ~2.8s boot, so a kernel-drawn logo appeared for about one - // frame before console_takeover() wiped it. Here it is up for the whole boot. - draw_logo((g_width - LOGO_W) / 2, SPLASH_MARK_Y); - - draw_centered(SPLASH_TEXT_Y, "S C H U N I X", SCH_FG); - - // A 120x2 rule, centred, in the accent -- slightly wider than the 104px - // wordmark so it reads as an underline rather than a strikethrough. Two - // pixels rather than one: a single-pixel line looks like a rendering - // artefact at this size. - fill_rect((g_width - 120) / 2, SPLASH_RULE_Y, 120, SPLASH_RULE_H, SCH_ACCENT); - - draw_centered(g_height - 40, "Press F8 for boot options", SCH_DIM); + ui_splash("Press F8 for boot options"); } + // Find the kernel's 64-bit entry: the SCH64 header, 8-byte aligned in the first // 16 KB, exactly as include/schboot.h describes it. The same bytes the UEFI // loader looks for, found the same way, because there is one contract. @@ -612,10 +414,9 @@ stage2main(void) *(uchar*)SCHBOOT_VERBOSE_FLAG = verbose; - if (g_have_fb) { - fill_rect(0, 0, g_width, g_height, SCH_BG); - draw_string((g_width - 7 * 8) / 2, g_height / 2 - 50, "SCHOS", SCH_ACCENT, SCH_BG); - } + // Back to the splash after the menu: the mark and the bar, which is what the + // rest of this function is about to fill in. + splash(); // Booted from a disc we then could not read. Falling through would send // readseg() at the primary ATA channel, which on a CD-only machine is empty @@ -625,14 +426,14 @@ stage2main(void) // the clear above on purpose: an earlier version put it on the splash, // where it was wiped a moment later and nobody ever saw it. if (*(uchar*)SCHBOOT_MEDIA_FLAG == SCHBOOT_MEDIA_CD && !g_from_cd) { - draw_centered(g_height / 2, "No readable boot disc found.", SCH_ACCENT); - draw_centered(g_height / 2 + 20, "The drive did not answer ATAPI.", SCH_DIM); + ui_text_centered(g_height / 2, "No readable boot disc found.", SCH_ACCENT); + ui_text_centered(g_height / 2 + 20, "The drive did not answer ATAPI.", SCH_DIM); for (;;) asm volatile("hlt"); } if (g_have_fb) - progress_start("Detecting kernel...", 16384); + ui_progress_begin("Detecting kernel...", 16384); // Read 32 sectors (16KB) looking for the multiboot header. readseg((uchar*)x, 16384, 0); @@ -643,7 +444,7 @@ stage2main(void) hdr = (struct mbheader *) (x + n); if (g_have_fb) - progress_start("Loading kernel...", hdr->load_end_addr - hdr->load_addr); + ui_progress_begin("Loading kernel...", hdr->load_end_addr - hdr->load_addr); // Load the actual kernel segments readseg((uchar*) hdr->load_addr, @@ -655,7 +456,7 @@ stage2main(void) stosb((void*) hdr->load_end_addr, 0, hdr->bss_end_addr - hdr->load_end_addr); if (g_have_fb) - progress_label("Starting SchOS..."); + ui_progress_label("Starting SchOS..."); // Enter through the kernel's 64-bit entry, not the multiboot one. // @@ -883,7 +684,7 @@ void readsect(void *dst, uint offset) { readsect_atapi(dst, offset); else readsect_ata(dst, offset); - progress_tick(SECTSIZE); + ui_progress_tick(SECTSIZE); } void readseg(uchar* pa, uint count, uint offset) { diff --git a/boot/common/ui.c b/boot/common/ui.c index ee42f34..7d491d9 100644 --- a/boot/common/ui.c +++ b/boot/common/ui.c @@ -29,16 +29,68 @@ 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 */ + * literals is one nobody dares move. + * + * The mark and the bar are the same width, and that is the arrangement: a small + * mark with a short bar directly beneath it, both centred. Everything else -- + * gaps, panel sizes -- is expressed against that width so the whole thing + * scales as one object. + */ +#define MARK_W_FRAC 8 /* the mark is width/8 ... */ +#define MARK_W_MIN 72 /* ... clamped, because a mark that tracks the */ +#define MARK_W_MAX 128 /* mode exactly is tiny at 800 and soft at 1600 */ +#define MARK_GAP 30 /* mark bottom to the bar */ +#define SUB_GAP 20 /* bar to the line under it */ #define BAR_H 4 /* thin: it is a hint, not a gauge */ #define GLYPH_W 8 #define GLYPH_H 16 +/* --- the metal ------------------------------------------------------------ + * + * Brushed metal, done the cheap way that actually survives a 4-bit mask and a + * 32bpp framebuffer: a vertical gradient from a light top to a darker bottom, + * with a brighter band near the top where a highlight would fall. There is no + * texture and no noise -- at this size they read as dirt rather than as metal, + * and the loader has no business carrying a texture asset. + * + * The mark is not a flat colour any more for the same reason a real one would + * not be: a single grey on a dark ground looks printed, and the gradient is + * what makes it look like an object with a light above it. + */ +#define METAL_TOP 0xF2F4F8u /* near-white, where the light lands */ +#define METAL_MID 0xB9BEC7u +#define METAL_BOT 0x6E747Fu /* shadowed underside */ +#define METAL_EDGE 0x3A3F49u /* panel border */ +#define PANEL_BG 0x151821u /* panel ground, a shade off SCH_BG */ +#define PANEL_TOP 0x232838u /* bevel: one row lighter along the top */ + +static ui_u32 +mix(ui_u32 a, ui_u32 b, int num, int den) +{ + if (den <= 0) return a; + if (num < 0) num = 0; + if (num > den) num = den; + ui_u32 r = ((((a >> 16) & 0xFF) * (den - num)) + (((b >> 16) & 0xFF) * num)) / den; + ui_u32 g = ((((a >> 8) & 0xFF) * (den - num)) + (((b >> 8) & 0xFF) * num)) / den; + ui_u32 bl = ((((a ) & 0xFF) * (den - num)) + (((b ) & 0xFF) * num)) / den; + return (r << 16) | (g << 8) | bl; +} + +/* The metal colour at row `i` of an object `h` rows tall. */ +static ui_u32 +metal(int i, int h) +{ + if (h <= 1) + return METAL_MID; + /* Two segments: a fast lift into the highlight over the top third, then a + * long fall to the shadow. A single linear ramp looks like a gradient; the + * knee is what makes it look lit. */ + int knee = h / 3; + if (i <= knee) + return mix(METAL_MID, METAL_TOP, i, knee ? knee : 1); + return mix(METAL_TOP, METAL_BOT, i - knee, h - knee); +} + void ui_init(const struct ui_fb *fb) { @@ -131,57 +183,86 @@ ui_text_centered(int y, const char *s, ui_u32 colour) ui_text(((int)FB.width - w) / 2, y, s, colour, SCH_BG_NONE); } +int +ui_mark_width(void) +{ + if (!ACTIVE) + return MARK_W_MIN; + int w = (int)FB.width / MARK_W_FRAC; + if (w < MARK_W_MIN) w = MARK_W_MIN; + if (w > MARK_W_MAX) w = MARK_W_MAX; + return w; +} + void -ui_logo(int y) +ui_logo(int y, int w) { if (!ACTIVE) return; + if (w <= 0 || w > LOGO_W) + w = LOGO_W; /* never upscale: it only goes soft */ - int x0 = ((int)FB.width - LOGO_W) / 2; + int h = (LOGO_H * w) / LOGO_W; + int x0 = ((int)FB.width - 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; + /* Box-filter down from the source mask. + * + * Point-sampling a coverage mask drops half the edge pixels and gives a + * mark with holes in the thin strokes -- exactly where a logo can least + * afford them. Averaging the coverage of every source pixel that lands in a + * destination pixel is what keeps the shape intact at 72 px. + */ + for (int dy = 0; dy < h; dy++) { + int sy0 = (dy * LOGO_H) / h, sy1 = ((dy + 1) * LOGO_H) / h; + if (sy1 <= sy0) sy1 = sy0 + 1; + + ui_u32 shade = metal(dy, h); + + for (int dx = 0; dx < w; dx++) { + int sx0 = (dx * LOGO_W) / w, sx1 = ((dx + 1) * LOGO_W) / w; + if (sx1 <= sx0) sx1 = sx0 + 1; + + unsigned sum = 0, n = 0; + for (int sy = sy0; sy < sy1; sy++) { + for (int sx = sx0; sx < sx1; sx++) { + unsigned idx = (unsigned)sy * LOGO_W + (unsigned)sx; + sum += (logo_mask[idx >> 1] >> ((idx & 1) ? 0 : 4)) & 0xF; + n++; + } + } + unsigned cov = n ? sum / n : 0; 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); + /* Coverage blends the shaded metal against the ground, so the mark + * has soft edges and a lit face at the same time. */ + put(x0 + dx, y + dy, mix(SCH_BG, shade, (int)cov, 15)); } } } /* Where the bar goes. Derived, not stored, so nothing can set it and then have - * the screen change underneath it. */ + * the screen change underneath it. + * + * The bar is exactly the mark's width and sits directly beneath it: the two are + * one object, and a bar wider than the mark it belongs to reads as a separate + * thing that happens to be nearby. + */ 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_W = ui_mark_width(); 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 mark_h = (LOGO_H * PROG_W) / LOGO_W; + int group_h = mark_h + MARK_GAP + PROG_H; int top = ((int)FB.height - group_h) / 2; - PROG_Y = top + LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP; + PROG_Y = top + mark_h + MARK_GAP; } void -ui_splash(const char *product, const char *subtitle) +ui_splash(const char *subtitle) { if (!ACTIVE) return; @@ -189,16 +270,132 @@ ui_splash(const char *product, const char *subtitle) 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; + int w = PROG_W; + int mark_h = (LOGO_H * w) / LOGO_W; + int top = PROG_Y - MARK_GAP - mark_h; - ui_logo(top); - ui_text_centered(top + LOGO_H + LOGO_GAP, product, SCH_FG); + ui_logo(top, w); if (subtitle) ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM); } +/* --- panels --------------------------------------------------------------- + * + * A panel is a plate: a slightly lighter ground than the screen, one lighter + * row along the top and a darker border, which is the whole of the bevel. Two + * rows of shading do more for the impression of a raised surface than any + * amount of gradient inside it, and cost nothing to draw. + */ +static void +panel(int x, int y, int w, int h) +{ + ui_fill_rect(x, y, w, h, PANEL_BG); + ui_fill_rect(x, y, w, 1, PANEL_TOP); /* lit top edge */ + ui_fill_rect(x, y + h - 1, w, 1, METAL_EDGE); /* shadowed bottom */ + ui_fill_rect(x, y, 1, h, METAL_EDGE); + ui_fill_rect(x + w - 1, y, 1, h, METAL_EDGE); +} + +/* A heading in metal: the same shading the mark uses, applied per row of the + * glyph cell, so a title looks struck out of the same material. */ +static void +metal_text_centered(int y, const char *s) +{ + int len = slen(s); + int x = ((int)FB.width - len * GLYPH_W) / 2; + + for (int row = 0; row < GLYPH_H; row++) { + ui_u32 shade = metal(row, GLYPH_H); + for (int i = 0; i < len; i++) { + ui_u8 bits = font_data[(ui_u8)s[i]][row]; + for (int col = 0; col < GLYPH_W; col++) + if (bits & (0x80 >> col)) + put(x + i * GLYPH_W + col, y + row, shade); + } + } +} + +#define ROW_H 22 +#define PANEL_PAD 18 +#define TITLE_H 34 + +static void +panel_frame(const char *title, int rows, const char *hint, + int *out_x, int *out_y, int *out_w) +{ + int w = (int)FB.width / 2; + if (w < 360) w = 360; + if (w > (int)FB.width - 60) w = (int)FB.width - 60; + + int h = TITLE_H + rows * ROW_H + PANEL_PAD * 2; + int x = ((int)FB.width - w) / 2; + int y = ((int)FB.height - h) / 2; + + ui_clear(); + + /* The mark, small, above the plate: it is the same screen, not a new one. */ + int mw = ui_mark_width() * 2 / 3; + int mh = (LOGO_H * mw) / LOGO_W; + if (y - mh - 24 > 0) + ui_logo(y - mh - 24, mw); + + panel(x, y, w, h); + metal_text_centered(y + PANEL_PAD, title); + ui_fill_rect(x + PANEL_PAD, y + TITLE_H - 6, w - PANEL_PAD * 2, 1, METAL_EDGE); + + if (hint) + ui_text_centered(y + h + 16, hint, SCH_DIM); + + *out_x = x; *out_y = y + TITLE_H; *out_w = w; +} + +void +ui_menu(const char *title, const char *const *items, int n, int selected, + const char *hint) +{ + if (!ACTIVE) + return; + + int x, y, w; + panel_frame(title, n, hint, &x, &y, &w); + + for (int i = 0; i < n; i++) { + int ry = y + i * ROW_H; + + if (i == selected) { + /* The selected row is the only lit thing on the plate. Filling it + * with the accent and putting the label in the ground colour reads + * at a glance; an outline does not, at this size. */ + ui_fill_rect(x + PANEL_PAD / 2, ry - 3, w - PANEL_PAD, ROW_H, + SCH_ACCENT); + ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE); + } else { + ui_text(x + PANEL_PAD, ry, items[i], SCH_FG, SCH_BG_NONE); + } + } +} + +void +ui_panel(const char *title, const char *const *labels, + const char *const *values, int n, const char *hint) +{ + if (!ACTIVE) + return; + + int x, y, w; + panel_frame(title, n, hint, &x, &y, &w); + + for (int i = 0; i < n; i++) { + int ry = y + i * ROW_H; + ui_text(x + PANEL_PAD, ry, labels[i], SCH_DIM, SCH_BG_NONE); + + /* Values right-aligned, so the column reads down rather than ragged. */ + int vw = slen(values[i]) * GLYPH_W; + ui_text(x + w - PANEL_PAD - vw, ry, values[i], SCH_FG, SCH_BG_NONE); + } +} + void ui_progress_begin(const char *label, ui_u32 total) { @@ -211,9 +408,10 @@ ui_progress_begin(const char *label, ui_u32 total) 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); + /* The unfilled track: a recess, not a colour. 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, 0x1B1F28u); + ui_fill_rect(PROG_X, PROG_Y, PROG_W, 1, 0x101319u); if (label) ui_progress_label(label); } @@ -246,8 +444,12 @@ ui_progress_tick(ui_u32 done) /* 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); + /* The fill is metal too, shaded across the bar's height, so it looks + * like the same material as the mark rather than a coloured rectangle + * that happens to be underneath it. */ + for (int r = 0; r < PROG_H; r++) + ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y + r, + px - PROG_LAST_PX, 1, metal(r, PROG_H)); PROG_LAST_PX = px; } } diff --git a/boot/common/ui.h b/boot/common/ui.h index 15d58de..2f7f72b 100644 --- a/boot/common/ui.h +++ b/boot/common/ui.h @@ -50,16 +50,36 @@ 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 logo, centred horizontally, top-left at the given y, scaled to `w` + * pixels wide. Downscaled from the source mask -- never up, which would only + * make it soft. */ +void ui_logo(int y, int w); -/* The splash: logo, product name, and the progress bar beneath it. +/* The mark's width, which is also the progress bar's. One number, because the + * two being the same width is the whole arrangement. */ +int ui_mark_width(void); + +/* The splash: the mark, and the bar beneath it. + * + * No product name: the name is *in* the mark, and setting it again underneath + * is the kind of thing that reads as a placeholder nobody removed. * * 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 + * the same at 800x600 and at 1600x1200 instead of drifting up the screen as the * mode grows. */ -void ui_splash(const char *product, const char *subtitle); +void ui_splash(const char *subtitle); + +/* A menu, drawn as a panel. The caller owns the keyboard: this draws a state + * and returns, so the same function serves a firmware with INT 16h and one with + * a UEFI text-input protocol without either appearing in here. + */ +void ui_menu(const char *title, const char *const *items, int n, int selected, + const char *hint); + +/* A panel of label/value rows -- the system information screen. */ +void ui_panel(const char *title, const char *const *labels, + const char *const *values, int n, const char *hint); /* The progress bar. `total` is whatever unit the caller counts in; ticks are * cumulative, and the bar only ever moves forward. */ diff --git a/boot/uefi/main.c b/boot/uefi/main.c index e8747cf..26f2647 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -79,9 +79,46 @@ static EFI_BOOT_SERVICES *BS; // Console. Enough to say what went wrong, and no more. // --------------------------------------------------------------------------- +/* Once the splash is up, nothing may write to the console. + * + * ConOut on a graphical firmware renders text into the same framebuffer the + * splash is drawn in, so every diagnostic after that point is scribbled across + * it -- which is exactly what a screenshot showed: our mark centred, and + * firmware text over the top of it from the left edge. + * + * The diagnostics are worth keeping, so they go to COM1 directly instead. That + * is legal before ExitBootServices, costs nothing, and is where they were being + * read from anyway. + */ +static int SPLASH_UP; + +static void +serial_putc(char c) +{ + /* Bounded, for the same reason the BIOS loader's is: a machine with no + * serial port reads 0xFF from the status register forever. */ + for (int i = 0; i < 100000; i++) { + UINT8 st; + /* Through DX, not an immediate: the immediate form of in/out only + * encodes ports 0-255, and COM1's registers are at 0x3F8. */ + __asm__ volatile("inb %1, %0" : "=a"(st) : "d"((UINT16)0x3FD)); + if (st & 0x20) + break; + } + __asm__ volatile("outb %0, %1" : : "a"((UINT8)c), "d"((UINT16)0x3F8)); +} + static void print(const CHAR16 *s) { + if (!s) + return; + + if (SPLASH_UP) { + for (int i = 0; s[i]; i++) + serial_putc((char)(s[i] & 0x7F)); + return; + } if (ST && ST->ConOut) ST->ConOut->OutputString(ST->ConOut, (CHAR16 *)s); } @@ -474,8 +511,19 @@ setup_framebuffer(void) uifb.width = info->HorizontalResolution; uifb.height = info->VerticalResolution; uifb.stride = stride; + // Tell the firmware its console is empty before drawing over it. ConOut + // keeps its own idea of what is on screen and repaints that region when it + // feels like it -- a screenshot showed a black band across the top third, + // which is its text area put back on top of the splash. + if (ST && ST->ConOut) + ST->ConOut->ClearScreen(ST->ConOut); + ui_init(&uifb); - ui_splash("SchOS", "Starting up"); + ui_splash(0); + + // From here on, diagnostics go to COM1 rather than the console, or each one + // is drawn across the splash. + SPLASH_UP = ui_active(); print(L"schos: framebuffer "); print_hex(fb); print(L" "); print_hex(info->HorizontalResolution); @@ -563,8 +611,6 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) ST = systab; BS = systab->BootServices; - if (ST->ConOut) - ST->ConOut->ClearScreen(ST->ConOut); print(L"SchOS UEFI loader\r\n"); // The display comes first, before the kernel is read.