aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rwxr-xr-xtools/mk_sl0pboy_bios.py85
2 files changed, 88 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 5965ca5..dbd0fc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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()