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 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-11 08:49:44 +02:00
commit 799abbc19a
5 changed files with 375 additions and 298 deletions

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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. */

View file

@ -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.