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()
|