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.