diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rwxr-xr-x | tools/mk_sl0pboy_bios.py | 85 |
2 files changed, 88 insertions, 0 deletions
@@ -6,3 +6,6 @@ roms/ compile_commands.json /bios/ + +# generated boot-ROM logo preview +bios/*.preview.png diff --git a/tools/mk_sl0pboy_bios.py b/tools/mk_sl0pboy_bios.py new file mode 100755 index 0000000..1904a19 --- /dev/null +++ b/tools/mk_sl0pboy_bios.py @@ -0,0 +1,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() |
