Files
SUB-OS/scripts/mklogo.py
T
subhobhai943andClaude Opus 5 ba06264d12 feat(boot): Add SUB-OS boot logo and splash, share the console font, and lift the kernel size ceiling
Boot logo (init/bootlogo.c, init/logo_data.c, scripts/mklogo.py):
  The logo appears in two forms because the framebuffer does not exist for the
  first third of boot. An ASCII rendition goes to the text console and serial
  line from the first moments; once the video driver is up at step [5/14] the
  real bitmap takes over as a graphical splash with a progress bar tied to the
  actual boot stages.

  The asset is 4-bit palette indexed, so 160x160 pixels cost 12.5 KB instead of
  100 KB, and carries a transparency mask so the mark composites onto the
  gradient rather than stamping a flat rectangle over it. scripts/mklogo.py
  regenerates init/logo_data.c from the source PNG.

  Boot reaches the desktop in well under a second, so the splash is held for a
  minimum duration and the bar sweeps to 100% during the hold; without that the
  logo would flash past unseen. `splashtime=<ms>` tunes it and `nosplash`,
  `nogui` or `text` skip it.

Bootloader (boot/stage2.asm):
  stage2 staged the whole kernel in conventional RAM before copying it to 1 MB,
  which capped the kernel at roughly 576 KB before the load ran into video
  memory at 0xA0000. The logo pushed it over that line and the kernel stopped
  booting. Each 32 KB chunk now goes into one fixed low buffer and is copied
  straight to its final address, using unreal mode for the 32-bit reach. The
  ceiling is now the image itself, 1.375 MB.

