SchOS/kernel/dev/gfx.c
Nexoniarz 953f9c5c3c branding: one mark, and PSOGM draws it
Guru and SchCotie are gone. Shift is the only mark, and the crash screen draws
it where the face used to be.

Guru was an icon -- a pictogram with a face, denoting one state -- that lived
on the crash screen and nowhere else. Its argument was identity: `:(` is
recognisably Microsoft's, and a crash screen is the single screenshot most
likely to leave the machine, so it is the worst place to be wearing somebody
else's face. That part holds, and it is kept in the branding README because it
is the argument against any second mark. What does not follow is that the
answer is a *new* mark. Shift already signs the system, it is already what the
machine drew on the splash it started with, and a reader who has seen it twice
before the crash recognises it faster than a face they have never seen.

SchCotie was a mascot, and never existed: the file said so itself under "what
this does not cover". A brief for a character nobody is about to draw is not
worth carrying.

So `draw_guru()` and its SVG-space geometry leave panic.c, the two `guru.svg`
files are deleted, and PSOGM calls gfx_draw_logo() -- the same include/logo.h
mask the loader splash and the boot console composite. At its native 128 px:
there is no resampler in the crash path, and the face was scalable in a place
that only ever drew it at one size.

The name stays. "Purple Screen of Guru Meditation" is the Amiga nod, which
panic.c's own header already called out as a separate thing from the mark.

**And taking the screenshot to check it caught a regression from the TTF
console work.** `gfx_draw_char()` -- the pixel-positioned path, which only
PSOGM uses -- still walked `font_data[c][row]` for `row < CHAR_HEIGHT`.
font_data has exactly 16 rows per glyph; CHAR_HEIGHT is the *TrueType* cell
height now, about 21. So every line of the crash report drew five rows of the
next character's bitmap underneath itself, and it looked like each line had
been printed twice. The grid path had been updated when the cell size stopped
being a constant; this one had not, because nothing routine draws text in pixel
coordinates. Both go through one draw_glyph_px() now, and the bitmap's own
geometry is spelled BITMAP_W/BITMAP_H so it cannot be mistaken for a cell size
again.

Two stale claims corrected while in there, in panic.c's header and in
graphics.md: PSOGM has not dimmed-and-floated over the old screen for a while
-- it clears to full purple, and the file's own code comment says why -- and
there is no longer "a face that is obviously a cartoon".

The website's crash section shows the mark in flat white rather than its own
violets, which is what the kernel actually draws: PSOGM composites a coverage
mask in one colour, and those violets sit close enough to #4B1D9A to
half-disappear against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:35:58 +02:00

652 lines
22 KiB
C

