Boot manager: Niveus window, keys and wells, Instrument Sans, pointer, other systems, working power entries

The F8 menu is redrawn in the Niveus language: a window with three lights,
bevelled keys with a lit edge, wells for panels, anti-aliased corners and a
soft shadow, set in Instrument Sans rasterised into include/gfx/nvfont.h
(tools/python/mknvfont.py, make nvfont). Scale doubles on screens 1400 px tall
or more. Lists taller than the screen scroll.

The keyboard still drives everything, and a mouse, touchpad or tablet does
too: both UEFI pointer protocols are polled, hover moves the highlight, a
click activates. The cursor stays hidden until the pointer has really done
something, since the firmware installs a pointer protocol with or without a
mouse. The 'keep this resolution?' countdown is one ten-second budget however
many times the pointer moves.

Loaders found at well-known paths on any volume (Windows, Ubuntu, Debian,
Fedora, Arch, openSUSE, systemd-boot, rEFInd, other BOOTX64.EFI) are listed
and chain-loaded with LoadImage/StartImage. 'Reboot to firmware' used to halt;
it now sets OsIndications and restarts, and Restart and Shut Down call
ResetSystem.

Checked under QEMU+OVMF: menu, resolution list (incl. 2560x1600 at double
scale), system information, shut down, firmware setup, chain-loading a copy of
the loader from a second disk, and a full boot of the ISO to the login prompt.
Pointer hover and click were checked only with synthetic events: in this setup
a USB mouse or tablet delivered no event to the loader (one USB I/O handle
appeared but no pointer protocol came from it), and why was not established.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 15:31:39 +01:00
commit dee3affa3b
11 changed files with 9304 additions and 208 deletions

View file

