UEFI had no boot menu at all, and asked about resolution in a prompt of
its own before anything else. Two questions at two moments through two
mechanisms is what made the firmwares feel like different programs.
There is one window now: press F8 (or 'o' -- a serial console sends no
scan codes, and that is how this is usually driven) and the same panel
the BIOS loader shows appears, with resolution as a row in it. Selecting
it steps to the next 32bpp mode at or above 800x600 and redraws; the
framebuffer is re-read every pass, because changing mode moves it and
drawing where it used to be is a black screen with no explanation.
Two layout bugs, both visible and both arithmetic:
- TITLE_H was 34 while the heading alone occupies PANEL_PAD + GLYPH_H
= 34, so the rule at TITLE_H - 6 was drawn *through* the lettering
and the first row started on the heading's baseline. TITLE_H is
derived from its parts now and cannot drift from them again.
- the selected row's highlight was offset by a fixed 3px against a
24px row, so it sat on the text rather than around it. It is centred
on the glyph box: (ROW_H - GLYPH_H) / 2 above and below.
Verified from the host: the panel is centred at 640 on a 1280 screen, the
highlight is exactly ROW_H tall and clear of the rule, and Enter boots
through to cpu0. bootcheck 108 [ok], iso builds.
The BIOS resolution chooser is still a separate text prompt, and that is
not an oversight to fix later in the same way. VBE mode setting is an
INT 10h call, so it can only happen in real mode -- before there is any
framebuffer to draw a menu in. Offering it from the graphical menu means
either returning to real mode from long mode, or entering unreal mode to
draw the menu before the switch. Both are real mode-switching work in the
riskiest code in the tree, and neither is a tidy-up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
478 lines
15 KiB
C
478 lines
15 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Panel metrics.
|
|
*
|
|
* TITLE_H is the whole title band -- the padding above the heading, the
|
|
* heading itself, and the air beneath it before the rule. It was 34 while the
|
|
* heading alone needs 18 + 16 = 34, so the rule at TITLE_H - 6 was drawn
|
|
* *through* the lettering and the first row started on the heading's baseline.
|
|
* Derived from its parts now, so it cannot drift from them again.
|
|
*/
|
|
#define ROW_H 24
|
|
#define PANEL_PAD 18
|
|
#define TITLE_GAP 14 /* heading to the rule */
|
|
#define TITLE_H (PANEL_PAD + GLYPH_H + TITLE_GAP) /* = 48 */
|
|
#define ROW_LIFT ((ROW_H - GLYPH_H) / 2) /* highlight overhang */
|
|
|
|
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 + PANEL_PAD + GLYPH_H + TITLE_GAP / 2,
|
|
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. */
|
|
/* Centred on the glyph box: (ROW_H - GLYPH_H) / 2 above it, the
|
|
* same below. Off-by-a-few here is what makes a highlight look
|
|
* like it is sitting on the text rather than around it. */
|
|
ui_fill_rect(x + PANEL_PAD / 2, ry - ROW_LIFT, 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;
|
|
}
|