// Drawing and the text console, on whatever display is bound. See include/gfx.h
// for why this is not a driver and does not name one.
//
// It was kernel/dev/vga.c, and the name was the problem: panic.c called
// gfx_fill_rect, console.c called gfx_putc, /dev/fb0 called gfx_fb_phys. None
// of this code is VGA-specific -- it is bitmap drawing over a linear
// framebuffer -- and the one part that genuinely was is now the driver, in
// kernel/drivers/gpu/vga/.
//
// The framebuffer arrives through gfx_attach_scanout() rather than being read
// out of the boot block here. That is the whole of the inversion: a driver
// publishes what it owns, and this file draws on it without knowing what it is.
#include "types.h"
#include "x86.h"
#include "memlayout.h"
#include "defs.h"
#include "theme.h"
#include "font.h"
#include "logo.h"
#include "device.h"
#include "display.h"
#include "gfx.h"
#include "vt.h"
#include "text.h"
#include "ttf.h"
#define BACKSPACE 0x100
#define COLOR_BG SCH_BG
// The cell size, and why it is not a constant any more.
//
// It was 8x16, the size of the built-in bitmap font, and everything in this
// file multiplied by it. That font is CP437: 256 glyphs, no Ą, no Ę, no Ś, no
// Ω, no ∞. Those did not come out wrong, they came out as `?`, because the
// grid could not hold a codepoint above 255 and mapped what it could not keep.
//
// So the console draws from a real typeface now, and a real typeface decides
// its own metrics. These start at the bitmap's size because the console has to
// work from the first panic onwards -- before gfx_font_init() has run there is
// no font, and 8x16 against font_data is exactly what there always was.
// The bitmap's own geometry, which is fixed forever: font_data is
// `uchar[256][16]` and each row is eight bits. Naming it separately from the
// cell size is not pedantry -- the two were the same number, then one of them
// started moving, and the code that kept indexing font_data by the moving one
// read straight off the end of every glyph.
#define BITMAP_W 8
#define BITMAP_H 16
static int cell_w = BITMAP_W;
static int cell_h = BITMAP_H;
#define CHAR_WIDTH cell_w
#define CHAR_HEIGHT cell_h
// The typeface, once there is one. `cfont` is the switch: null means fall back
// to the bitmap, which is what early boot and any font failure get.
static struct ttf consttf;
static struct font *cfont;
static int cfont_baseline;
static uint32 blend(uint32 bg, uint32 fg, uint a);
static void draw_glyph_px(int x, int y, uint32 cp, uint32 fg, uint32 bg);
// The scanout, as published by whichever driver owns the display. Zeroed until
// one does, and every entry point checks `fb` before touching memory -- a
// kernel that panics before any display driver has run still has to be able to
// call in here and do nothing gracefully.
static struct fb_info scanout;
static volatile uchar *fb;
static int cursor_x = 0;
static int cursor_y = 0;
// Current fill color for glyph backgrounds, backspace erase, and scroll --
// normally COLOR_BG, but panic() switches this to paint the whole console a
// solid color (e.g. PSOGM's purple) instead of leaving cream-colored boxes
// around each character.
static uint32 gfx_bg = COLOR_BG;
void
gfx_attach_scanout(const struct fb_info *info)
{
scanout = *info;
fb = info->ptr;
// Tell the terminals how big the screen actually is. They were sized before
// this driver existed -- the PCI bus is walked long after the terminals are
// set up -- so until this call every grid is the 80x24 fallback. See
// vt_resize().
vt_resize();
}
const struct fb_info *
gfx_scanout(void)
{
return fb ? &scanout : 0;
}
void
gfx_set_bg(uint32 color)
{
gfx_bg = color;
}
// The mode that was actually set. Exported because the resolution is chosen at
// boot now (press R -- see boot/bios/stage2.S), so nothing above may assume it.
uint32
gfx_width(void)
{
return scanout.width;
}
uint32
gfx_height(void)
{
return scanout.height;
}
void
gfx_draw_pixel(int x, int y, uint32 color)
{
if (!fb || x < 0 || x >= scanout.width || y < 0 || y >= scanout.height)
return;
uint32 offset = (y * scanout.stride) + (x * (scanout.bpp / 8));
if (scanout.bpp == 32) {
*(volatile uint32*)(fb + offset) = color;
} else if (scanout.bpp == 24) {
fb[offset] = color & 0xFF;
fb[offset + 1] = (color >> 8) & 0xFF;
fb[offset + 2] = (color >> 16) & 0xFF;
} else if (scanout.bpp == 16) {
uint16 c16 = (((color >> 19) & 0x1F) << 11) | (((color >> 10) & 0x3F) << 5) | ((color >> 3) & 0x1F);
*(volatile uint16*)(fb + offset) = c16;
}
}
void
gfx_clear(uint32 color)
{
for (int y = 0; y < scanout.height; y++) {
for (int x = 0; x < scanout.width; x++) {
gfx_draw_pixel(x, y, color);
}
}
}
void
gfx_draw_char(int x, int y, uchar c, uint32 color)
{
// Pixel-positioned rather than on the grid -- the crash screen lays itself
// out in pixels because it is a report, not a terminal. Same glyph path as
// the grid uses, which it did not used to be; see draw_glyph_px().
draw_glyph_px(x, y, c, color, gfx_bg);
}
void
gfx_puts(int x, int y, char *s, uint32 color)
{
while (*s) {
gfx_putc((uchar)*s, color);
s++;
}
}
void
gfx_putc(int c, uint32 color)
{
if (!fb) return;
if (c == BACKSPACE || c == 0x08 || c == '\b' || c == 127) {
if (cursor_x >= CHAR_WIDTH) {
cursor_x -= CHAR_WIDTH;
for (int i = 0; i < CHAR_HEIGHT; i++) {
for (int j = 0; j < CHAR_WIDTH; j++) {
gfx_draw_pixel(cursor_x + j, cursor_y + i, gfx_bg);
}
}
}
return;
}
if (c == '\n') {
cursor_x = 0;
cursor_y += CHAR_HEIGHT;
} else if (c == '\r') {
cursor_x = 0;
} else if (c == '\t') {
// Advance to the next multiple of eight columns, erasing as it goes so a
// tab overwrites rather than skipping over stale characters.
//
// There was no tab case at all, so '\t' fell through to gfx_draw_char and
// was drawn as glyph 9 -- which in CP437 is a hollow circle. Pressing Tab
// printed a circle. Same for every other control character: the font has
// a picture for each of them, and none of those pictures is what the
// character means.
int col = cursor_x / CHAR_WIDTH;
int stop = (col + 8) & ~7;
while (col < stop && cursor_x < (int)scanout.width) {
for (int i = 0; i < CHAR_HEIGHT; i++)
for (int j = 0; j < CHAR_WIDTH; j++)
gfx_draw_pixel(cursor_x + j, cursor_y + i, gfx_bg);
cursor_x += CHAR_WIDTH;
col++;
}
} else if (c < 32) {
return; // any other control character: not a glyph
} else {
gfx_draw_char(cursor_x, cursor_y, (uchar)c, color);
cursor_x += CHAR_WIDTH;
}
if (cursor_x >= scanout.width) {
cursor_x = 0;
cursor_y += CHAR_HEIGHT;
}
if (cursor_y > (scanout.height - CHAR_HEIGHT)) {
uint32 line_size = scanout.stride * CHAR_HEIGHT;
uint32 total_size = scanout.stride * scanout.height;
uint32 copy_size = total_size - line_size;
memmove((void*)fb, (void*)(fb + line_size), copy_size);
for (int y = scanout.height - CHAR_HEIGHT; y < scanout.height; y++) {
for (int x = 0; x < scanout.width; x++) {
gfx_draw_pixel(x, y, gfx_bg);
}
}
cursor_y = scanout.height - CHAR_HEIGHT;
}
}
// --- primitives for the text layer ----------------------------------------
//
// kernel/dev/vt.c owns a grid of cells and needs exactly two things the
// console-cursor interface above cannot give it: draw one cell at a grid
// position with its *own* background, and scroll the whole screen without
// consulting a cursor. Everything else a terminal does is bookkeeping on the
// grid, which is not this file's business.
// One cell, addressed in cells rather than pixels, with its own colours.
// gfx_draw_char paints its background from the file-static gfx_bg, which is
// right for a console that has one background and wrong for a terminal where
// every cell carries its own.
// One glyph, at a pixel position, in a cell-sized box.
//
// Both the console grid and the pixel-positioned text in the crash screen come
// through here, which is the point: they used to be two implementations, and
// only one of them was updated when the cell size stopped being 8x16.
// gfx_draw_char() still walked `font_data[c][row]` for `row < CHAR_HEIGHT` --
// and CHAR_HEIGHT is the *TrueType* cell height now, while font_data has
// exactly 16 rows per glyph. Every line of PSOGM therefore drew five rows of
// the *next character's* bitmap underneath itself. It looked like each line
// had been printed twice.
static void
draw_glyph_px(int x, int y, uint32 cp, uint32 fg, uint32 bg)
{
if (!fb)
return;
if (x < 0 || y < 0 ||
x + cell_w > (int)scanout.width || y + cell_h > (int)scanout.height)
return;
// No font yet: the bitmap, exactly as before. Anything it cannot hold is
// drawn as a space rather than as the wrong glyph -- `?` was at least honest
// about being a substitute, and a CP437 picture of something unrelated is
// not.
//
// BITMAP_W/H and not cell_w/h: the bitmap's geometry is a property of
// font_data, not of whatever the console decided a cell is. They agree
// whenever cfont is null, and the assert below is what keeps that true.
if (!cfont) {
uchar g = cp < 256 ? (uchar)cp : ' ';
for (int r = 0; r < BITMAP_H; r++) {
uchar bits = font_data[g][r];
for (int c = 0; c < BITMAP_W; c++)
gfx_draw_pixel(x + c, y + r, (bits & (0x80 >> c)) ? fg : bg);
}
return;
}
// Background first, then the glyph blended over it. The cell is always
// filled: a terminal cell owns its rectangle, and leaving the old pixels
// where a narrow glyph does not reach is how ghosting gets in.
for (int r = 0; r < cell_h; r++)
for (int c = 0; c < cell_w; c++)
gfx_draw_pixel(x + c, y + r, bg);
struct glyph gl;
if (cp == ' ' || cfont->ops->glyph(cfont, cp, &gl) != 0)
return; // no such glyph: the cell stays background
// `cov` is w*h bytes of coverage, 0..255, positioned by the bearing from the
// pen. The pen sits on the baseline at the left of the cell.
int gx = x + gl.bearing_x;
int gy = y + cfont_baseline - gl.bearing_y;
for (int r = 0; r < gl.h; r++) {
int py = gy + r;
if (py < y || py >= y + cell_h)
continue; // clip to the cell: a tall glyph must not
for (int c = 0; c < gl.w; c++) { // bleed into the line above or below
int px = gx + c;
if (px < x || px >= x + cell_w)
continue;
uint a = gl.cov[r * gl.w + c];
if (a == 0)
continue;
gfx_draw_pixel(px, py, a == 255 ? fg : blend(bg, fg, a));
}
}
}
void
gfx_draw_cell(int col, int row, uint32 cp, uint32 fg, uint32 bg)
{
draw_glyph_px(col * cell_w, row * cell_h, cp, fg, bg);
}
// Bring the console typeface up. Called once, early, from the display path.
//
// The font is linked into the kernel (see CONSFONT in the Makefile) because
// the console has to work before there is a filesystem -- a panic during boot
// has to be readable.
//
// A size failure is not fatal and not silent: `cfont` stays null and every
// cell keeps coming from the bitmap, which is what this system has always
// drawn. A console that lost its font would be a much worse outcome than one
// that never had a good one.
void
gfx_font_init(void)
{
extern unsigned char _binary_consfont_start[], _binary_consfont_end[];
uint32 len = (uint32)(_binary_consfont_end - _binary_consfont_start);
struct glyph gl;
if (cfont)
return;
// 16 pixels per em. The bitmap it replaces is 16 tall, so the terminal keeps
// the number of rows it had and nothing that assumed a screenful has to
// change.
if (ttf_open(&consttf, _binary_consfont_start, len, 16) != 0)
return;
struct font *f = ttf_font(&consttf);
if (!f)
return;
// The cell is as wide as the font's own advance. JetBrains Mono is
// monospaced, so measuring one glyph measures all of them -- and measuring
// rather than assuming is what keeps this correct if the font is ever
// changed for another one.
if (f->ops->glyph(f, 'M', &gl) != 0 || gl.advance <= 0)
return;
int w = gl.advance, h = f->ops->height(f);
// Checked *before* they are stored. Writing them first and returning on a
// bad value leaves the console with a cell size no font is drawing at, and
// the bitmap fallback reading past its own 16-row glyphs.
if (w < 4 || w > 64 || h < 6 || h > 64)
return; // implausible metrics: keep the bitmap
cell_w = w;
cell_h = h;
cfont_baseline = f->ops->baseline(f);
cfont = f;
}
// **Nothing here prints.** gfx_font_init() runs before vt_init(), because
// vt_init() has to know how big a cell is before it can work out how many fit
// on the screen -- and by then cons_serial_only may already be clear, so a
// cprintf() would reach vt_current() before there is a current terminal. That
// is a fault during boot with no console to report it on, which is a
// spectacularly unhelpful way to fail.
//
// So main() asks afterwards and says it then.
int
gfx_font_ready(void)
{
return cfont != 0;
}
void
gfx_cell_size(int *w, int *h)
{
if (w) *w = cell_w;
if (h) *h = cell_h;
}
// Move the whole screen up one text row and clear the row it exposes. The
// cursor is not touched: the caller tracks it in cells and knows where it
// wants to be, which is not necessarily where a pixel-scroll would leave it.
void
gfx_scroll_up(uint32 bg)
{
if (!fb || scanout.height < CHAR_HEIGHT)
return;
uint32 line_size = scanout.stride * CHAR_HEIGHT;
uint32 total_size = scanout.stride * scanout.height;
memmove((void*)fb, (void*)(fb + line_size), total_size - line_size);
for (int y = (int)scanout.height - CHAR_HEIGHT; y < (int)scanout.height; y++)
for (int x = 0; x < (int)scanout.width; x++)
gfx_draw_pixel(x, y, bg);
}
void
gfx_screen_reset(void)
{
gfx_bg = COLOR_BG;
gfx_clear(COLOR_BG);
cursor_x = 0;
cursor_y = 0;
}
/* ------------------------------------------------------------------ */
/* Drawing primitives for the crash screen */
/* */
/* The crash screen lays itself out rather than printing, which needs */
/* two things the console never did: filled shapes, and text at an */
/* absolute position that leaves the cursor alone. */
/* ------------------------------------------------------------------ */
int
gfx_ready(void)
{
return fb != 0;
}
void
gfx_dims(int *w, int *h)
{
if (w) *w = (int)scanout.width;
if (h) *h = (int)scanout.height;
}
int gfx_char_w(void) { return CHAR_WIDTH; }
int gfx_char_h(void) { return CHAR_HEIGHT; }
/* ------------------------------------------------------------------ */
/* Framebuffer geometry */
/* */
/* Whatever the attached scanout says, rather than anything read from */
/* the boot block here -- the driver that owns the display is the only */
/* thing that knows where the pixels are. See kernel/dev/fb.c for the */
/* device these back. */
/* ------------------------------------------------------------------ */
uint64 gfx_fb_phys(void) { return scanout.phys; }
// Bytes of framebuffer, measured in scanlines rather than as
// width * height * bpp/8 -- the two differ whenever the card pads its rows,
// and it is the stride that says how far apart two rows really are.
uint64 gfx_fb_size(void)
{
if (!fb)
return 0;
return (uint64)scanout.stride * (uint64)scanout.height;
}
uint32 gfx_fb_stride(void) { return scanout.stride; }
uint32 gfx_fb_bpp(void) { return scanout.bpp; }
volatile uchar *gfx_fb_ptr(void) { return fb; }
/* ------------------------------------------------------------------ */
/* Cursor addressing and erasing */
/* */
/* What an ANSI escape parser needs and gfx_putc alone cannot give it: */
/* the ability to put the cursor somewhere specific and to erase a */
/* region without writing spaces over it. See kernel/core/console.c. */
/* ------------------------------------------------------------------ */
int gfx_cols(void) { return scanout.width ? (int)(scanout.width / CHAR_WIDTH) : 0; }
int gfx_rows(void) { return scanout.height ? (int)(scanout.height / CHAR_HEIGHT) : 0; }
void
gfx_cursor_get(int *col, int *row)
{
if (col) *col = cursor_x / CHAR_WIDTH;
if (row) *row = cursor_y / CHAR_HEIGHT;
}
// Clamped rather than validated: an escape sequence is untrusted input that
// arrives from any program that can write to the terminal, and "CUP to row
// 9999" should put the cursor on the last row, not off the end of the
// framebuffer.
void
gfx_cursor_set(int col, int row)
{
int maxc = gfx_cols(), maxr = gfx_rows();
if (maxc <= 0 || maxr <= 0)
return;
if (col < 0) col = 0;
if (row < 0) row = 0;
if (col >= maxc) col = maxc - 1;
if (row >= maxr) row = maxr - 1;
cursor_x = col * CHAR_WIDTH;
cursor_y = row * CHAR_HEIGHT;
}
// Fill a run of character cells with the background colour.
static void
erase_cells(int col, int row, int count)
{
int maxc = gfx_cols();
while (count-- > 0) {
int x = col * CHAR_WIDTH, y = row * CHAR_HEIGHT;
for (int i = 0; i < CHAR_HEIGHT; i++)
for (int j = 0; j < CHAR_WIDTH; j++)
gfx_draw_pixel(x + j, y + i, gfx_bg);
if (++col >= maxc) { col = 0; row++; }
}
}
// ED -- erase in display. 0: cursor to end, 1: start to cursor, 2: all.
void
gfx_erase_display(int mode)
{
int col, row, maxc = gfx_cols(), maxr = gfx_rows();
gfx_cursor_get(&col, &row);
switch (mode) {
case 0: erase_cells(col, row, (maxr - row) * maxc - col); break;
case 1: erase_cells(0, 0, row * maxc + col + 1); break;
default: gfx_clear(gfx_bg); break;
}
}
// EL -- erase in line. Same three modes, bounded to the current row.
void
gfx_erase_line(int mode)
{
int col, row, maxc = gfx_cols();
gfx_cursor_get(&col, &row);
switch (mode) {
case 0: erase_cells(col, row, maxc - col); break;
case 1: erase_cells(0, row, col + 1); break;
default: erase_cells(0, row, maxc); break;
}
}
// Row pointer computed once per row rather than per pixel. The crash screen
// paints a solid card behind the QR symbol, and at 8 pixels per module that is
// tens of thousands of writes going through gfx_draw_pixel's bounds check and
// multiply.
void
gfx_fill_rect(int x0, int y0, int w, int h, uint32 color)
{
if (!fb)
return;
if (x0 < 0) { w += x0; x0 = 0; }
if (y0 < 0) { h += y0; y0 = 0; }
if (x0 + w > (int)scanout.width) w = (int)scanout.width - x0;
if (y0 + h > (int)scanout.height) h = (int)scanout.height - y0;
if (w <= 0 || h <= 0)
return;
// Only the 32bpp case gets the fast path; anything else is rare enough that
// correctness through gfx_draw_pixel is the better trade.
if (scanout.bpp != 32) {
for (int y = y0; y < y0 + h; y++)
for (int x = x0; x < x0 + w; x++)
gfx_draw_pixel(x, y, color);
return;
}
for (int y = y0; y < y0 + h; y++) {
volatile uint32 *row = (volatile uint32 *)(fb + y * scanout.stride) + x0;
for (int x = 0; x < w; x++)
row[x] = color;
}
}
void
gfx_fill_circle(int cx, int cy, int r, uint32 color)
{
for (int dy = -r; dy <= r; dy++)
for (int dx = -r; dx <= r; dx++)
if (dx * dx + dy * dy <= r * r)
gfx_draw_pixel(cx + dx, cy + dy, color);
}
// Text at an absolute position, leaving the console cursor alone. The crash
// screen lays itself out, so gfx_putc's cursor tracking and scrolling are
// exactly what it does not want.
void
gfx_text_at(int x, int y, const char *s, uint32 color)
{
for (int i = 0; s[i]; i++)
gfx_draw_char(x + i * CHAR_WIDTH, y, (uchar)s[i], color);
}
// --- Logo ------------------------------------------------------------------
//
// The boot splash's logo is drawn by the *loader*, not here -- see splash() in
// boot/bios/stage2main.c. It has to be, because the kernel does not reach gfx_init()
// until roughly the last 300ms of a 2.8s boot: drawing it here put the logo on
// screen for one frame before console_takeover() wiped it, which is worse than
// not having one.
//
// What lives here is the same drawing code against the same mask, for the
// things that want the mark *after* boot -- the crash screen, and eventually
// whatever schinit shows. Keeping one copy of the compositing logic is why this
// is not simply deleted.
// Blend `fg` over `bg` by coverage a (0..255). Per-channel, integer only: this
// runs before any floating point is safe to touch.
static uint32
blend(uint32 bg, uint32 fg, uint a)
{
uint32 r = (((fg >> 16) & 0xFF) * a + ((bg >> 16) & 0xFF) * (255 - a)) / 255;
uint32 g = (((fg >> 8) & 0xFF) * a + ((bg >> 8) & 0xFF) * (255 - a)) / 255;
uint32 b = (((fg ) & 0xFF) * a + ((bg ) & 0xFF) * (255 - a)) / 255;
return (r << 16) | (g << 8) | b;
}
void
gfx_draw_logo(int x, int y, uint32 color, uint32 bg)
{
if (!fb)
return;
for (int dy = 0; dy < LOGO_H; dy++) {
for (int dx = 0; dx < LOGO_W; dx++) {
uint a = LOGO_INK(dy * LOGO_W + dx);
// Skip the background rather than painting it. The mask is a square but
// the mark is a circle, so most of it is empty -- and drawing that empty
// area in bg would stamp a visible box over whatever is underneath.
if (a == 0)
continue;
gfx_draw_pixel(x + dx, y + dy, a == 255 ? color : blend(bg, color, a));
}
}
}
// Centred, for callers that own the whole screen.
void
gfx_logo_centered(int cy, uint32 color, uint32 bg)
{
if (!fb || scanout.width < LOGO_W)
return;
gfx_draw_logo(((int)scanout.width - LOGO_W) / 2, cy - LOGO_H / 2, color, bg);
}