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:
parent
64e4835524
commit
799abbc19a
5 changed files with 375 additions and 298 deletions
286
boot/common/ui.c
286
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue