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>
464 lines
14 KiB
C
464 lines
14 KiB
C
/* SPDX-License-Identifier: Apache-2.0 */
|
|
/*
|
|
* The loader's user interface. See ui.h for why this can be shared at all.
|
|
*
|
|
* The look is deliberately quiet: a dark ground, the mark centred, one thin
|
|
* bar beneath it, and no text that is not load-bearing. A loader is not a
|
|
* place to be interesting -- it is looked at for four seconds by someone who
|
|
* wants to be somewhere else, and the useful thing it can do is stay still and
|
|
* tell them it is working.
|
|
*/
|
|
|
|
#include "ui.h"
|
|
#include "../../include/theme.h"
|
|
#include "../../include/font.h"
|
|
#include "../../include/logo.h"
|
|
|
|
/* "Draw the glyph, leave the background alone." Not a colour anyone can mean:
|
|
* the palette is 24-bit, so the high byte is free for a sentinel. */
|
|
#define SCH_BG_NONE 0xFF000000u
|
|
|
|
static struct ui_fb FB;
|
|
static int ACTIVE;
|
|
|
|
/* Progress state. Kept here rather than passed around because the caller ticks
|
|
* it from inside a read loop that has no business carrying UI state. */
|
|
static ui_u32 PROG_TOTAL, PROG_DONE;
|
|
static int PROG_X, PROG_Y, PROG_W, PROG_H;
|
|
static int PROG_LAST_PX; /* pixels already filled */
|
|
static int PROG_ACTIVE;
|
|
|
|
/* Layout constants. One place, because a splash that is laid out by scattered
|
|
* literals is one nobody dares move.
|
|
*
|
|
* 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)
|
|
{
|
|
ACTIVE = 0;
|
|
if (!fb || !fb->base || !fb->width || !fb->height)
|
|
return;
|
|
FB = *fb;
|
|
if (!FB.stride)
|
|
FB.stride = FB.width * 4;
|
|
ACTIVE = 1;
|
|
}
|
|
|
|
int ui_active(void) { return ACTIVE; }
|
|
ui_u32 ui_width(void) { return ACTIVE ? FB.width : 0; }
|
|
ui_u32 ui_height(void) { return ACTIVE ? FB.height : 0; }
|
|
|
|
static void
|
|
put(int x, int y, ui_u32 colour)
|
|
{
|
|
if (!ACTIVE || x < 0 || y < 0 || (ui_u32)x >= FB.width || (ui_u32)y >= FB.height)
|
|
return;
|
|
*(ui_u32 *)(FB.base + (ui_u64)y * FB.stride + (ui_u64)x * 4) = colour;
|
|
}
|
|
|
|
void
|
|
ui_fill_rect(int x, int y, int w, int h, ui_u32 colour)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
for (int r = 0; r < h; r++) {
|
|
int yy = y + r;
|
|
if (yy < 0 || (ui_u32)yy >= FB.height)
|
|
continue;
|
|
/* One row at a time through a row pointer: put() per pixel costs a
|
|
* bounds check and a multiply for every one of them, and this is used
|
|
* to clear a whole screen. */
|
|
ui_u32 *row = (ui_u32 *)(FB.base + (ui_u64)yy * FB.stride);
|
|
for (int c = 0; c < w; c++) {
|
|
int xx = x + c;
|
|
if (xx >= 0 && (ui_u32)xx < FB.width)
|
|
row[xx] = colour;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_clear(void)
|
|
{
|
|
ui_fill_rect(0, 0, (int)FB.width, (int)FB.height, SCH_BG);
|
|
}
|
|
|
|
static void
|
|
glyph(int x, int y, ui_u8 c, ui_u32 colour, ui_u32 bg)
|
|
{
|
|
for (int row = 0; row < GLYPH_H; row++) {
|
|
ui_u8 bits = font_data[c][row];
|
|
for (int col = 0; col < GLYPH_W; col++) {
|
|
if (bits & (0x80 >> col))
|
|
put(x + col, y + row, colour);
|
|
else if (bg != SCH_BG_NONE)
|
|
put(x + col, y + row, bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_text(int x, int y, const char *s, ui_u32 colour, ui_u32 bg)
|
|
{
|
|
if (!ACTIVE || !s)
|
|
return;
|
|
for (int i = 0; s[i]; i++)
|
|
glyph(x + i * GLYPH_W, y, (ui_u8)s[i], colour, bg);
|
|
}
|
|
|
|
static int
|
|
slen(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s && s[n])
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
void
|
|
ui_text_centered(int y, const char *s, ui_u32 colour)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
int w = slen(s) * GLYPH_W;
|
|
ui_text(((int)FB.width - w) / 2, y, s, colour, SCH_BG_NONE);
|
|
}
|
|
|
|
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, int w)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
if (w <= 0 || w > LOGO_W)
|
|
w = LOGO_W; /* never upscale: it only goes soft */
|
|
|
|
int h = (LOGO_H * w) / LOGO_W;
|
|
int x0 = ((int)FB.width - w) / 2;
|
|
|
|
/* 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;
|
|
|
|
/* 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 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 = ui_mark_width();
|
|
PROG_H = BAR_H;
|
|
PROG_X = ((int)FB.width - PROG_W) / 2;
|
|
|
|
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 + mark_h + MARK_GAP;
|
|
}
|
|
|
|
void
|
|
ui_splash(const char *subtitle)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
|
|
ui_clear();
|
|
bar_geometry();
|
|
|
|
int w = PROG_W;
|
|
int mark_h = (LOGO_H * w) / LOGO_W;
|
|
int top = PROG_Y - MARK_GAP - mark_h;
|
|
|
|
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)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
|
|
bar_geometry();
|
|
PROG_TOTAL = total ? total : 1;
|
|
PROG_DONE = 0;
|
|
PROG_LAST_PX = 0;
|
|
PROG_ACTIVE = 1;
|
|
|
|
/* 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);
|
|
}
|
|
|
|
void
|
|
ui_progress_label(const char *label)
|
|
{
|
|
if (!ACTIVE || !label)
|
|
return;
|
|
|
|
/* Clear the line first. A shorter label after a longer one would otherwise
|
|
* leave the tail of the old one behind. */
|
|
int y = PROG_Y + PROG_H + SUB_GAP;
|
|
ui_fill_rect(0, y, (int)FB.width, GLYPH_H, SCH_BG);
|
|
ui_text_centered(y, label, SCH_DIM);
|
|
}
|
|
|
|
void
|
|
ui_progress_tick(ui_u32 done)
|
|
{
|
|
if (!ACTIVE || !PROG_ACTIVE)
|
|
return;
|
|
|
|
if (done > PROG_TOTAL)
|
|
done = PROG_TOTAL;
|
|
PROG_DONE = done;
|
|
|
|
int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL);
|
|
|
|
/* Only the newly filled part. Filling from zero each time is O(width) per
|
|
* tick and visibly slower than the thing it is reporting on. */
|
|
if (px > PROG_LAST_PX) {
|
|
/* 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;
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_progress_end(void)
|
|
{
|
|
if (!ACTIVE || !PROG_ACTIVE)
|
|
return;
|
|
ui_progress_tick(PROG_TOTAL);
|
|
PROG_ACTIVE = 0;
|
|
}
|