Console font (lib/font8x8.c):
  fb_draw_char was a stub that ignored its character argument and drew a filled
  rectangle, which is why the splash rendered its text as solid blocks. It now
  renders real glyphs, and the table it uses is shared: the framebuffer console,
  the canvas rasterizer and the desktop compositor had three divergent private
  copies, two of which still carried the `// \` line-continuation defect.

printk console sink (kernel/printk.c):
  printk output can be redirected to a callback, so a caller can run a subsystem
  that reports via printk and capture what it printed. Serial and the dmesg ring
  still see everything, which keeps a captured run debuggable.

Shell builtins (userland/lazybox/shell.c):
  The builtin chain is extracted into shell_execute_builtin(), so the kernel TTY
  and the desktop terminal accept the same vocabulary instead of the GUI seeing
  only the applet table. Callers that cannot be suspended pass allow_blocking =
  false to refuse the animating ones.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L5yKJT4Wo2cV3rfPrwY5MP
2026-08-25 01:37:20 +05:30

155 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate the SUB-OS boot logo asset from a source image.
Emits init/logo_data.c: a 4-bit palette-indexed bitmap plus the ARGB palette
and an ASCII-art rendition used before the framebuffer is up.
python3 scripts/mklogo.py ~/Pictures/logo_SUB.png
Re-run this whenever the source artwork changes; init/logo_data.c is generated
and should not be hand-edited.
"""
import sys
import os
from PIL import Image
SIZE = 160 # Bitmap edge length in pixels
COLORS = 16 # Palette entries, one nibble per pixel
BACKGROUND = (11, 19, 43) # 0xFF0B132B, the desktop wallpaper's top gradient stop
ASCII_W = 46 # Text-mode logo width in characters
ASCII_RAMP = " .:-=+*#%@"
def build_bitmap(path):
img = Image.open(path).convert("RGBA")
# Composite onto the wallpaper colour so the logo's transparent surround
# blends into the splash instead of turning black.
canvas = Image.new("RGB", img.size, BACKGROUND)
canvas.paste(img, (0, 0), img)
canvas = canvas.resize((SIZE, SIZE), Image.LANCZOS)
# Dithering would scatter isolated palette indices through flat areas and
# look like noise at this size, so quantize with it disabled.
quant = canvas.quantize(colors=COLORS, dither=Image.NONE)
raw_palette = quant.getpalette()[: COLORS * 3]
palette = [tuple(raw_palette[i * 3 : i * 3 + 3]) for i in range(COLORS)]
indices = list(quant.getdata())
return palette, indices
def build_ascii(path):
img = Image.open(path).convert("RGBA")
canvas = Image.new("RGB", img.size, BACKGROUND)
canvas.paste(img, (0, 0), img)
# Console cells are about twice as tall as they are wide.
height = max(1, ASCII_W // 2)
small = canvas.convert("L").resize((ASCII_W, height), Image.LANCZOS)
pixels = list(small.getdata())
lo, hi = min(pixels), max(pixels)
span = max(1, hi - lo)
rows = []
for y in range(height):
row = []
for x in range(ASCII_W):
level = (pixels[y * ASCII_W + x] - lo) * (len(ASCII_RAMP) - 1) // span
row.append(ASCII_RAMP[level])
rows.append("".join(row).rstrip())
while rows and not rows[0]:
rows.pop(0)
while rows and not rows[-1]:
rows.pop()
return rows
def transparent_mask(palette):
"""Bitmask of palette entries close enough to the background to skip.
The source art has a transparent surround, which the compositing step turns
into the background colour. Quantization then spreads that flat area across
several near-identical entries, and drawing them would stamp a visible
rectangle over whatever the logo sits on. Marking them lets the renderer
skip those pixels so only the logo itself is painted.
"""
mask = 0
br, bg, bb = BACKGROUND
for i, (r, g, b) in enumerate(palette):
if abs(r - br) + abs(g - bg) + abs(b - bb) <= 12:
mask |= 1 << i
return mask
def emit(path, palette, indices, ascii_rows, out_path):
packed = []
for i in range(0, len(indices), 2):
hi = indices[i] & 0x0F
lo = indices[i + 1] & 0x0F if i + 1 < len(indices) else 0
packed.append((hi << 4) | lo)
lines = []
lines.append("// Generated by scripts/mklogo.py -- do not edit by hand.")
lines.append("// Source artwork: %s" % os.path.basename(path))
lines.append("")
lines.append('#include <init/bootlogo.h>')
lines.append("")
lines.append("const int boot_logo_width = %d;" % SIZE)
lines.append("const int boot_logo_height = %d;" % SIZE)
lines.append("")
lines.append("// Palette entries that are background rather than artwork; the renderer")
lines.append("// skips these so the logo composites onto whatever is behind it.")
lines.append("const uint16_t boot_logo_transparent_mask = 0x%04X;"
% transparent_mask(palette))
lines.append("")
lines.append("// 16-entry ARGB palette; index 0 is the wallpaper background.")
lines.append("const uint32_t boot_logo_palette[%d] = {" % COLORS)
for r, g, b in palette:
lines.append(" 0xFF%02X%02X%02X," % (r, g, b))
lines.append("};")
lines.append("")
lines.append("// %dx%d pixels, two 4-bit indices per byte, row major." % (SIZE, SIZE))
lines.append("const uint8_t boot_logo_bitmap[%d] = {" % len(packed))
for i in range(0, len(packed), 16):
chunk = ", ".join("0x%02X" % b for b in packed[i : i + 16])
lines.append(" %s," % chunk)
lines.append("};")
lines.append("")
lines.append("// ASCII rendition for the text console, before the framebuffer exists.")
lines.append("const char* const boot_logo_ascii[] = {")
for row in ascii_rows:
lines.append(' "%s",' % row.replace("\\", "\\\\").replace('"', '\\"'))
lines.append(" NULL")
lines.append("};")
lines.append("")
with open(out_path, "w") as fh:
fh.write("\n".join(lines))
return len(packed), len(ascii_rows)
def main():
if len(sys.argv) < 2:
print(__doc__)
return 1
src = os.path.expanduser(sys.argv[1])
out = sys.argv[2] if len(sys.argv) > 2 else "init/logo_data.c"
palette, indices = build_bitmap(src)
ascii_rows = build_ascii(src)
nbytes, nrows = emit(src, palette, indices, ascii_rows, out)
print("%s: %dx%d bitmap, %d palette entries, %d bytes packed, %d ASCII rows"
% (out, SIZE, SIZE, len(palette), nbytes, nrows))
return 0
if __name__ == "__main__":
sys.exit(main())