@ -653,7 +653,7 @@ UEFI_CFLAGS = $(UEFI_DEBUG_FLAGS) --target=x86_64-unknown-windows -ffreestanding
# Hence the check and the message. It is worth the four lines. # Hence the check and the message. It is worth the four lines.
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/arch/schboot.h \ $(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/arch/schboot.h \
boot/common/ui.c boot/common/ui.h \ boot/common/ui.c boot/common/ui.h \
include/gfx/logo.h include/gfx/theme.h include/gfx/font.h include/gfx/logo.h include/gfx/theme.h include/gfx/font.h include/gfx/nvfont.h
@mkdir -p $(BUILD)/uefi @mkdir -p $(BUILD)/uefi
@command -v clang >/dev/null 2>&1 || { \ @command -v clang >/dev/null 2>&1 || { \
echo ""; \ echo ""; \
@ -1775,6 +1775,13 @@ LOGO_PX = 128
logo: logo:
$(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/gfx/logo.h $(LOGO_PX) $(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/gfx/logo.h $(LOGO_PX)
# include/gfx/nvfont.h: the boot manager's typeface (Instrument Sans, from
# docs/Website/niveus/fonts), rasterised once and committed, same arrangement as
# logo.h. Needs Pillow, so it is run by hand: nix-shell -p 'python3.withPackages
# (p: [p.pillow])' --run "make nvfont"
nvfont:
$(PYTHON) tools/python/mknvfont.py docs/Website/niveus/fonts/static include/gfx/nvfont.h
# The USB vendor/product name database, from linux-usb.org. # The USB vendor/product name database, from linux-usb.org.
# #
# Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; SchOS takes the BSD # Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; SchOS takes the BSD
@ -1839,7 +1846,7 @@ qrcheck: $(QRTEST)
.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm \ .PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm \
schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \ schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi logo nvfont qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi
.PRECIOUS: $(BUILD)/%.o .PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives, # 'clean' removes generated output only. dist/ (upstream tarballs) survives,

View file

@ -13,6 +13,7 @@
#include "../../include/gfx/theme.h" #include "../../include/gfx/theme.h"
#include "../../include/gfx/font.h" #include "../../include/gfx/font.h"
#include "../../include/gfx/logo.h" #include "../../include/gfx/logo.h"
#include "../../include/gfx/nvfont.h"
/* "Draw the glyph, leave the background alone." Not a colour anyone can mean: /* "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. */ * the palette is 24-bit, so the high byte is free for a sentinel. */
@ -21,6 +22,25 @@
static struct ui_fb FB; static struct ui_fb FB;
static int ACTIVE; static int ACTIVE;
/* What the mark is blended against: the splash ground, or a window body. */
static ui_u32 LOGO_BG = SCH_BG;
/* The boot manager's scale and faces. 2 on a screen big enough that a 15 px
* face would be a speck; the faces are rasterised at both sizes, not doubled. */
static int SC = 1;
static const struct nvfont *F_BODY, *F_SMALL, *F_TITLE;
static int CUR_ON; /* the pointer is drawn right now */
static void
ui_set_scale(void)
{
SC = (FB.height >= 1400 || FB.width >= 2400) ? 2 : 1;
F_BODY = SC == 2 ? &nvfont_body2 : &nvfont_body1;
F_SMALL = SC == 2 ? &nvfont_small2 : &nvfont_small1;
F_TITLE = SC == 2 ? &nvfont_title2 : &nvfont_title1;
CUR_ON = 0; /* a new framebuffer holds no pointer */
}
/* Progress state. Kept here rather than passed around because the caller ticks /* 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. */ * it from inside a read loop that has no business carrying UI state. */
static ui_u32 PROG_TOTAL, PROG_DONE; static ui_u32 PROG_TOTAL, PROG_DONE;
@ -101,6 +121,7 @@ ui_init(const struct ui_fb *fb)
if (!FB.stride) if (!FB.stride)
FB.stride = FB.width * 4; FB.stride = FB.width * 4;
ACTIVE = 1; ACTIVE = 1;
ui_set_scale();
} }
int ui_active(void) { return ACTIVE; } int ui_active(void) { return ACTIVE; }
@ -236,7 +257,7 @@ ui_logo(int y, int w)
/* Coverage blends the shaded metal against the ground, so the mark /* Coverage blends the shaded metal against the ground, so the mark
* has soft edges and a lit face at the same time. */ * has soft edges and a lit face at the same time. */
put(x0 + dx, y + dy, mix(SCH_BG, shade, (int)cov, 15)); put(x0 + dx, y + dy, mix(LOGO_BG, shade, (int)cov, 15));
} }
} }
} }
@ -280,85 +301,521 @@ ui_splash(const char *subtitle)
ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM); ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM);
} }
/* --- panels --------------------------------------------------------------- /* --- Niveus -----------------------------------------------------------------
* *
* A panel is a plate: a slightly lighter ground than the screen, one lighter * The boot manager is drawn in the same language as the rest of the system
* row along the top and a darker border, which is the whole of the bevel. Two * (docs/Website/niveus): windows are solid and carry three lights, controls are
* rows of shading do more for the impression of a raised surface than any * bevelled keys with light falling across them, fields are wells cut into the
* amount of gradient inside it, and cost nothing to draw. * surface, and the ground under all of it is warm graphite rather than blue.
*
* Every shape here is a rounded rectangle with a vertical gradient, which is
* all that skeuomorphism needs at this size. Corners are anti-aliased by
* supersampling the corner pixels only, and the AA blends against a colour the
* caller *names*, never against what is in the framebuffer: reading the
* framebuffer back is uncached on real hardware and ruinously slow.
*
* The typeface is Instrument Sans, rasterised at build time (nvfont.h).
*/ */
static void
panel(int x, int y, int w, int h) #define NV_BG_TOP 0x1B1B1Eu /* the ground, light at the top */
#define NV_BG_BOT 0x0B0B0Du
#define NV_WIN_BODY 0x232326u
#define NV_WIN_EDGE 0x050507u
#define NV_TITLE_TOP 0x45444Au
#define NV_TITLE_BOT 0x2C2B30u
#define NV_HILITE 0x5E5D64u /* the lit upper edge of a raised thing */
#define NV_KEY_TOP 0x3F3E44u
#define NV_KEY_BOT 0x2D2C32u
#define NV_KEY_EDGE 0x111113u
#define NV_KEY_SHADOW 0x141416u
#define NV_WELL_TOP 0x101012u
#define NV_WELL_BOT 0x171719u
#define NV_WELL_EDGE 0x0A0A0Bu
#define NV_WELL_LIP 0x3A393Eu /* light catching the lower lip of a well */
#define NV_TXT 0xECEAE5u
#define NV_TXT2 0xA9A7A1u
#define NV_TXT3 0x74726Du
#define NV_LIGHT_CLOSE 0xFF5F57u
#define NV_LIGHT_MIN 0xFEBC2Eu
#define NV_LIGHT_ZOOM 0x28C840u
#define S(n) ((n) * SC)
#define WHITE 0xFFFFFFu
#define BLACK 0x000000u
static ui_u32
bg_at(int y)
{ {
ui_fill_rect(x, y, w, h, PANEL_BG); return mix(NV_BG_TOP, NV_BG_BOT, y, (int)FB.height > 1 ? (int)FB.height - 1 : 1);
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 static void
metal_text_centered(int y, const char *s) nv_backdrop(void)
{ {
int len = slen(s); for (int y = 0; y < (int)FB.height; y++)
int x = ((int)FB.width - len * GLYPH_W) / 2; ui_fill_rect(0, y, (int)FB.width, 1, bg_at(y));
}
for (int row = 0; row < GLYPH_H; row++) { static int
ui_u32 shade = metal(row, GLYPH_H); nv_glyph_index(unsigned c)
for (int i = 0; i < len; i++) { {
ui_u8 bits = font_data[(ui_u8)s[i]][row]; if (c < NVFONT_FIRST || c > NVFONT_LAST)
for (int col = 0; col < GLYPH_W; col++) c = '?';
if (bits & (0x80 >> col)) return (int)(c - NVFONT_FIRST);
put(x + i * GLYPH_W + col, y + row, shade); }
static int
nv_width(const struct nvfont *f, const char *s)
{
int w = 0;
for (; *s && *s != '\n'; s++)
w += f->g[nv_glyph_index((ui_u8)*s)].adv;
return (w + 32) >> 6;
}
static int
nv_height(const struct nvfont *f)
{
return f->ascent + f->descent;
}
/* Draw `s` (up to a newline) with its origin on baseline `base`. The colour
* under it is a vertical gradient from bgtop at row y0 to bgbot at y0+h. */
static void
nv_text(const struct nvfont *f, int x, int base, const char *s, ui_u32 fg,
ui_u32 bgtop, ui_u32 bgbot, int y0, int h)
{
int pen = 0; /* 1/64 px: the face's own spacing */
for (; *s && *s != '\n'; s++) {
const struct nvglyph *g = &f->g[nv_glyph_index((ui_u8)*s)];
const ui_u8 *bits = f->bits + g->off;
int gx = x + ((pen + 32) >> 6);
for (int r = 0; r < g->h; r++) {
int py = base + g->oy + r;
ui_u32 bg = mix(bgtop, bgbot, py - y0, h > 1 ? h - 1 : 1);
for (int c = 0; c < g->w; c++) {
ui_u8 a = bits[r * g->w + c];
if (a)
put(gx + g->ox + c, py, mix(bg, fg, a, 255));
}
}
pen += g->adv;
}
}
/* How much of the pixel at (dx,dy) from a corner's centre lies inside a circle
* of radius r, in sixteenths. */
static int
corner_cov(int dx, int dy, int r)
{
int n = 0, R = r * 8;
for (int j = 0; j < 4; j++)
for (int i = 0; i < 4; i++) {
int sx = dx * 8 + 2 * i + 1, sy = dy * 8 + 2 * j + 1;
if (sx * sx + sy * sy <= R * R)
n++;
}
return n;
}
static void
rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot,
ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh)
{
if (w <= 0 || h <= 0)
return;
if (r * 2 > h) r = h / 2;
if (r * 2 > w) r = w / 2;
for (int rr = 0; rr < h; rr++) {
int yy = y + rr;
ui_u32 col = mix(top, bot, rr, h > 1 ? h - 1 : 1);
int cy, corner = 1;
if (rr < r) cy = y + r;
else if (rr >= h - r) cy = y + h - r;
else corner = 0;
if (!corner) {
ui_fill_rect(x, yy, w, 1, col);
continue;
}
ui_u32 bg = mix(bgtop, bgbot, yy - bgy, bgh > 1 ? bgh - 1 : 1);
ui_fill_rect(x + r, yy, w - 2 * r, 1, col);
for (int k = 0; k < r; k++) {
int dy = yy - cy;
int cl = corner_cov(k - r, dy, r);
int cr = corner_cov(k, dy, r);
if (cl) put(x + k, yy, mix(bg, col, cl, 16));
if (cr) put(x + w - r + k, yy, mix(bg, col, cr, 16));
} }
} }
} }
/* Panel metrics. /* A raised object: an edge, a gradient face, and a bright line just inside the
* * top where the light lands. */
* TITLE_H is the whole title band -- the padding above the heading, the static void
* heading itself, and the air beneath it before the rule. It was 34 while the bevel(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot, ui_u32 edge,
* heading alone needs 18 + 16 = 34, so the rule at TITLE_H - 6 was drawn ui_u32 hi, ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh)
* *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. rrect(x, y, w, h, r, edge, edge, bgtop, bgbot, bgy, bgh);
*/ rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r > SC ? r - SC : r,
#define ROW_H 24 top, bot, edge, edge, 0, 1);
#define PANEL_PAD 18 if (hi)
#define TITLE_GAP 14 /* heading to the rule */ ui_fill_rect(x + r, y + SC, w - 2 * r, SC, hi);
#define TITLE_H (PANEL_PAD + GLYPH_H + TITLE_GAP) /* = 48 */ }
#define ROW_LIFT ((ROW_H - GLYPH_H) / 2) /* highlight overhang */
/* A recess: the same thing with the light reversed. Darker at the top where the
* rim shades it, with a pale lip along the bottom edge. */
static void
well(int x, int y, int w, int h, ui_u32 bgcol)
{
int r = S(8);
bevel(x, y, w, h, r, NV_WELL_TOP, NV_WELL_BOT, NV_WELL_EDGE, 0, bgcol, bgcol, 0, 1);
ui_fill_rect(x + r, y + h - SC, w - 2 * r, SC, NV_WELL_LIP);
}
/* The soft shadow beneath a window, as stacked translucent layers laid down
* from the outermost in, each one blended against the backdrop's own gradient. */
static void
shadow(int x, int y, int w, int h, int r)
{
int spread = S(16), dy = S(8);
for (int i = spread; i > 0; i -= SC) {
int t = spread - i;
int a = 60 * t * t / (spread * spread); /* percent black */
int ty = y - i + dy, th = h + 2 * i;
ui_u32 top = mix(bg_at(ty), BLACK, a, 100);
ui_u32 bot = mix(bg_at(ty + th), BLACK, a, 100);
rrect(x - i, ty, w + 2 * i, th, r + i, top, bot,
bg_at(ty), bg_at(ty + th), ty, th);
}
}
static void static void
panel_frame(const char *title, int rows, const char *hint, light(int x, int y, int d, ui_u32 colour, int tb_y, int tb_h)
int *out_x, int *out_y, int *out_w)
{ {
int w = (int)FB.width / 2; ui_u32 top = mix(colour, WHITE, 28, 100);
if (w < 360) w = 360; ui_u32 bot = mix(colour, BLACK, 22, 100);
if (w > (int)FB.width - 60) w = (int)FB.width - 60; ui_u32 rim = mix(colour, BLACK, 62, 100);
rrect(x - SC, y - SC, d + 2 * SC, d + 2 * SC, d / 2 + SC, rim, rim,
NV_TITLE_TOP, NV_TITLE_BOT, tb_y, tb_h);
rrect(x, y, d, d, d / 2, top, bot, rim, rim, 0, 1);
}
int h = TITLE_H + rows * ROW_H + PANEL_PAD * 2; /* --- the pointer ----------------------------------------------------------- */
int x = ((int)FB.width - w) / 2;
int y = ((int)FB.height - h) / 2;
ui_clear(); #define CUR_W 12
#define CUR_H 19
static const char *const CUR_SHAPE[CUR_H] = {
"X ",
"XX ",
"X.X ",
"X..X ",
"X...X ",
"X....X ",
"X.....X ",
"X......X ",
"X.......X ",
"X........X ",
"X.........X ",
"X......XXXXX",
"X...X..X ",
"X..X X..X ",
"X.X X..X ",
"XX X..X ",
"X X..X ",
" X..X ",
" XXX ",
};
static ui_u32 CUR_SAVE[CUR_W * 2 * CUR_H * 2];
static int CUR_X, CUR_Y, CUR_WANT;
/* The mark, small, above the plate: it is the same screen, not a new one. */ static void
int mw = ui_mark_width() * 2 / 3; cursor_hide(void)
int mh = (LOGO_H * mw) / LOGO_W; {
if (y - mh - 24 > 0) if (!CUR_ON || !ACTIVE)
ui_logo(y - mh - 24, mw); return;
int w = CUR_W * SC, h = CUR_H * SC;
for (int r = 0; r < h; r++)
for (int c = 0; c < w; c++) {
int px = CUR_X + c, py = CUR_Y + r;
if (px >= 0 && py >= 0 && (ui_u32)px < FB.width && (ui_u32)py < FB.height)
*(ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4) =
CUR_SAVE[r * w + c];
}
CUR_ON = 0;
}
panel(x, y, w, h); static void
metal_text_centered(y + PANEL_PAD, title); cursor_show(void)
ui_fill_rect(x + PANEL_PAD, y + PANEL_PAD + GLYPH_H + TITLE_GAP / 2, {
w - PANEL_PAD * 2, 1, METAL_EDGE); if (!CUR_WANT || CUR_ON || !ACTIVE)
return;
int w = CUR_W * SC, h = CUR_H * SC;
for (int r = 0; r < h; r++)
for (int c = 0; c < w; c++) {
int px = CUR_X + c, py = CUR_Y + r;
if (px < 0 || py < 0 || (ui_u32)px >= FB.width || (ui_u32)py >= FB.height)
continue;
ui_u32 *p = (ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4);
CUR_SAVE[r * w + c] = *p;
char ch = CUR_SHAPE[r / SC][c / SC];
if (ch == 'X') *p = BLACK;
else if (ch == '.') *p = WHITE;
}
CUR_ON = 1;
}
if (hint) void
ui_text_centered(y + h + 16, hint, SCH_DIM); ui_cursor(int x, int y, int show)
{
cursor_hide();
CUR_X = x; CUR_Y = y; CUR_WANT = show;
cursor_show();
}
*out_x = x; *out_y = y + TITLE_H; *out_w = w; /* --- window chrome --------------------------------------------------------- */
#define TB_H S(34)
#define BODY_PAD S(18)
#define HINT_H S(36)
static void
window(int x, int y, int w, int h, const char *title)
{
int r = S(12), tb = TB_H;
shadow(x, y, w, h, r);
rrect(x, y, w, h, r, NV_WIN_EDGE, NV_WIN_EDGE, bg_at(y), bg_at(y + h), y, h);
rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r - SC, NV_WIN_BODY, NV_WIN_BODY,
NV_WIN_EDGE, NV_WIN_EDGE, 0, 1);
/* The titlebar is rounded on top only: draw it tall enough that its bottom
* corners fall below the line, then lay the body colour over the excess. */
rrect(x + SC, y + SC, w - 2 * SC, tb + r, r - SC, NV_TITLE_TOP, NV_TITLE_BOT,
NV_WIN_EDGE, NV_WIN_EDGE, 0, 1);
ui_fill_rect(x + SC, y + SC + tb, w - 2 * SC, r, NV_WIN_BODY);
ui_fill_rect(x + SC, y + SC + tb - SC, w - 2 * SC, SC, NV_WIN_EDGE);
ui_fill_rect(x + r, y + SC, w - 2 * r, SC, NV_HILITE);
int d = S(12), ly = y + SC + (tb - d) / 2, lx = x + S(18);
light(lx, ly, d, NV_LIGHT_CLOSE, y + SC, tb);
light(lx + S(20), ly, d, NV_LIGHT_MIN, y + SC, tb);
light(lx + S(40), ly, d, NV_LIGHT_ZOOM, y + SC, tb);
int tw = nv_width(F_BODY, title);
int base = y + SC + (tb - nv_height(F_BODY)) / 2 + F_BODY->ascent;
nv_text(F_BODY, x + (w - tw) / 2, base, title, NV_TXT2,
NV_TITLE_TOP, NV_TITLE_BOT, y + SC, tb);
}
static void
footer(int x, int y, int w, int h, const char *hint)
{
if (!hint)
return;
int fy = y + h - HINT_H;
ui_fill_rect(x + S(14), fy, w - S(28), SC, 0x1A1A1Cu);
ui_fill_rect(x + S(14), fy + SC, w - S(28), SC, 0x2E2E32u);
int tw = nv_width(F_SMALL, hint);
int base = fy + (HINT_H - nv_height(F_SMALL)) / 2 + F_SMALL->ascent + SC;
nv_text(F_SMALL, x + (w - tw) / 2, base, hint, NV_TXT3,
NV_WIN_BODY, NV_WIN_BODY, fy, HINT_H);
}
/* --- keys ------------------------------------------------------------------ */
#define MAXKEYS 40
static struct { int x, y, w, h; } KEY[MAXKEYS];
static struct {
const char *title, *hint;
const char *const *items;
int n, sel, scroll, vis, kind; /* kind: 0 a list of keys, 1 a panel */
int has_detail, kh, gap, logo;
int wx, wy, ww, wh;
int lx, ly, lw; /* the list's box */
int ypos[MAXKEYS];
} M;
static int
has_nl(const char *s)
{
for (; *s; s++)
if (*s == '\n')
return 1;
return 0;
}
static const char *
label_of(const char *s)
{
return s[0] == 1 ? s + 1 : s;
}
static const char *
detail_of(const char *s)
{
for (; *s; s++)
if (*s == '\n')
return s + 1;
return 0;
}
static void
paint_key(int i, int sel)
{
if (i < M.scroll || i >= M.scroll + M.vis || i >= MAXKEYS)
return;
int x = KEY[i].x, y = KEY[i].y, w = KEY[i].w, h = KEY[i].h, r = S(8);
const char *item = M.items[i];
const char *label = label_of(item), *detail = detail_of(item);
/* Wipe the ring/shadow a previous state left, then draw this one. */
ui_fill_rect(x - S(3), y - S(3), w + S(6), h + S(7), NV_WIN_BODY);
ui_u32 top, bot, edge, hi, fg, fg2;
if (sel) {
ui_u32 ring = mix(NV_WIN_BODY, SCH_ACCENT, 70, 100);
rrect(x - S(2), y - S(2), w + S(4), h + S(4), r + S(2), ring, ring,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
top = mix(SCH_ACCENT, WHITE, 20, 100);
bot = mix(SCH_ACCENT, BLACK, 24, 100);
edge = mix(SCH_ACCENT, BLACK, 62, 100);
hi = mix(top, WHITE, 45, 100);
fg = WHITE; fg2 = 0xE2DBFFu;
} else {
rrect(x, y + SC, w, h, r, NV_KEY_SHADOW, NV_KEY_SHADOW,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
top = NV_KEY_TOP; bot = NV_KEY_BOT; edge = NV_KEY_EDGE; hi = NV_HILITE;
fg = NV_TXT; fg2 = NV_TXT3;
}
bevel(x, y, w, h, r, top, bot, edge, hi, NV_WIN_BODY, NV_WIN_BODY, 0, 1);
int ty0 = y + SC, th = h - 2 * SC, tx = x + S(18);
if (detail) {
int lh = nv_height(F_BODY), dh = nv_height(F_SMALL);
int total = lh + S(2) + dh;
int t = y + (h - total) / 2;
nv_text(F_BODY, tx, t + F_BODY->ascent, label, fg, top, bot, ty0, th);
nv_text(F_SMALL, tx, t + lh + S(2) + F_SMALL->ascent, detail,
sel ? fg2 : NV_TXT2, top, bot, ty0, th);
} else {
int base = y + (h - nv_height(F_BODY)) / 2 + F_BODY->ascent;
nv_text(F_BODY, tx, base, label, fg, top, bot, ty0, th);
}
if (M.kind == 0 && M.n <= 9 && M.vis == M.n) {
char num[2] = { (char)('1' + i), 0 };
int nw = nv_width(F_SMALL, num);
int base = y + (h - nv_height(F_SMALL)) / 2 + F_SMALL->ascent;
nv_text(F_SMALL, x + w - S(18) - nw, base, num, sel ? fg2 : NV_TXT3,
top, bot, ty0, th);
}
}
static void
menu_layout(void)
{
int n = M.n > MAXKEYS ? MAXKEYS : M.n;
M.n = n;
M.has_detail = 0;
for (int i = 0; i < n; i++)
if (has_nl(M.items[i]))
M.has_detail = 1;
M.gap = S(8);
M.kh = M.has_detail ? S(50) : S(34);
M.ww = S(520);
if (M.ww > (int)FB.width - S(24))
M.ww = (int)FB.width - S(24);
int maxh = (int)FB.height - S(24);
int chrome = TB_H + BODY_PAD * 2 + HINT_H + 2 * SC;
int logoh = S(56) + S(16);
int full;
for (int pass = 0; pass < 2; pass++) {
int y = 0;
for (int i = 0; i < n; i++) {
if (pass == 0 && i && M.items[i][0] == 1)
y += S(14);
M.ypos[i] = y;
y += M.kh + M.gap;
}
full = n ? y - M.gap : 0;
if (pass == 0 && chrome + full <= maxh)
break;
if (pass == 0 && chrome + logoh + full <= maxh)
break;
}
M.logo = 1;
M.vis = n;
int list_h = full;
if (chrome + logoh + full > maxh)
M.logo = 0;
if (chrome + full > maxh) {
int avail = maxh - chrome;
M.vis = (avail + M.gap) / (M.kh + M.gap);
if (M.vis < 1) M.vis = 1;
if (M.vis > n) M.vis = n;
list_h = M.vis * M.kh + (M.vis - 1) * M.gap;
}
M.wh = chrome + (M.logo ? logoh : 0) + list_h;
M.wx = ((int)FB.width - M.ww) / 2;
M.wy = ((int)FB.height - M.wh) / 2;
if (M.wy < 0) M.wy = 0;
M.lx = M.wx + S(20);
M.lw = M.ww - S(40) - (M.vis < n ? S(14) : 0);
M.ly = M.wy + SC + TB_H + BODY_PAD + (M.logo ? logoh : 0);
if (M.sel < M.scroll) M.scroll = M.sel;
if (M.sel >= M.scroll + M.vis) M.scroll = M.sel - M.vis + 1;
if (M.scroll < 0) M.scroll = 0;
if (M.scroll > n - M.vis) M.scroll = n - M.vis;
if (M.scroll < 0) M.scroll = 0;
for (int i = 0; i < n; i++) {
KEY[i].x = M.lx;
KEY[i].w = M.lw;
KEY[i].h = M.kh;
KEY[i].y = M.ly + M.ypos[i] - M.ypos[M.scroll];
}
}
static void
menu_paint(void)
{
cursor_hide();
nv_backdrop();
window(M.wx, M.wy, M.ww, M.wh, M.title);
if (M.logo) {
ui_u32 saved = LOGO_BG;
LOGO_BG = NV_WIN_BODY;
ui_logo(M.wy + SC + TB_H + BODY_PAD, S(56));
LOGO_BG = saved;
}
for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++)
paint_key(i, i == M.sel);
if (M.vis < M.n) {
int tx = M.lx + M.lw + S(8), tw = S(4);
int th = M.vis * M.kh + (M.vis - 1) * M.gap;
rrect(tx, M.ly, tw, th, tw / 2, NV_WELL_TOP, NV_WELL_BOT,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
int thumb = th * M.vis / M.n;
if (thumb < S(16)) thumb = S(16);
int ty = M.ly + (th - thumb) * M.scroll / (M.n - M.vis);
rrect(tx, ty, tw, thumb, tw / 2, NV_KEY_TOP, NV_KEY_BOT,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
}
footer(M.wx, M.wy, M.ww, M.wh, M.hint);
cursor_show();
} }
void void
@ -367,47 +824,111 @@ ui_menu(const char *title, const char *const *items, int n, int selected,
{ {
if (!ACTIVE) if (!ACTIVE)
return; return;
M.title = title; M.items = items; M.n = n; M.sel = selected; M.hint = hint;
M.scroll = 0; M.kind = 0;
menu_layout();
menu_paint();
}
int x, y, w; void
panel_frame(title, n, hint, &x, &y, &w); ui_menu_select(int selected)
{
if (!ACTIVE || M.kind != 0 || selected < 0 || selected >= M.n || selected == M.sel)
return;
int old = M.sel;
M.sel = selected;
if (selected < M.scroll || selected >= M.scroll + M.vis) {
menu_layout();
menu_paint();
return;
}
cursor_hide();
paint_key(old, 0);
paint_key(selected, 1);
cursor_show();
}
int
ui_menu_hit(int x, int y)
{
if (!ACTIVE)
return -1;
for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++)
if (x >= KEY[i].x && x < KEY[i].x + KEY[i].w &&
y >= KEY[i].y && y < KEY[i].y + KEY[i].h)
return i;
return -1;
}
/* A panel: label/value rows in a well, and one button underneath. With no
* values, the labels are a message and are centred. */
static void
box(const char *title, const char *const *labels, const char *const *values,
int n, const char *button, const char *hint)
{
if (!ACTIVE)
return;
int rowh = S(28), wpad = S(12);
int well_h = n * rowh + 2 * wpad;
int btn_h = S(34), btn_w = S(180);
M.title = title; M.hint = hint; M.kind = 1;
M.items = &button; M.n = 1; M.sel = 0; M.scroll = 0; M.vis = 1;
M.ww = S(520);
if (M.ww > (int)FB.width - S(24))
M.ww = (int)FB.width - S(24);
M.wh = TB_H + 2 * SC + BODY_PAD + well_h + S(18) + btn_h + BODY_PAD + HINT_H;
M.wx = ((int)FB.width - M.ww) / 2;
M.wy = ((int)FB.height - M.wh) / 2;
if (M.wy < 0) M.wy = 0;
cursor_hide();
nv_backdrop();
window(M.wx, M.wy, M.ww, M.wh, title);
int wx = M.wx + S(20), ww = M.ww - S(40);
int wy = M.wy + SC + TB_H + BODY_PAD;
well(wx, wy, ww, well_h, NV_WIN_BODY);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
int ry = y + i * ROW_H; int ry = wy + wpad + i * rowh;
int base = ry + (rowh - nv_height(F_BODY)) / 2 + F_BODY->ascent;
if (i == selected) { if (i)
/* The selected row is the only lit thing on the plate. Filling it ui_fill_rect(wx + S(14), ry, ww - S(28), SC, 0x1E1E21u);
* with the accent and putting the label in the ground colour reads if (values) {
* at a glance; an outline does not, at this size. */ nv_text(F_BODY, wx + S(16), base, labels[i], NV_TXT2,
/* Centred on the glyph box: (ROW_H - GLYPH_H) / 2 above it, the NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
* same below. Off-by-a-few here is what makes a highlight look int vw = nv_width(F_BODY, values[i]);
* like it is sitting on the text rather than around it. */ nv_text(F_BODY, wx + ww - S(16) - vw, base, values[i], NV_TXT,
ui_fill_rect(x + PANEL_PAD / 2, ry - ROW_LIFT, w - PANEL_PAD, ROW_H, NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
SCH_ACCENT);
ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE);
} else { } else {
ui_text(x + PANEL_PAD, ry, items[i], SCH_FG, SCH_BG_NONE); int lw = nv_width(F_BODY, labels[i]);
nv_text(F_BODY, wx + (ww - lw) / 2, base, labels[i], NV_TXT,
NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
} }
} }
KEY[0].w = btn_w; KEY[0].h = btn_h;
KEY[0].x = M.wx + (M.ww - btn_w) / 2;
KEY[0].y = wy + well_h + S(18);
M.has_detail = 0;
paint_key(0, 1);
footer(M.wx, M.wy, M.ww, M.wh, hint);
cursor_show();
} }
void void
ui_panel(const char *title, const char *const *labels, ui_panel(const char *title, const char *const *labels,
const char *const *values, int n, const char *hint) const char *const *values, int n, const char *hint)
{ {
if (!ACTIVE) box(title, labels, values, n, "Back", hint);
return; }
int x, y, w; void
panel_frame(title, n, hint, &x, &y, &w); ui_notice(const char *title, const char *const *lines, int n,
const char *button, const char *hint)
for (int i = 0; i < n; i++) { {
int ry = y + i * ROW_H; box(title, lines, 0, n, button, hint);
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 void

View file

@ -77,6 +77,21 @@ void ui_splash(const char *subtitle);
void ui_menu(const char *title, const char *const *items, int n, int selected, void ui_menu(const char *title, const char *const *items, int n, int selected,
const char *hint); const char *hint);
/* Repaint only the keys that changed when the highlight moves (a full paint
* redraws the screen). Returns without effect on a panel. */
void ui_menu_select(int selected);
/* Which key is under a point, or -1. A panel has one key, index 0. */
int ui_menu_hit(int x, int y);
/* The pointer: drawn on top of everything, with what is under it saved and put
* back. Menus hide it while they repaint and show it again afterwards. */
void ui_cursor(int x, int y, int show);
/* A message in the same window as the panels, with one button. */
void ui_notice(const char *title, const char *const *lines, int n,
const char *button, const char *hint);
/* A panel of label/value rows -- the system information screen. */ /* A panel of label/value rows -- the system information screen. */
void ui_panel(const char *title, const char *const *labels, void ui_panel(const char *title, const char *const *labels,
const char *const *values, int n, const char *hint); const char *const *values, int n, const char *hint);

View file

@ -27,3 +27,11 @@ What this will need, in rough order:
The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the
ISO carries a single El Torito EFI entry. ISO carries a single El Torito EFI entry.
## The boot manager
`F8` (or `o` on a serial line) opens a Niveus-styled boot manager, drawn by
`../common/ui.c` in Instrument Sans (`include/gfx/nvfont.h`, regenerate with
`make nvfont`). It takes keyboard and pointer, lists other systems it finds on
the disks and can chain-load them, and has working Firmware Setup, Restart and
Shut Down. See [docs/architecture/boot.md](../../docs/architecture/boot.md#the-boot-manager).

View file

@ -153,17 +153,22 @@ typedef struct {
void *LocateDevicePath; void *LocateDevicePath;
void *InstallConfigurationTable; void *InstallConfigurationTable;
void *LoadImage; EFI_STATUS (*LoadImage)(BOOLEAN BootPolicy, EFI_HANDLE ParentImage,
void *StartImage; void *DevicePath, void *SourceBuffer,
UINTN SourceSize, EFI_HANDLE *ImageHandle);
EFI_STATUS (*StartImage)(EFI_HANDLE ImageHandle, UINTN *ExitDataSize,
CHAR16 **ExitData);
void *Exit; void *Exit;
void *UnloadImage; EFI_STATUS (*UnloadImage)(EFI_HANDLE ImageHandle);
EFI_STATUS (*ExitBootServices)(EFI_HANDLE ImageHandle, UINTN MapKey); EFI_STATUS (*ExitBootServices)(EFI_HANDLE ImageHandle, UINTN MapKey);
void *GetNextMonotonicCount; void *GetNextMonotonicCount;
EFI_STATUS (*Stall)(UINTN Microseconds); EFI_STATUS (*Stall)(UINTN Microseconds);
void *SetWatchdogTimer; void *SetWatchdogTimer;
void *ConnectController; /* Recursive = TRUE asks for every driver that will bind, down the tree. */
EFI_STATUS (*ConnectController)(EFI_HANDLE Controller, EFI_HANDLE *DriverImage,
void *RemainingDevicePath, BOOLEAN Recursive);
void *DisconnectController; void *DisconnectController;
void *OpenProtocol; void *OpenProtocol;
@ -181,6 +186,43 @@ typedef struct {
void **Interface); void **Interface);
} EFI_BOOT_SERVICES; } EFI_BOOT_SERVICES;
// --- runtime services -----------------------------------------------------
//
// Only the two things the boot manager needs: variables, to ask the firmware to
// show its own setup on the next start, and ResetSystem, to restart or power
// off. The slots before them are spelled out as padding so the offsets hold.
#define EfiResetCold 0
#define EfiResetShutdown 2
#define EFI_VARIABLE_NON_VOLATILE 0x1
#define EFI_VARIABLE_BOOTSERVICE_ACCESS 0x2
#define EFI_VARIABLE_RUNTIME_ACCESS 0x4
#define EFI_GLOBAL_VARIABLE_GUID \
{ 0x8BE4DF61, 0x93CA, 0x11D2, { 0xAA, 0x0D, 0x00, 0xE0, 0x98, 0x03, 0x2B, 0x8C } }
/* OsIndications bit 0: "boot into the firmware's own user interface". */
#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x1ULL
typedef struct {
char Hdr[24];
void *GetTime;
void *SetTime;
void *GetWakeupTime;
void *SetWakeupTime;
void *SetVirtualAddressMap;
void *ConvertPointer;
EFI_STATUS (*GetVariable)(CHAR16 *Name, EFI_GUID *Vendor, UINT32 *Attributes,
UINTN *DataSize, void *Data);
void *GetNextVariableName;
EFI_STATUS (*SetVariable)(CHAR16 *Name, EFI_GUID *Vendor, UINT32 Attributes,
UINTN DataSize, void *Data);
void *GetNextHighMonotonicCount;
void (*ResetSystem)(UINTN ResetType, EFI_STATUS Status, UINTN DataSize,
void *ResetData);
} EFI_RUNTIME_SERVICES;
typedef struct { typedef struct {
char Hdr[24]; char Hdr[24];
CHAR16 *FirmwareVendor; CHAR16 *FirmwareVendor;
@ -191,7 +233,7 @@ typedef struct {
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
EFI_HANDLE StandardErrorHandle; EFI_HANDLE StandardErrorHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
void *RuntimeServices; EFI_RUNTIME_SERVICES *RuntimeServices;
EFI_BOOT_SERVICES *BootServices; EFI_BOOT_SERVICES *BootServices;
UINTN NumberOfTableEntries; UINTN NumberOfTableEntries;
void *ConfigurationTable; void *ConfigurationTable;
@ -348,4 +390,42 @@ typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL {
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode; EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL; } EFI_GRAPHICS_OUTPUT_PROTOCOL;
// --- pointers -------------------------------------------------------------
//
// Two protocols, because firmware offers whichever the device is: a mouse or
// touchpad reports movement (SIMPLE_POINTER), a tablet -- QEMU's, and most
// touchscreens -- reports where it is (ABSOLUTE_POINTER). Both are polled.
#define EFI_SIMPLE_POINTER_PROTOCOL_GUID \
{ 0x31878C87, 0x0B75, 0x11D5, { 0x9A, 0x4F, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D } }
#define EFI_ABSOLUTE_POINTER_PROTOCOL_GUID \
{ 0x8D59D32B, 0xC655, 0x4AE9, { 0x9B, 0x15, 0xF2, 0x59, 0x04, 0x99, 0x2A, 0x43 } }
#define EFI_NOT_READY ((EFI_STATUS)(0x8000000000000000ULL | 6))
typedef struct { int RelativeMovementX, RelativeMovementY, RelativeMovementZ;
BOOLEAN LeftButton, RightButton; } EFI_SIMPLE_POINTER_STATE;
typedef struct { UINT64 ResolutionX, ResolutionY, ResolutionZ;
BOOLEAN LeftButton, RightButton; } EFI_SIMPLE_POINTER_MODE;
typedef struct EFI_SIMPLE_POINTER_PROTOCOL {
EFI_STATUS (*Reset)(struct EFI_SIMPLE_POINTER_PROTOCOL *This, BOOLEAN Extended);
EFI_STATUS (*GetState)(struct EFI_SIMPLE_POINTER_PROTOCOL *This,
EFI_SIMPLE_POINTER_STATE *State);
void *WaitForInput;
EFI_SIMPLE_POINTER_MODE *Mode;
} EFI_SIMPLE_POINTER_PROTOCOL;
typedef struct { UINT64 CurrentX, CurrentY, CurrentZ; UINT32 ActiveButtons; }
EFI_ABSOLUTE_POINTER_STATE;
typedef struct { UINT64 AbsoluteMinX, AbsoluteMinY, AbsoluteMinZ,
AbsoluteMaxX, AbsoluteMaxY, AbsoluteMaxZ;
UINT32 Attributes; } EFI_ABSOLUTE_POINTER_MODE;
typedef struct EFI_ABSOLUTE_POINTER_PROTOCOL {
EFI_STATUS (*Reset)(struct EFI_ABSOLUTE_POINTER_PROTOCOL *This, BOOLEAN Extended);
EFI_STATUS (*GetState)(struct EFI_ABSOLUTE_POINTER_PROTOCOL *This,
EFI_ABSOLUTE_POINTER_STATE *State);
void *WaitForInput;
EFI_ABSOLUTE_POINTER_MODE *Mode;
} EFI_ABSOLUTE_POINTER_PROTOCOL;
#endif /* SCHOS_EFI_H */ #endif /* SCHOS_EFI_H */

View file

@ -81,6 +81,8 @@ struct elf64_phdr {
#define PT_LOAD 1 #define PT_LOAD 1
static EFI_SYSTEM_TABLE *ST; static EFI_SYSTEM_TABLE *ST;
static EFI_HANDLE SELF; /* this image */
static EFI_HANDLE BOOT_VOL; /* the volume it was loaded from */
static EFI_BOOT_SERVICES *BS; static EFI_BOOT_SERVICES *BS;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -183,7 +185,20 @@ serial_item(const char *const *items, int i, int sel, int numbered)
} else if (numbered) { } else if (numbered) {
serial_puts(" "); /* keep the column */ serial_puts(" "); /* keep the column */
} }
serial_puts(items[i]); /* The label, and its detail in brackets. Items carry both, with a newline
* between and a leading \001 where a group begins: the screen uses those
* for layout and a serial line has no use for either. */
const char *t = items[i];
if (t[0] == 1)
t++;
for (; *t && *t != '\n'; t++)
serial_putc(*t);
if (*t == '\n') {
serial_puts(" (");
for (t++; *t; t++)
serial_putc(*t);
serial_putc(')');
}
serial_puts("\n"); serial_puts("\n");
} }
@ -360,6 +375,7 @@ open_boot_volume(EFI_HANDLE image)
// is not a thing to do by accident. // is not a thing to do by accident.
st = BS->HandleProtocol(image, &li_guid, (void **)&li); st = BS->HandleProtocol(image, &li_guid, (void **)&li);
if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st); if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st);
BOOT_VOL = li->DeviceHandle;
self_base = (UINT64)(UINTN)li->ImageBase; self_base = (UINT64)(UINTN)li->ImageBase;
self_size = li->ImageSize; self_size = li->ImageSize;
@ -904,6 +920,231 @@ u64_to_dec(UINT64 v, char *out, int cap)
out[i] = 0; out[i] = 0;
} }
/* --- pointers, and one place to wait for input ------------------------------
*
* The boot manager is operated by keyboard, mouse, touchpad or touchscreen, and
* every screen waits through here so that all of them behave the same way.
*
* Both pointer protocols are polled rather than waited on: the loop is already
* polling the keyboard, and an event per device would mean a wait-for-any call
* that most firmware implements badly. A machine with no pointer simply never
* shows a cursor.
*/
#define MAX_PTR 4
static EFI_SIMPLE_POINTER_PROTOCOL *PTR_REL[MAX_PTR];
static EFI_ABSOLUTE_POINTER_PROTOCOL *PTR_ABS[MAX_PTR];
static int PTR_NREL, PTR_NABS;
static int PTR_X, PTR_Y, PTR_DOWN;
static int PTR_RESCANS, PTR_TICKS;
static int PTR_ACTIVE; /* a real event has arrived; until then there is no cursor */
static void u64_to_dec(UINT64 v, char *out, int cap);
#define EV_NONE 0
#define EV_KEY 1
#define EV_HOVER 2 /* the pointer moved onto a key: *item */
#define EV_CLICK 3 /* a key was clicked: *item */
#define PE_MOVED 1
#define PE_DOWN 2
#define PE_UP 4 /* wheel */
#define PE_DOWNW 8
/* Bind every driver to every controller.
*
* The firmware connects only what it needs to boot: the devices that carry a
* console, and the disks. A USB mouse or tablet is neither, so on most
* machines its driver has not been started and no pointer protocol exists yet.
* Doing it here, when the menu opens, costs a second or so on a real machine
* and nothing on a boot that never shows a menu. */
static void
connect_all(void)
{
EFI_HANDLE *h = 0;
UINTN n = 0;
if (EFI_ERROR(BS->LocateHandleBuffer(0 /* AllHandles */, 0, 0, &n, &h)) || !h)
return;
for (UINTN i = 0; i < n; i++)
BS->ConnectController(h[i], 0, 0, 1);
BS->FreePool(h);
}
static void
pointers_find(void)
{
EFI_GUID g1 = EFI_SIMPLE_POINTER_PROTOCOL_GUID;
EFI_GUID g2 = EFI_ABSOLUTE_POINTER_PROTOCOL_GUID;
EFI_HANDLE *h = 0;
UINTN n = 0;
if (!EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &g1, 0, &n, &h)) && h) {
for (UINTN i = 0; i < n && PTR_NREL < MAX_PTR; i++) {
void *p = 0;
if (!EFI_ERROR(BS->HandleProtocol(h[i], &g1, &p)) && p) {
EFI_SIMPLE_POINTER_PROTOCOL *sp = p;
int seen = 0;
for (int k = 0; k < PTR_NREL; k++)
if (PTR_REL[k] == sp) seen = 1;
if (!seen) {
sp->Reset(sp, 0);
PTR_REL[PTR_NREL++] = sp;
}
}
}
BS->FreePool(h);
}
h = 0; n = 0;
if (!EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &g2, 0, &n, &h)) && h) {
for (UINTN i = 0; i < n && PTR_NABS < MAX_PTR; i++) {
void *p = 0;
if (!EFI_ERROR(BS->HandleProtocol(h[i], &g2, &p)) && p) {
EFI_ABSOLUTE_POINTER_PROTOCOL *ap = p;
int seen = 0;
for (int k = 0; k < PTR_NABS; k++)
if (PTR_ABS[k] == ap) seen = 1;
if (!seen) {
ap->Reset(ap, 0);
PTR_ABS[PTR_NABS++] = ap;
}
}
}
BS->FreePool(h);
}
if (PTR_X == 0 && PTR_Y == 0)
PTR_X = PTR_Y = -1; /* centred on first use, when the size is known */
}
static int
pointer_poll(void)
{
int ev = 0, w = (int)ui_width(), h = (int)ui_height();
int down = PTR_DOWN;
for (int i = 0; i < PTR_NREL; i++) {
EFI_SIMPLE_POINTER_STATE st;
if (EFI_ERROR(PTR_REL[i]->GetState(PTR_REL[i], &st)))
continue;
int dx = st.RelativeMovementX, dy = st.RelativeMovementY;
/* A little acceleration: the counts are the device's, not pixels, and
* a fast flick has to be able to cross a 4K panel. */
if (dx > 6 || dx < -6) dx *= 2;
if (dy > 6 || dy < -6) dy *= 2;
if (dx || dy) {
PTR_X += dx; PTR_Y += dy;
ev |= PE_MOVED;
}
if (st.RelativeMovementZ)
ev |= st.RelativeMovementZ < 0 ? PE_UP : PE_DOWNW;
down = st.LeftButton;
}
for (int i = 0; i < PTR_NABS; i++) {
EFI_ABSOLUTE_POINTER_STATE st;
EFI_ABSOLUTE_POINTER_MODE *m = PTR_ABS[i]->Mode;
if (EFI_ERROR(PTR_ABS[i]->GetState(PTR_ABS[i], &st)) || !m)
continue;
UINT64 rx = m->AbsoluteMaxX - m->AbsoluteMinX;
UINT64 ry = m->AbsoluteMaxY - m->AbsoluteMinY;
if (rx && ry) {
PTR_X = (int)(((st.CurrentX - m->AbsoluteMinX) * (UINT64)(w - 1)) / rx);
PTR_Y = (int)(((st.CurrentY - m->AbsoluteMinY) * (UINT64)(h - 1)) / ry);
ev |= PE_MOVED;
}
down = (int)(st.ActiveButtons & 1);
}
if (PTR_X < 0) PTR_X = 0;
if (PTR_Y < 0) PTR_Y = 0;
if (PTR_X >= w) PTR_X = w - 1;
if (PTR_Y >= h) PTR_Y = h - 1;
if (down && !PTR_DOWN)
ev |= PE_DOWN;
PTR_DOWN = down;
return ev;
}
/* Wait for a key, a click on a key, or the pointer moving onto one.
*
* `budget` is a countdown in milliseconds that survives across calls: a
* caller that returns here after a hover continues the same ten seconds
* instead of being given a fresh ten. NULL waits for ever.
*
* The cursor stays hidden until the pointer has actually done something. The
* firmware's console layer installs a pointer protocol whether or not a mouse
* is attached, so "a protocol exists" proves nothing -- and a cursor parked in
* the middle of the menu of a laptop with no mouse is worse than none. */
static int
wait_event(EFI_INPUT_KEY *key, int *item, UINTN *budget)
{
int have = PTR_NREL + PTR_NABS;
int last = -2;
if (PTR_X < 0) {
PTR_X = (int)ui_width() / 2;
PTR_Y = (int)ui_height() / 2;
}
ui_cursor(PTR_X, PTR_Y, PTR_ACTIVE);
for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, key)))
return EV_KEY;
/* USB devices appear some time after their controller is connected,
* and the menu is already on screen by then: look again, twice a
* second for the first ten. */
if (PTR_RESCANS < 20 && ++PTR_TICKS >= 250) {
PTR_TICKS = 0;
PTR_RESCANS++;
pointers_find();
have = PTR_NREL + PTR_NABS;
}
if (have) {
int e = pointer_poll();
if ((e & (PE_MOVED | PE_DOWN)) && !PTR_ACTIVE)
PTR_ACTIVE = 1;
if (e & PE_MOVED || ((e & PE_DOWN) && PTR_ACTIVE))
ui_cursor(PTR_X, PTR_Y, PTR_ACTIVE);
if (e & (PE_UP | PE_DOWNW)) {
key->UnicodeChar = 0;
key->ScanCode = (e & PE_UP) ? 0x01 : 0x02;
return EV_KEY;
}
int hit = ui_menu_hit(PTR_X, PTR_Y);
if ((e & PE_DOWN) && hit >= 0) {
*item = hit;
return EV_CLICK;
}
if ((e & PE_MOVED) && hit >= 0 && hit != last) {
last = hit;
*item = hit;
return EV_HOVER;
}
if (hit < 0)
last = -2;
}
if (budget) {
if (*budget < 2)
return EV_NONE;
*budget -= 2;
}
BS->Stall(2000);
}
}
/* A message and a button. Any key or a click dismisses it. */
static void
notice(const char *title, const char *const *lines, int n, const char *button,
const char *hint)
{
EFI_INPUT_KEY key;
int item = 0;
ui_notice(title, lines, n, button, hint);
mirrors_show();
for (;;) {
int e = wait_event(&key, &item, 0);
if (e == EV_KEY || e == EV_CLICK)
return;
}
}
static void static void
res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap) res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
{ {
@ -916,6 +1157,8 @@ res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p; for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p;
if (i < cap - 1) out[i++] = 'x'; if (i < cap - 1) out[i++] = 'x';
for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p; for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p;
for (const char *p = "\nChange the screen size"; *p && i < cap - 1; p++)
out[i++] = *p;
out[i] = 0; out[i] = 0;
} }
@ -1038,16 +1281,16 @@ res_read_number(const char *prompt)
shown[i] = 0; shown[i] = 0;
const char *rows[1] = { shown }; const char *rows[1] = { shown };
ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept"); ui_notice("Custom resolution", rows, 1, "Accept", "digits, Enter to accept");
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key; EFI_INPUT_KEY key;
for (;;) { int item = 0, e;
if (ST->ConIn && do
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) e = wait_event(&key, &item, 0);
break; while (e == EV_HOVER);
BS->Stall(10000); if (e == EV_CLICK)
} return v;
if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n') if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n')
return v; return v;
if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9') if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9')
@ -1091,15 +1334,21 @@ mode_confirmed(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
uifb.height = gop->Mode->Info->VerticalResolution; uifb.height = gop->Mode->Info->VerticalResolution;
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
ui_init(&uifb); ui_init(&uifb);
ui_menu("Resolution", msg, 2, 0, "going back in 10 seconds"); ui_notice("Resolution", msg, 2, "Keep", "going back in 10 seconds");
if (ST->ConIn) if (ST->ConIn)
while (!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) while (!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
; /* the key that chose the mode is not an answer */ ; /* the key that chose the mode is not an answer */
for (UINTN waited = 0; waited < 10000; waited += 10) { UINTN left = 10000; /* one countdown, however many hovers */
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) for (;;) {
int item = 0;
int e = wait_event(&key, &item, &left);
if (e == EV_NONE)
break;
if (e == EV_CLICK)
return 1;
if (e == EV_KEY)
return key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n'; return key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n';
BS->Stall(10000);
} }
return 0; return 0;
} }
@ -1136,7 +1385,6 @@ static void
res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
struct ui_fb uifb; struct ui_fb uifb;
EFI_INPUT_KEY key;
const char *msg[1] = { "Not kept -- back to the size before" }; const char *msg[1] = { "Not kept -- back to the size before" };
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase; uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
@ -1144,13 +1392,7 @@ res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
uifb.height = gop->Mode->Info->VerticalResolution; uifb.height = gop->Mode->Info->VerticalResolution;
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
ui_init(&uifb); ui_init(&uifb);
ui_menu("Resolution", msg, 1, 0, "any key to go back"); notice("Resolution", msg, 1, "OK", "any key to go back");
mirrors_show();
for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
} }
static void static void
@ -1158,6 +1400,8 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
int sel = 0; int sel = 0;
int echoed = -1; int echoed = -1;
static const char hint[] =
"W/S or arrows to choose, Enter to apply, Esc or Q to go back";
for (;;) { for (;;) {
struct ui_fb uifb; struct ui_fb uifb;
@ -1174,41 +1418,53 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
items[i] = RES_TEXT[i]; items[i] = RES_TEXT[i];
if (sel > RES_N) sel = RES_N; if (sel > RES_N) sel = RES_N;
ui_menu("Resolution", items, RES_N + 1, sel, ui_menu("Resolution", items, RES_N + 1, sel, hint);
"W/S to choose, Enter to apply, Esc or Q to go back");
if (echoed < 0) { if (echoed < 0)
serial_menu("Resolution", items, RES_N + 1, sel, serial_menu("Resolution", items, RES_N + 1, sel, hint, 0);
"W/S to choose, Enter to apply, Esc or Q to go back", 0);
} else if (echoed != sel) {
serial_item(items, sel, sel, 0);
}
echoed = sel; echoed = sel;
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key;
for (;;) { for (;;) {
if (ST->ConIn && EFI_INPUT_KEY key;
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) int item = -1, act = 0;
break; int e = wait_event(&key, &item, 0);
BS->Stall(10000);
} if (e == EV_HOVER || e == EV_CLICK) {
if (item != sel) {
sel = item;
ui_menu_select(sel);
serial_item(items, sel, sel, 0);
mirrors_show();
}
act = e == EV_CLICK;
} else {
CHAR16 c = key.UnicodeChar;
/* Q as well as Esc, for the same reason f8_window() accepts
* 'o' as well as F8: on a serial line a lone ESC byte is
* indistinguishable from the start of an escape sequence, so
* the terminal driver holds it back waiting for the rest and
* it never arrives as a key. Esc is the obvious thing to
* press on a screen and unreachable on a cable. */
if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q')
return;
int to = sel;
if (key.ScanCode == 0x01 || c == L'w' || c == L'W')
to = (sel + RES_N) % (RES_N + 1);
else if (key.ScanCode == 0x02 || c == L's' || c == L'S')
to = (sel + 1) % (RES_N + 1);
else if (c == L'\r' || c == L'\n')
act = 1;
if (to != sel) {
sel = to;
ui_menu_select(sel);
serial_item(items, sel, sel, 0);
mirrors_show();
}
}
if (!act)
continue;
CHAR16 c = key.UnicodeChar;
/* Q as well as Esc, for the same reason f8_window() accepts 'o' as
* well as F8: on a serial line a lone ESC byte is indistinguishable
* from the start of an escape sequence, so the terminal driver holds
* it back waiting for the rest and it never arrives as a key. Esc is
* the obvious thing to press on a screen and unreachable on a cable. */
if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q')
return;
if (key.ScanCode == 0x01 || c == L'w' || c == L'W') {
sel = (sel + RES_N) % (RES_N + 1); continue;
}
if (key.ScanCode == 0x02 || c == L's' || c == L'S') {
sel = (sel + 1) % (RES_N + 1); continue;
}
if (c == L'\r' || c == L'\n') {
if (sel < RES_N) { if (sel < RES_N) {
if (!mode_set_checked(gop, RES_MODE[sel])) { if (!mode_set_checked(gop, RES_MODE[sel])) {
res_refused(gop); res_refused(gop);
@ -1242,14 +1498,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
/* Nothing matched. Saying so beats silently keeping the old mode /* Nothing matched. Saying so beats silently keeping the old mode
* and letting the user wonder whether the key registered. */ * and letting the user wonder whether the key registered. */
const char *msg[1] = { "No such mode -- keeping this one" }; const char *msg[1] = { "No such mode -- keeping this one" };
ui_menu("Resolution", msg, 1, 0, "any key to go back"); notice("Resolution", msg, 1, "OK", "any key to go back");
mirrors_show();
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
return; return;
} }
} }
@ -1505,19 +1754,205 @@ sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key; EFI_INPUT_KEY key;
int item = 0;
for (;;) { for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) int e = wait_event(&key, &item, 0);
if (e == EV_KEY || e == EV_CLICK)
return; return;
BS->Stall(10000);
} }
} }
/* --- other systems ------------------------------------------------------------
*
* A boot manager that can only boot itself is a menu with one item. Other
* operating systems on this machine leave their loader at a known path on an
* EFI system partition, and chain-loading one is the firmware's own
* LoadImage and StartImage: no ExitBootServices of ours is involved, and if the
* other loader gives control back the menu is still here.
*
* Only paths that are well known are probed. Walking every directory of every
* volume would find more and would also find things that are not an operating
* system, and a boot menu that lists a firmware updater is not an improvement.
*/
#define MAX_OTHERS 6
static const struct { const CHAR16 *path; const char *name; } KNOWN_LOADERS[] = {
{ L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi", "Windows Boot Manager" },
{ L"\\EFI\\ubuntu\\shimx64.efi", "Ubuntu" },
{ L"\\EFI\\debian\\shimx64.efi", "Debian" },
{ L"\\EFI\\fedora\\shimx64.efi", "Fedora" },
{ L"\\EFI\\arch\\grubx64.efi", "Arch Linux" },
{ L"\\EFI\\opensuse\\grubx64.efi", "openSUSE" },
{ L"\\EFI\\systemd\\systemd-bootx64.efi", "systemd-boot" },
{ L"\\EFI\\refind\\refind_x64.efi", "rEFInd" },
{ L"\\EFI\\BOOT\\BOOTX64.EFI", "Another EFI loader" },
};
static struct {
EFI_HANDLE vol;
const CHAR16 *path;
char text[96]; /* "name\ndetail", as the menu wants it */
} OTHERS[MAX_OTHERS];
static int OTHER_N;
static void
others_find(void)
{
EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
EFI_HANDLE *h = 0;
UINTN n = 0;
OTHER_N = 0;
if (EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &fs_guid, 0, &n, &h)) || !h)
return;
for (UINTN i = 0; i < n && OTHER_N < MAX_OTHERS; i++) {
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0;
EFI_FILE_PROTOCOL *root = 0;
if (EFI_ERROR(BS->HandleProtocol(h[i], &fs_guid, (void **)&fs)) || !fs)
continue;
if (EFI_ERROR(fs->OpenVolume(fs, &root)) || !root)
continue;
for (UINTN k = 0; k < sizeof KNOWN_LOADERS / sizeof KNOWN_LOADERS[0] &&
OTHER_N < MAX_OTHERS; k++) {
/* Our own volume is probed too -- SchOS shares the ESP with
* Windows on a laptop -- but the fallback path there is this
* loader, and offering to start ourselves again is no help. */
if (h[i] == BOOT_VOL && k == sizeof KNOWN_LOADERS / sizeof KNOWN_LOADERS[0] - 1)
continue;
EFI_FILE_PROTOCOL *f = 0;
if (EFI_ERROR(root->Open(root, &f, (CHAR16 *)KNOWN_LOADERS[k].path,
EFI_FILE_MODE_READ, 0)) || !f)
continue;
f->Close(f);
int o = OTHER_N++;
OTHERS[o].vol = h[i];
OTHERS[o].path = KNOWN_LOADERS[k].path;
int c = 0;
for (const char *q = KNOWN_LOADERS[k].name; *q && c < 40; q++)
OTHERS[o].text[c++] = *q;
OTHERS[o].text[c++] = '\n';
for (const char *q = "Start it from "; *q; q++)
OTHERS[o].text[c++] = *q;
char pn[48];
narrow(KNOWN_LOADERS[k].path, pn, sizeof pn);
for (const char *q = pn; *q && c < (int)sizeof OTHERS[o].text - 1; q++)
OTHERS[o].text[c++] = *q;
OTHERS[o].text[c] = 0;
}
root->Close(root);
}
BS->FreePool(h);
}
/* Start another loader. The device path for LoadImage is the volume's own path
* with one more node on the end naming the file: copy the volume's nodes up to
* its terminator, append a file-path node, and terminate again. */
static void
chain_load(int o)
{
EFI_GUID dp_guid = EFI_DEVICE_PATH_PROTOCOL_GUID;
static UINT8 dp[512];
UINT8 *vol = 0;
UINTN len = 0;
if (EFI_ERROR(BS->HandleProtocol(OTHERS[o].vol, &dp_guid, (void **)&vol)) || !vol)
return;
for (;;) {
UINTN nl = (UINTN)vol[len + 2] | ((UINTN)vol[len + 3] << 8);
if (nl < 4 || len + nl > sizeof dp - 64)
return;
if (vol[len] == 0x7F && vol[len + 1] == 0xFF) /* End of Entire Path */
break;
len += nl;
}
copy(dp, vol, len);
UINTN chars = 0;
while (OTHERS[o].path[chars])
chars++;
UINTN nl = 4 + (chars + 1) * 2;
if (len + nl + 4 > sizeof dp)
return;
dp[len] = 4; dp[len + 1] = 4; /* media, file path */
dp[len + 2] = (UINT8)nl; dp[len + 3] = (UINT8)(nl >> 8);
copy(dp + len + 4, OTHERS[o].path, (chars + 1) * 2);
len += nl;
dp[len] = 0x7F; dp[len + 1] = 0xFF; dp[len + 2] = 4; dp[len + 3] = 0;
EFI_HANDLE img = 0;
if (EFI_ERROR(BS->LoadImage(0, SELF, dp, 0, 0, &img)) || !img) {
const char *msg[1] = { "The firmware would not load it." };
notice("Start another system", msg, 1, "OK", "any key to go back");
return;
}
BS->StartImage(img, 0, 0);
BS->UnloadImage(img);
}
/* Ask the firmware to open its own setup on the next start, then restart.
*
* This used to halt with "reset to reach firmware setup", which is the screen
* of a loader that never learned the protocol. The firmware publishes a
* variable for exactly this: OsIndicationsSupported says what it can do and
* OsIndications is where the request goes. Not every firmware has it, and the
* ones that do not are told so rather than restarted for nothing. */
static void
firmware_setup(void)
{
EFI_RUNTIME_SERVICES *rt = ST->RuntimeServices;
EFI_GUID gv = EFI_GLOBAL_VARIABLE_GUID;
UINT64 supported = 0, cur = 0;
UINTN sz = sizeof supported;
UINT32 attr = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS;
if (EFI_ERROR(rt->GetVariable(L"OsIndicationsSupported", &gv, 0, &sz,
&supported)) ||
!(supported & EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) {
const char *msg[2] = { "This firmware cannot be asked to open its",
"setup. Use its own key when powering on." };
notice("Firmware Setup", msg, 2, "OK", "any key to go back");
return;
}
UINT32 a = 0;
sz = sizeof cur;
if (EFI_ERROR(rt->GetVariable(L"OsIndications", &gv, &a, &sz, &cur)))
cur = 0;
cur |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI;
if (EFI_ERROR(rt->SetVariable(L"OsIndications", &gv, attr, sizeof cur, &cur))) {
const char *msg[1] = { "The firmware refused the request." };
notice("Firmware Setup", msg, 1, "OK", "any key to go back");
return;
}
rt->ResetSystem(EfiResetCold, 0, 0, 0);
for (;;) __asm__ volatile("hlt");
}
static void
power(UINTN how)
{
ST->RuntimeServices->ResetSystem(how, 0, 0, 0);
for (;;) __asm__ volatile("hlt");
}
enum { A_BOOT, A_VERBOSE, A_OTHER, A_DISPLAY, A_INFO, A_FIRMWARE, A_RESTART,
A_SHUTDOWN, A_DEBUG };
static int static int
boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
int selected = 0; int selected = 0;
int echoed = -1; // the serial copy: header once, then moves int echoed = -1; // the serial copy: header once, then moves
char resbuf[48]; char resbuf[96];
connect_all();
pointers_find();
others_find();
for (;;) { for (;;) {
/* Re-read the framebuffer every pass: choosing a resolution moves it, /* Re-read the framebuffer every pass: choosing a resolution moves it,
@ -1532,29 +1967,38 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
res_row(gop, resbuf, sizeof resbuf); res_row(gop, resbuf, sizeof resbuf);
// Six entries in a debug build, five in a release one.
//
// The debugger entry is compiled out rather than hidden, because a // The debugger entry is compiled out rather than hidden, because a
// menu item that is present and refuses is an invitation to work out // menu item that is present and refuses is an invitation to work out
// how to make it not refuse. An ISO built for anybody else has no way // how to make it not refuse. An ISO built for anybody else has no way
// to turn a network debugger on, because the code that would set the // to turn a network debugger on, because the code that would set the
// flag is not in it. // flag is not in it.
const char *items[6]; //
int nitems = 5; // An item is a label, then a newline and a line of detail; a leading
items[0] = "Boot SchOS"; // \001 starts a new group, drawn with a gap above it. `what` says what
items[1] = "Boot SchOS (verbose)"; // each row does, because the number of rows is not fixed: other
items[2] = resbuf; // systems found on the disks are listed after the two ways of
items[3] = "System Information"; // starting this one.
items[4] = "Reboot to firmware"; const char *items[8 + MAX_OTHERS];
int what[8 + MAX_OTHERS], arg[8 + MAX_OTHERS];
int nitems = 0;
#define ITEM(text, a, x) do { items[nitems] = (text); what[nitems] = (a); arg[nitems] = (x); nitems++; } while (0)
ITEM("Boot SchOS\nStart the system", A_BOOT, 0);
ITEM("Boot SchOS (verbose)\nShow kernel messages while starting", A_VERBOSE, 0);
for (int o = 0; o < OTHER_N; o++)
ITEM(OTHERS[o].text, A_OTHER, o);
ITEM(resbuf, A_DISPLAY, 0);
ITEM("System Information\nFirmware, processor and memory", A_INFO, 0);
ITEM("\001Firmware Setup\nRestart into the UEFI settings", A_FIRMWARE, 0);
ITEM("Restart\nReboot the machine", A_RESTART, 0);
ITEM("Shut Down\nPower the machine off", A_SHUTDOWN, 0);
#ifdef SCHOS_DEBUG #ifdef SCHOS_DEBUG
items[5] = DEBUG_BOOT ? "Remote debugging: on (schdbgd, port 4444)" ITEM(DEBUG_BOOT ? "Remote debugging: on\nschdbgd, port 4444"
: "Remote debugging: off"; : "Remote debugging: off\nschdbgd, port 4444", A_DEBUG, 0);
nitems = 6;
#endif #endif
const char *hint = nitems == 6 ? "W/S or 1-6 to choose, Enter to select" const char *hint = "W/S or arrows to choose, Enter to select - number keys jump";
: "W/S or 1-5 to choose, Enter to select"; if (selected >= nitems) selected = 0;
ui_menu("Boot Manager", items, nitems, selected, hint); ui_menu("SchOS Boot Manager", items, nitems, selected, hint);
// The same menu on the serial line: the whole thing once, then one // The same menu on the serial line: the whole thing once, then one
// line per *move*. Reprinting on every keypress would be correct and // line per *move*. Reprinting on every keypress would be correct and
@ -1562,46 +2006,58 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
// unrecognised key would scroll the selection past again for nothing. // unrecognised key would scroll the selection past again for nothing.
if (echoed < 0) { if (echoed < 0) {
serial_menu("Boot Manager", items, nitems, selected, hint, 1); serial_menu("Boot Manager", items, nitems, selected, hint, 1);
} else if (echoed != selected) {
serial_item(items, selected, selected, 1);
} }
echoed = selected; echoed = selected;
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key;
CHAR16 c = 0;
UINT16 scan = 0;
for (;;) { for (;;) {
if (ST->ConIn && EFI_INPUT_KEY key;
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) { int item = -1, act = 0;
c = key.UnicodeChar; scan = key.ScanCode; break; int e = wait_event(&key, &item, 0);
int to = selected;
if (e == EV_HOVER || e == EV_CLICK) {
to = item;
act = e == EV_CLICK;
} else {
CHAR16 c = key.UnicodeChar;
if (key.ScanCode == 0x01 || c == L'w' || c == L'W')
to = (selected + nitems - 1) % nitems;
else if (key.ScanCode == 0x02 || c == L's' || c == L'S')
to = (selected + 1) % nitems;
else if (c >= L'1' && c <= L'9' && (int)(c - L'1') < nitems)
to = (int)(c - L'1');
else if (c == L'\r' || c == L'\n')
act = 1;
} }
BS->Stall(10000);
}
if (scan == 0x01) { selected = (selected + nitems - 1) % nitems; continue; } if (to != selected) {
if (scan == 0x02) { selected = (selected + 1) % nitems; continue; } selected = to;
if (c == L'w' || c == L'W') { selected = (selected + nitems - 1) % nitems; continue; } ui_menu_select(selected);
if (c == L's' || c == L'S') { selected = (selected + 1) % nitems; continue; } serial_item(items, selected, selected, 1);
if (c >= L'1' && c <= L'0' + nitems) { selected = (int)(c - L'1'); continue; } echoed = selected;
mirrors_show();
}
if (!act)
continue;
if (c == L'\r' || c == L'\n') { switch (what[selected]) {
if (selected == 0) return 0; case A_BOOT: return 0;
if (selected == 1) return 1; case A_VERBOSE: return 1;
if (selected == 2) { res_choose(gop); continue; } case A_OTHER: chain_load(arg[selected]); break;
if (selected == 3) { sys_info(gop); continue; } case A_DISPLAY: res_choose(gop); break;
case A_INFO: sys_info(gop); break;
case A_FIRMWARE: firmware_setup(); break;
case A_RESTART: power(EfiResetCold); break;
case A_SHUTDOWN: power(EfiResetShutdown); break;
#ifdef SCHOS_DEBUG #ifdef SCHOS_DEBUG
// A toggle rather than an action: picking it changes what the // A toggle rather than an action: picking it changes what the
// entry says and leaves you in the menu, so the state you booted // entry says and leaves you in the menu, so the state you booted
// with is the state you can see before you boot. // with is the state you can see before you boot.
if (selected == 5) { DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; continue; } case A_DEBUG: DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; break;
#endif #endif
if (selected == 4) {
ui_clear();
ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup",
0xB9BEC7u);
for (;;) __asm__ volatile("hlt");
} }
break; // repaint: the thing that ran may have changed the screen
} }
} }
} }
@ -2180,6 +2636,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{ {
ST = systab; ST = systab;
BS = systab->BootServices; BS = systab->BootServices;
SELF = image;
print(L"SchOS UEFI loader\r\n"); print(L"SchOS UEFI loader\r\n");

View file

@ -330,9 +330,64 @@ Dropping back to protected mode to re-enter through the 32-bit multiboot path
is a mode switch whose failure mode is a triple fault with no console. The is a mode switch whose failure mode is a triple fault with no console. The
32-bit entry is still present in `entry.S` and now has no caller. 32-bit entry is still present in `entry.S` and now has no caller.
### The boot manager
F8 (or `o` on a serial line) opens the boot manager. It used to be a text list
on a dark plate; it is now drawn in the Niveus language the site is drawn in
(`docs/Website/niveus`): a window with three lights, keys with a lit upper edge
and a shadow beneath, panels cut into the surface as wells, a graphite ground
that darkens towards the bottom, and the mark above the list.
What makes that possible in a loader is that nothing in it is large:
- **Shapes** are rounded rectangles with a vertical gradient. Corners are
anti-aliased by supersampling only the corner pixels, against a colour the
caller names. Nothing reads the framebuffer back, because that memory is
uncached on real hardware and a read per pixel is the difference between a
menu that appears and one that crawls. (The one exception is the pointer,
which saves sixteen by nineteen pixels.)
- **Type** is Instrument Sans, the face Niveus ships. A TrueType rasteriser
does not belong in a loader, so `tools/python/mknvfont.py` renders it once
into `include/gfx/nvfont.h` as 8-bit coverage bitmaps with fractional
advances, at the size for ordinary screens and at twice that for screens 1400
pixels tall or more. Rasterising at the real size is what keeps it crisp;
doubling pixels does not.
- **Layout** is computed from the item list, not drawn from fixed positions: a
list taller than the screen drops the mark, then scrolls with a bar.
An entry is a label, a newline and a line of detail. The entries are:
| Entry | Does |
|---|---|
| Boot SchOS / (verbose) | the two ways of starting this system |
| other systems | one entry per loader found at a well-known path on any volume: Windows Boot Manager, Ubuntu, Debian, Fedora, Arch, openSUSE, systemd-boot, rEFInd, and any other `\EFI\BOOT\BOOTX64.EFI` that is not this one. Chosen, they are started with the firmware's own `LoadImage`/`StartImage`, and if the other loader returns, the menu is still here |
| Resolution | the list of modes the firmware offers, or a typed size, with the "keep this?" confirmation |
| System Information | firmware, UEFI revision, processor, memory, display, ACPI |
| Firmware Setup | sets `OsIndications` and restarts into the firmware's own settings; on firmware that does not support it, says so instead of restarting for nothing |
| Restart, Shut Down | `ResetSystem` |
| Remote debugging | `make DEBUG=1` builds only |
Keyboard, mouse, touchpad and touchscreen all drive it: both UEFI pointer
protocols are polled (a mouse reports movement, a tablet reports position),
hover moves the highlight, a click activates, and the wheel scrolls. All input
goes through one function, `wait_event()`, so every screen behaves the same way.
The cursor stays hidden until the pointer has actually done something, because
the firmware's console layer installs a pointer protocol whether or not a mouse
is attached. USB devices appear some time after their controller is connected
and the menu is already up by then, so the loader connects every controller when
the menu opens and looks for pointers again twice a second for ten seconds.
The "keep this resolution?" confirmation counts ten seconds down once, however
many times the pointer moves over the button: a person with a garbled screen
waving a mouse around must not be able to keep postponing the revert.
**Not verified:** hover and click were exercised with synthetic events. Under
QEMU and OVMF a USB mouse or tablet never delivered an event to the loader, so
the pointer path has not been seen working on a device -- real hardware first.
### The menu is on the serial line too ### The menu is on the serial line too
The three F8 screens paint into the framebuffer, which is the right place for The screens paint into the framebuffer, which is the right place for
them on a machine with a screen and nowhere at all on one driven over a serial them on a machine with a screen and nowhere at all on one driven over a serial
cable — which is how this loader is usually driven. `f8_window()` already went cable — which is how this loader is usually driven. `f8_window()` already went
out of its way to support that, accepting `o` as well as F8 because *"a serial out of its way to support that, accepting `o` as well as F8 because *"a serial
@ -351,7 +406,7 @@ its text across the menu that was just painted there.
Two details that are not cosmetic: Two details that are not cosmetic:
- **The item number is shown only where it is a key you can press.** The boot - **The item number is shown only where it is a key you can press.** The boot
menu takes 1–5. The resolution list takes only W/S and Enter and is longer menu takes 1–9. The resolution list takes only W/S and Enter and is longer
than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a
character nobody can type, offering to select something. character nobody can type, offering to select something.
- **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is - **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is

View file

@ -29,10 +29,10 @@ asked for one. That is the right default and it has one bad consequence —
to enable it.** A laptop with no serial port that stops before it can draw has to enable it.** A laptop with no serial port that stops before it can draw has
nowhere to say why, and `schinitd enable` needs a prompt that does not exist. nowhere to say why, and `schinitd enable` needs a prompt that does not exist.
So a build made with `make DEBUG=1` has a sixth entry in the F8 menu: So a build made with `make DEBUG=1` has one more entry at the end of the F8 menu:
``` ```
> 6. Remote debugging: off > Remote debugging: off
``` ```
Picking it toggles the entry, booting carries the choice through Picking it toggles the entry, booting carries the choice through

View file

@ -82,15 +82,21 @@ Nothing here starts before [1.0.md](1.0.md)'s register is empty, except where th
## 5b. The boot manager ## 5b. The boot manager
The loader ([boot.md](../architecture/boot.md)) is a text menu: a splash, "Press F8", a list of options. UEFI gives much more: The loader ([boot.md](../architecture/boot.md#the-boot-manager)) is now a Niveus-styled window: bevelled keys, wells, Instrument Sans,
a graphics-output framebuffer at the panel's native mode, a pointer protocol, file systems to read themes from. The plan is a keyboard and pointer, a list of other systems it finds on the disks and can chain-load, the F8 options (resolution with its
boot manager that is worth looking at — in the **Niveus / skeuomorphic** style the site and the wallpapers already use (the "keep this?" confirmation, verbose boot), and real Firmware Setup, Restart and Shut Down.
Shift mark, real materials, depth) — and usable: a mouse and keyboard, a list of what can be booted (this ISO, a SchFS
partition, other operating systems' loaders), the options that live in the F8 menu now (resolution with its "keep this?" Still to do:
confirmation, verbose boot, drivers off), and an honest failure screen. It is its own piece of work in `boot/uefi/`
(a small text and image renderer in the loader — the kernel's TrueType rasteriser is not available yet at that point) and it - [ ] boot entries from a file on the ESP (several kernels, command lines, "drivers off", a default and a timeout), instead of
replaces the boot-time display handling that grew by accretion (GOP mode choice, the "keep this resolution" probe, the font scaling the two fixed ways of starting this system
in the kernel) with one design. - [ ] a SchFS partition as a boot source, not just the volume the loader came from
- [ ] an honest failure screen for the loader's own errors (today `die()` prints to the console)
- [ ] the pointer path verified on a real machine; QEMU with OVMF never delivered a USB mouse event to the loader
- [ ] light appearance, and a theme read from the ESP
It is its own piece of work in `boot/uefi/`; the kernel's TrueType rasteriser is not available at that point, which is why the
loader carries a pre-rasterised face (`include/gfx/nvfont.h`).
### The installer and `.sip` ### The installer and `.sip`

7867
include/gfx/nvfont.h Normal file

File diff suppressed because it is too large Load diff

80
tools/python/mknvfont.py Normal file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Rasterise Niveus's interface face into a header the boot manager can draw.
The loader cannot carry a TrueType rasteriser, and the answer to that has been
the same since include/gfx/font.h: do the work at build time and commit the
result. This takes Instrument Sans (SIL OFL, see docs/Website/niveus/fonts/)
at three sizes and writes 8-bit coverage bitmaps, one per printable ASCII
character, with the metrics needed to lay them out proportionally.
python3 tools/python/mknvfont.py docs/Website/niveus/fonts/static include/gfx/nvfont.h
Needs Pillow; nothing else, and nothing at all at build time of the OS.
"""
import sys, os
from PIL import ImageFont
# Each size twice: x1 for ordinary screens and x2 for the ones where a 15 px
# face would be a speck. Rendering at the real size beats doubling pixels.
SIZES = []
for _scale in (1, 2):
SIZES += [
("body%d" % _scale, "InstrumentSans-Medium.ttf", 15 * _scale),
("small%d" % _scale, "InstrumentSans-Regular.ttf", 12 * _scale),
("title%d" % _scale, "InstrumentSans-SemiBold.ttf", 24 * _scale),
]
FIRST, LAST = 32, 126
def render(path, px):
f = ImageFont.truetype(path, px)
asc, desc = f.getmetrics()
glyphs, blob = [], bytearray()
for code in range(FIRST, LAST + 1):
ch = chr(code)
adv = int(round(f.getlength(ch) * 64)) # 26.6 fixed point: whole-pixel advances make text look cramped
mask, (ox, oy) = f.getmask2(ch, mode="L", anchor="ls")
w, h = mask.size
data = bytes(mask) if (w and h) else b""
glyphs.append((len(blob), w, h, ox, oy, adv))
blob += data
return asc, desc, glyphs, blob
def main():
src, out = sys.argv[1], sys.argv[2]
lines = [
"#pragma once",
"",
"// GENERATED by tools/python/mknvfont.py -- do not edit. Run `make nvfont`.",
"// source: docs/Website/niveus/fonts/static (Instrument Sans, SIL OFL 1.1)",
"//",
"// 8-bit coverage bitmaps for printable ASCII, proportionally spaced, used by",
"// the boot manager (boot/common/ui.c). Not included by the kernel.",
"",
"struct nvglyph { unsigned short off; unsigned char w, h; signed char ox, oy; unsigned short adv; /* 1/64 px */ };",
"struct nvfont { unsigned char ascent, descent; const struct nvglyph *g; const unsigned char *bits; };",
"#define NVFONT_FIRST %d" % FIRST,
"#define NVFONT_LAST %d" % LAST,
"",
]
for name, fn, px in SIZES:
asc, desc, glyphs, blob = render(os.path.join(src, fn), px)
lines.append("static const struct nvglyph nvg_%s[] = {" % name)
for (o, w, h, ox, oy, adv) in glyphs:
lines.append(" {%d,%d,%d,%d,%d,%d}," % (o, w, h, ox, oy, adv))
lines.append("};")
lines.append("static const unsigned char nvb_%s[] = {" % name)
for i in range(0, len(blob), 24):
lines.append(" " + ",".join(str(b) for b in blob[i:i + 24]) + ",")
if not blob:
lines.append(" 0")
lines.append("};")
lines.append("static const struct nvfont nvfont_%s = { %d, %d, nvg_%s, nvb_%s };" %
(name, asc, desc, name, name))
lines.append("")
open(out, "w").write("\n".join(lines) + "\n")
print("wrote", out)
main()