aboutsummaryrefslogtreecommitdiffstats
path: root/tools/mk_sl0pboy_bios.py
blob: 1904a192f7bc8934f47d0dfc349e4281a38deae9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/env python3
"""Fork the CGB boot ROM, swapping the "GAME BOY" boot wordmark for "SL0PBOY".

The stock CGB boot ROM (bios/gbc_bios.bin) draws its on-screen "GAME BOY"
wordmark from a 192-byte blob at ROM $0607 (the routine at $03F0 copies it to
VRAM tiles $08-$37). The blob is 48 tiles (16 wide x 3 tall, tile-sequential),
4 bytes per tile - each byte is one 8px row that the copy loop doubles
vertically (plane-1 stays 0). The cart's own Nintendo logo ($104) is untouched,
so the boot ROM's logo-integrity check still passes and hands off normally.

We just re-encode that blob with a "SL0PBOY" bitmap of the same size and patch
it in place - no code changes, no relocation. Re-run this to tweak the logo.

    tools/mk_sl0pboy_bios.py            # -> bios/sl0pboy_bios.bin (+ a preview)

Needs Pillow and a bold-italic TTF (LiberationSans-BoldItalic by default).
"""
import os, sys
from PIL import Image, ImageFont, ImageDraw

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
SRC  = os.path.join(ROOT, "bios/gbc_bios.bin")
OUT  = os.path.join(ROOT, "bios/sl0pboy_bios.bin")
FONT = os.environ.get("SL0P_FONT",
                      "/usr/share/fonts/liberation/LiberationSans-BoldItalic.ttf")
LOGO_OFF, LOGO_LEN = 0x607, 0xC0          # 192-byte "GAME BOY" blob
TEXT = os.environ.get("SL0P_TEXT", "SL0PBOY")

# tile grid: 16 wide x 3 tall, 4 logical rows/tile -> 128 px wide x 12 logical
# rows (the copy loop doubles each row, so the on-screen art is 128x24).
W, LROWS = 128, 12


def render_logical():
    """1bpp 128x12 bitmap of TEXT, bold-italic, fit to the logo box."""
    big = Image.new("L", (1600, 320), 0)
    ImageDraw.Draw(big).text((10, 10), TEXT, fill=255,
                             font=ImageFont.truetype(FONT, 240))
    crop = big.crop(big.getbbox())
    fit = crop.resize((W - 2, LROWS), Image.LANCZOS)   # 1px side margins
    logical = Image.new("L", (W, LROWS), 0)
    logical.paste(fit, (1, 0))
    return logical.point(lambda p: 255 if p > 100 else 0)


def encode(bw):
    """48 tiles (row-major 16x3), 4 bytes/tile, MSB = leftmost pixel."""
    px = bw.load()
    out = bytearray()
    for t in range(48):
        tx, ty = (t % 16) * 8, (t // 16) * 4
        for r in range(4):
            b = 0
            for x in range(8):
                if px[tx + x, ty + r] >= 128:
                    b |= 1 << (7 - x)
            out.append(b)
    return bytes(out)


def main():
    if not os.path.exists(SRC):
        sys.exit("missing %s" % SRC)
    if not os.path.exists(FONT):
        sys.exit("missing font %s (set SL0P_FONT)" % FONT)
    bw = render_logical()
    blob = encode(bw)
    assert len(blob) == LOGO_LEN, len(blob)
    bios = bytearray(open(SRC, "rb").read())
    bios[LOGO_OFF:LOGO_OFF + LOGO_LEN] = blob
    open(OUT, "wb").write(bios)
    # a doubled preview PNG next to the binary, for eyeballing
    prev = Image.new("L", (W, LROWS * 2))
    pp, bp = prev.load(), bw.load()
    for y in range(LROWS):
        for x in range(W):
            pp[x, 2 * y] = pp[x, 2 * y + 1] = bp[x, y]
    prev.resize((W * 4, LROWS * 8), Image.NEAREST).save(OUT + ".preview.png")
    print("wrote %s (patched $%04X-$%04X) + %s.preview.png"
          % (OUT, LOGO_OFF, LOGO_OFF + LOGO_LEN - 1, OUT))


if __name__ == "__main__":
    main()