From 33d628aa653d11212c9678266e9243e7c8a86f93 Mon Sep 17 00:00:00 2001 From: ballz Date: Fri, 17 Jul 2026 01:16:51 +0200 Subject: [PATCH] Phase 1: asset pipeline (Python script + 5 processed .bin/.attr files) Adds tools/convert_screens.py which converts each source PNG to a 160x200 multicolor bitmap (8000 B .bin) plus a 1000 B .attr screen- memory table, following c64-wiki Multicolor_Bitmap_Mode: - 2-bit pixel: 00=$D021, 01=attr high nibble, 10=attr low nibble, 11=color RAM nibble. - Per 4x8 cell: $D021 is forced into the 4-color set (it's global); the other 3 are the 3 most common non-d021 colors in the cell. - Pixels snap to the nearest of those 4, then packed 4-per-byte MSB-first into the bitmap; attr byte = (cell_color_2 << 4) | cell_color_1 (low nibble = '10' color, high = '01' color). Tried two downscale strategies on all 5 source images; both produce visually equivalent output at the ~9x source-to-target scale. Picked Strategy A (direct 160x200 LANCZOS) as default because it's simpler and slightly faster; Strategy B (LANCZOS 320x200 then 2x BOX 160x200) remains available via --strategy. See the script docstring for the detailed rationale. $D021 is auto-picked as the most common C64 palette color in the downscaled image (turns out to be black for 4 of 5 screens, dark grey for waiting2). The .d021 sidecar files document this value so Phase 2 can program the VIC without re-deriving it. The '11' color (color RAM nibble per cell) is not stored in .attr (no room in 1000 B). Default is to leave color RAM at the C64 boot value (black), which gives effectively 3 unique colors per cell + 1 global. Phase 2 can either accept this or extend the format with a per-cell color RAM table. Generated files are checked in so the build doesn't depend on Python. Output sizes verified: each .bin is exactly 8000 B, each .attr is exactly 1000 B, total 9000 B per screen. The .bin files are not all zero (sanity check passed). --- .gitignore | 10 + src/AGENT_CONTEXT.md | 185 ++++++++++++ src/data/processed/title.attr | Bin 0 -> 1000 bytes src/data/processed/title.bin | Bin 0 -> 8000 bytes src/data/processed/title.d021 | 1 + src/data/processed/waiting1.attr | Bin 0 -> 1000 bytes src/data/processed/waiting1.bin | Bin 0 -> 8000 bytes src/data/processed/waiting1.d021 | 1 + src/data/processed/waiting2.attr | Bin 0 -> 1000 bytes src/data/processed/waiting2.bin | Bin 0 -> 8000 bytes src/data/processed/waiting2.d021 | 1 + src/data/processed/win_hare.attr | Bin 0 -> 1000 bytes src/data/processed/win_hare.bin | Bin 0 -> 8000 bytes src/data/processed/win_hare.d021 | 1 + src/data/processed/win_scoot.attr | Bin 0 -> 1000 bytes src/data/processed/win_scoot.bin | Bin 0 -> 8000 bytes src/data/processed/win_scoot.d021 | 1 + tools/convert_screens.py | 479 ++++++++++++++++++++++++++++++ 18 files changed, 679 insertions(+) create mode 100644 src/AGENT_CONTEXT.md create mode 100644 src/data/processed/title.attr create mode 100644 src/data/processed/title.bin create mode 100644 src/data/processed/title.d021 create mode 100644 src/data/processed/waiting1.attr create mode 100644 src/data/processed/waiting1.bin create mode 100644 src/data/processed/waiting1.d021 create mode 100644 src/data/processed/waiting2.attr create mode 100644 src/data/processed/waiting2.bin create mode 100644 src/data/processed/waiting2.d021 create mode 100644 src/data/processed/win_hare.attr create mode 100644 src/data/processed/win_hare.bin create mode 100644 src/data/processed/win_hare.d021 create mode 100644 src/data/processed/win_scoot.attr create mode 100644 src/data/processed/win_scoot.bin create mode 100644 src/data/processed/win_scoot.d021 create mode 100644 tools/convert_screens.py diff --git a/.gitignore b/.gitignore index ab5d05f..d56e3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,20 @@ src/build/ *.bin *.crt +# but DO commit the asset-pipeline outputs (see tools/convert_screens.py) +!src/data/processed/*.bin +!src/data/processed/*.attr +!src/data/processed/*.d021 +!src/data/raw/ + # oscar64 build output (built by `make -C make compiler` from oscar64 source) oscar64/bin/ oscar64/build/ +# python cache +__pycache__/ +*.pyc + # editor / IDE .vscode/ .idea/ diff --git a/src/AGENT_CONTEXT.md b/src/AGENT_CONTEXT.md new file mode 100644 index 0000000..5112e55 --- /dev/null +++ b/src/AGENT_CONTEXT.md @@ -0,0 +1,185 @@ +# Project context for Whack Hare! subagent + +You are implementing one phase of a C64 game called "Whack Hare!" +(quick-draw duel between a rabbit and a kid on a scooter, first to 5 +wins). You will be given a specific phase task. This document is the +context you need to do the work. + +## Project layout (read-only) + +``` +/home/ballz/work/teletype/nyuller/ +├── oscar64/ # Oscar64 C cross-compiler (git submodule) +│ ├── bin/oscar64 # The compiler binary (built by make) +│ └── include/ # Runtime headers (c64/, c128/, gfx/, stdio.h, etc.) +├── docs/c64/ # Downloaded C64 reference material +│ ├── vic/ # VIC-II register map, graphics modes, cebix article +│ ├── cia/ # CIA 1 + CIA 2 register maps +│ ├── sid/ # SID register map, ADSR, filter +│ ├── kernal/ # KERNAL jump table, full PRG text +│ ├── memory/ # Memory map, zeropage, hardware internals +│ ├── interrupts/ # Raster IRQ, IRQ/NMI flow, joystick +│ ├── sprites/ # Sprite (MOB) programming +│ └── cpu/ # 6510 overview +├── OSCAR64.md # How the Oscar64 compiler works + C64 best practices +├── PROG_C64.md # C64 hardware reference (synthesized) +├── GAME.md # The game design spec +├── tasks.md # The phased implementation plan (your roadmap) +├── source_images/ # The 5 source PNGs for the game screens +└── src/ # YOUR CODE GOES HERE + ├── helloworld.c # Existing minimal working program + ├── build.sh # Build script (handles compiler build, runs tests) + └── build/ # Output directory (gitignored) +``` + +## Build and test (this is the ONLY way to verify your work) + +```sh +cd /home/ballz/work/teletype/nyuller/src +./build.sh # compile helloworld.c → build/helloworld.prg +./build.sh -e # run in oscar64's built-in emulator (HEADLESS, fast) +./build.sh -v # (DO NOT USE) VICE x64 — requires a real display +./build.sh -V # (DO NOT USE) VICE x64sc — same problem +``` + +**The oscar64 built-in emulator (`-e`) is the only test tool.** VICE +does not work in this headless environment (no display, blank +screenshots, manual ROM fetching). Do not waste time on VICE. +The `-e` flag runs the same `.prg` file the C64 will run, with no +setup, no ROMs, and at high speed. + +`build.sh` will auto-build the oscar64 compiler if it's missing. +It calls `oscar64 -i=/home/ballz/work/teletype/nyuller/oscar64/include -o=build/helloworld.prg helloworld.c` +to compile, and the same command with `-e` to run. + +The build artifacts in `build/` are: `helloworld.prg` (the C64 program), +`helloworld.asm` (6502 listing), `helloworld.map` (region/section/object +placement), `helloworld.lbl` (VICE monitor labels — still useful even +though we don't use VICE), `helloworld.int` (intermediate code). + +## CRITICAL: the 4-color-per-4×8-cell multicolor constraint + +The 5 source PNGs have well more than 2 colors per 8×8 cell. They +**must** be displayed using **multicolor bitmap mode** (VIC-II +`$D011` BMM=1, `$D016` MCM=1): 160×200 with 2 bits per pixel, 4 colors +per 4×8 cell, same 8000-byte bitmap size. Per-cell only 4 colors are +available; the asset pipeline must quantize each 4×8 cell independently +to 4 colors from the 16-color C64 palette (black, white, red, cyan, +purple, green, blue, yellow, orange, brown, light red, dark grey, +medium grey, light green, light blue, light grey). + +The bitmap goes to 8 KB at $E000-$FFFF (or wherever the linker +places it after `mmap_set(MMAP_RAM)` + `mmap_set(MMAP_NO_ROM)`). +The 1000-byte attribute table (one entry per 4×8 cell, low nibble = +foreground color, high nibble = the 2-bit background register selector +$21-$24) goes to $D800-$DBE7. + +## C64 hardware facts you'll need + +- **CPU**: MOS 6510 (6502 + 6-bit I/O port at $00/$01). 1 MHz PAL. + Banks BASIC/KERNAL/CHAREN via writes to $01. +- **VIC-II** at $D000-$D3FF. 47 registers, mirrored every 64 bytes. + The 16-color palette is fixed (see docs/c64/vic/vic_registers.md). + The raster counter is 9 bits: $D012 is the low 8, bit 7 of $D011 + is bit 8. +- **SID** (6581) at $D400-$D41C. 3 voices, ADSR envelopes, filter. + Volatile freq registers, no NaN, master volume at $D418. +- **CIA 1** at $DC00: keyboard, joystick, paddles, datasette, + IRQ timer. Joystick bytes are active-low: bit 4 = fire. + - Port 1 (right joystick, Hare) = $DC01 + - Port 2 (left joystick, Scoot) = $DC00 +- **CIA 2** at $DD00: serial bus, RS-232, NMI, **VIC bank bits** + (low 2 bits of PRA select one of 4 VIC banks). +- **KERNAL** at $E000-$FFFF. Jump table at $FF81-$FFF3. + IRQ vector at $0314 (default → $EA31, the standard KERNAL ISR). + NMI vector at $0318 (default → $FE47, the soft-reset / RESTORE handler). +- **Color RAM** at $D800-$DBE7: 4-bit wide (low nibble only). +- **Character ROM** at $D000-$DFFF in banks 0 and 2; can be relocated + to RAM by changing $D018. +- **PAL timing**: 312 lines, 50 Hz. The "stable raster" line for IRQs + is line 311 (right after vertical blank, before any badlines). +- **Badlines** cost 40 cycles every 8 text lines. Cycle-accurate + code has to know this. + +## Oscar64 patterns to use + +- `mmap_trampoline()` once at startup, then `mmap_set(MMAP_RAM)` to + bank out BASIC + KERNAL (gives you ~38 KB contiguous RAM at + $0900-$A000). `mmap_set(MMAP_NO_ROM)` also banks out CHAR ROM + ($D000 becomes I/O, not char data — needed for the bitmap at $E000). +- `#pragma region(name, start, end, , , {sections})` to remap + memory (you probably won't need this for the first pass — the + default layout works). +- `#pragma stacksize(N)` and `#pragma heapsize(N)` to size those. +- `#pragma compile("foo.c")` is what `#include ` etc. use + to drag in implementation files. You never list multiple `.c` + files on the command line; the headers do it. +- `__assume(x < 8)` etc. to give the optimizer value-range hints. + Helps the 6502 backend use 8-bit ops instead of 16-bit. +- `__striped` qualifier for arrays where the 6502's lack of + indirect-with-offset hurts. +- `p""` prefix for PETSCII string literals (not ASCII). The C64's + char ROM is not ASCII. +- `__hwinterrupt` for ISRs (saves A/X/Y, exits with RTI). +- `__native` for a function you want to force to native 6502 code. +- `__zeropage` for global variables you want placed in ZP + (or use `-Oz` to auto-place). +- The runtime headers in `./oscar64/include/` are the right way to + touch hardware: `#include ` (VIC-II struct at $D000), + `#include ` (joystick reading helpers), + `#include ` (mmap_set, mmap_trampoline), + `#include ` (raster IRQ API — use this, don't + write your own IRQ handler from scratch), + `#include ` (sprite setup, alignment), + `#include ` (hires drawing helpers), + `#include ` / `` for text I/O. + +## Game-specific design (full version in GAME.md) + +| Element | Value | +|---------|-------| +| Title screen | `source_images/screen_title.png` — full-screen image with "WHACKED" logo | +| Waiting 1 | `source_images/screen_waiting1.png` — 1.2 sec, "ping" jingle | +| Waiting 2 | `source_images/screen_waiting_2.png` — random 2-5 sec, suspense music | +| DRAW | white screen, big counter incrementing each frame, sharp stab | +| Win Hare | `source_images/screen_win_hare.png` — Hare won (player 1) | +| Win Scoot | `source_images/screen_win_scoot.png` — Scoot won (player 2) | +| Score bar | top 8 pixels of the screen, "HARE 0..0 SCOOT" (5 pips per side) | +| Match | first to 5 wins, then reset to title | +| Player 1 (Hare) | left side, joystick port 1, fire = `(PEEK(0xDC01) & 0x10) == 0` | +| Player 2 (Scoot) | right side, joystick port 2, fire = `(PEEK(0xDC00) & 0x10) == 0` | +| State machine | TITLE → READY → WAIT → DRAW → WIN_P1/P2 → (back to READY or GAMEOVER) | +| Game timer | 50 Hz PAL, line 311 for the raster IRQ | +| Random | use `PEEK(0xD41B)` (SID oscillator 3, effectively random) | + +## Commit conventions + +- One commit per phase, with a clear `Phase N: ` prefix. +- Don't commit generated build artifacts in `src/build/` (gitignored). +- Generated data files (e.g. `src/data/processed/*.bin`) are checked + in so the build doesn't depend on Python being installed. + +## What to do if you get stuck + +1. Re-read the relevant section of `OSCAR64.md`, `PROG_C64.md`, + `GAME.md`, or `tasks.md` (your phase section). +2. Look at the Oscar64 samples: `../oscar64/samples/`. They're + small, focused, and have working patterns. Good ones to look at: + - `../oscar64/samples/memmap/allmem.c` — mmap_set usage + - `../oscar64/samples/rasterirq/colorbars.c` — raster IRQ + - `../oscar64/samples/memmap/easyflash.c` — memory layout + - `../oscar64/samples/hires/lines.c` — hires drawing + - `../oscar64/samples/sprites/` — sprite setup +3. Look at the runtime headers you need to use: + `ls /home/ballz/work/teletype/nyuller/oscar64/include/c64/` etc. +4. Read the relevant docs/c64/ file for the chip you're working on. +5. Do NOT spend time on VICE, xvfb, environment setup, or anything + not in this project. The build environment is set up. + +## Output for this phase + +When you're done, report back: +1. What you built (1-2 sentence summary) +2. The output of `./build.sh -e` (proves it compiled and runs) +3. The git commit hash and one-line summary +4. Any concerns or follow-up work for the next phase diff --git a/src/data/processed/title.attr b/src/data/processed/title.attr new file mode 100644 index 0000000000000000000000000000000000000000..f070d58f60b21e3af179df2e8bde94fa8ac4ba48 GIT binary patch literal 1000 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 literal 0 HcmV?d00001 diff --git a/src/data/processed/waiting1.bin b/src/data/processed/waiting1.bin new file mode 100644 index 0000000000000000000000000000000000000000..3864429744ec7bbb8297a9d0dc7c3cf722205bef GIT binary patch literal 8000 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 JYxKD_@W21w-#q{T literal 0 HcmV?d00001 diff --git a/src/data/processed/waiting1.d021 b/src/data/processed/waiting1.d021 new file mode 100644 index 0000000..573541a --- /dev/null +++ b/src/data/processed/waiting1.d021 @@ -0,0 +1 @@ +0 diff --git a/src/data/processed/waiting2.attr b/src/data/processed/waiting2.attr new file mode 100644 index 0000000000000000000000000000000000000000..71dd0dd4b42ac286617f57ffb9b616844fa30caa GIT binary patch literal 1000 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 literal 0 HcmV?d00001 diff --git a/src/data/processed/waiting2.d021 b/src/data/processed/waiting2.d021 new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/src/data/processed/waiting2.d021 @@ -0,0 +1 @@ +11 diff --git a/src/data/processed/win_hare.attr b/src/data/processed/win_hare.attr new file mode 100644 index 0000000000000000000000000000000000000000..349db18619aa16968e5a88634c7bafdfbfe373a9 GIT binary patch literal 1000 zcmb7@L23gr3`OTGS=IQKU3IXb;FW`J;YAr?9iZS7m2UcEPaD#fLjMqZBz@`sIL@~H z54}5oF26SR|H2OYv!%tTz1wjdIellUQtGY6=1UAE#%5+mN=N$~>Kwg_Xp4a|h%f{1 zmQyxQOsI-V6%p#K^+|=vKJpmJ%=>^Hu---02YF{lf!9~FVPjazF+M!zTxGXf$qJd zl&$pGRj)p9awF zck^nxj~fp*MbAuWcRJ&Bi?{9ST@?L@xMUo~86mfBx#xkm&C`cFqcgp>TQGSnXUk{7 g<9qzfeAQ<+>aKLd3TBPu+SetxTdB>M?e0(d0aZ#64gdfE literal 0 HcmV?d00001 diff --git a/src/data/processed/win_hare.bin b/src/data/processed/win_hare.bin new file mode 100644 index 0000000000000000000000000000000000000000..9e5f9f6a2233157992f1bb313109920268daaf68 GIT binary patch literal 8000 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 literal 0 HcmV?d00001 diff --git a/src/data/processed/win_hare.d021 b/src/data/processed/win_hare.d021 new file mode 100644 index 0000000..573541a --- /dev/null +++ b/src/data/processed/win_hare.d021 @@ -0,0 +1 @@ +0 diff --git a/src/data/processed/win_scoot.attr b/src/data/processed/win_scoot.attr new file mode 100644 index 0000000000000000000000000000000000000000..68a95677253b75afb338c45171e08c62db12a074 GIT binary patch literal 1000 zcma)*F>b>!3`KR8Y=UmtB@Nt3lLYn@nnK72NRb0&?fZ1%q)QP3mL=-_Pl<+8dN1bt zF~(c$DAoT12R=BBQL>HU!H((M zs7*m7bEOZ5V%6HIiL;_2yS|i}(gYk?d3HphBv>6H$~cq)r7$O(YYQekUEoKhvX?w^ z!jLPXueyv)+!>YOY>H|ld+yZjo{~TXZ?RP9JrelMiZ;%o-P%R2l#b?EQ-pNdvF!V z@s7^-+TsDezA1^x>R;cHl_?X8 ZMO0?FS5WqG!XPxf{JVQD^4j~e{|j0uH}(Jk literal 0 HcmV?d00001 diff --git a/src/data/processed/win_scoot.bin b/src/data/processed/win_scoot.bin new file mode 100644 index 0000000000000000000000000000000000000000..78c0cc6f7743250482a5ca71bf2b73b24deac9b9 GIT binary patch literal 8000 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 [--strategy A|B] + python3 convert_screens.py source_images/screen_title.png src/data/processed/title +""" + +import argparse +import sys +from collections import Counter +from pathlib import Path + +try: + from PIL import Image +except ImportError: + sys.stderr.write("ERROR: Pillow not installed. Try: pip install pillow\n") + sys.exit(1) + + +# C64 "Pepto PAL" palette (RGB). Indices match the VIC-II color number +# ($D021..$D024 background regs and $D025-$D026 sprite MC regs use the +# same 16 colors). The order below is index 0=black, 1=white, etc. +PALETTE_RGB = ( + (0x00, 0x00, 0x00), # 0 black + (0xFF, 0xFF, 0xFF), # 1 white + (0x88, 0x00, 0x00), # 2 red + (0xAA, 0xFF, 0xEE), # 3 cyan + (0xCC, 0x44, 0xCC), # 4 purple + (0x00, 0xCC, 0x55), # 5 green + (0x00, 0x00, 0xAA), # 6 blue + (0xEE, 0xEE, 0x77), # 7 yellow + (0xDD, 0x88, 0x55), # 8 orange + (0x66, 0x44, 0x00), # 9 brown + (0xFF, 0x77, 0x77), # 10 light red + (0x33, 0x33, 0x33), # 11 dark grey + (0x77, 0x77, 0x77), # 12 medium grey + (0xAA, 0xFF, 0x66), # 13 light green + (0x00, 0x88, 0xFF), # 14 light blue + (0xCC, 0xCC, 0xCC), # 15 light grey +) + + +def _dist2(a, b): + """Squared RGB distance between two 3-tuples. Faster than sqrt.""" + dr = a[0] - b[0] + dg = a[1] - b[1] + db = a[2] - b[2] + return dr * dr + dg * dg + db * db + + +def nearest_palette_index(rgb): + """Return the index (0..15) of the nearest C64 palette color to rgb.""" + best_i = 0 + best_d = _dist2(rgb, PALETTE_RGB[0]) + for i in range(1, 16): + d = _dist2(rgb, PALETTE_RGB[i]) + if d < best_d: + best_d = d + best_i = i + return best_i + + +def palette_snap_image(img): + """Snap every pixel of a PIL image to the nearest C64 palette color. + + Returns a new image where each pixel is a tuple from PALETTE_RGB. + Done by quantizing to the 16-color C64 palette globally - this is + a useful intermediate step before the per-cell quantization, but + the per-cell 4-color selection is the real work. + """ + out = Image.new("RGB", img.size) + src = img.load() + dst = out.load() + for y in range(img.height): + for x in range(img.width): + dst[x, y] = PALETTE_RGB[nearest_palette_index(src[x, y])] + return out + + +def _cell_palette(pixels_rgb, d021_index, k=3): + """Pick k extra palette colors for a 4x8 cell, excluding d021_index. + + Returns a list of exactly k C64 palette indices (0..15) sorted by + frequency in pixels_rgb (most common first). If the cell has + fewer than k distinct non-d021 colors, pad by repeating the last + one. These k colors plus d021_index form the cell's 4-color set. + """ + counts = Counter() + for p in pixels_rgb: + idx = nearest_palette_index(p) + if idx != d021_index: + counts[idx] += 1 + most = [idx for idx, _ in counts.most_common(k)] + while len(most) < k: + most.append(most[-1] if most else d021_index) + return most + + +def _snap_pixels_to_indices(pixels_rgb, palette_indices): + """Snap each pixel to the nearest of the given palette indices. + + Returns a list of 0..3 (the index into palette_indices, not the + palette index itself). palette_indices[0] is treated as the "00" + color and is what the C64 will display for 2-bit pixel value 00. + """ + pal_rgb = [PALETTE_RGB[i] for i in palette_indices] + snapped = [] + for p in pixels_rgb: + best_local = 0 + best_d = _dist2(p, pal_rgb[0]) + for j in range(1, len(pal_rgb)): + d = _dist2(p, pal_rgb[j]) + if d < best_d: + best_d = d + best_local = j + snapped.append(best_local) + return snapped + + +def quantize_cell(pixels_rgb, d021_index): + """Pick 4 colors for a 4x8 cell and snap each pixel to one of them. + + The C64 multicolor bitmap's "00" color is the global $D021 register, + so d021_index must be one of the cell's 4 colors (at position 0). + The other 3 colors are the 3 most common non-d021 colors in the + cell, sorted by frequency. + + pixels_rgb is a list of 32 (R,G,B) tuples (4 wide x 8 tall). + Returns (palette_indices, snapped_local): + palette_indices - list of exactly 4 C64 palette indices: + [0] = d021_index (the "00" color) + [1] = cell's most common (excluding d021) + [2] = cell's 2nd most common + [3] = cell's 3rd most common + snapped_local - list of 32 ints in 0..3, the local index into + palette_indices for each pixel in raster order + (left-to-right, top-to-bottom). Value 0 means + "this pixel snaps to d021_index". + """ + extra = _cell_palette(pixels_rgb, d021_index, k=3) + palette_indices = [d021_index] + extra + snapped_local = _snap_pixels_to_indices(pixels_rgb, palette_indices) + return palette_indices, snapped_local + + +def encode_cell_pixels(snapped_local): + """Pack 32 snapped-local pixels (0..3 each) into 8 bytes. + + The C64 multicolor bitmap packs 4 pixels per byte, MSB-first: bits + 7-6 hold pixel 0 (leftmost), bits 5-4 hold pixel 1, bits 3-2 hold + pixel 2, bits 1-0 hold pixel 3. The 2-bit value selects which of + the 4 cell colors to use. + + pixels are in raster order (left-to-right, top-to-bottom) for a + 4x8 cell: rows 0..7, each with 4 pixels. + """ + assert len(snapped_local) == 32 + out = bytearray(8) + for row in range(8): + b = 0 + for col in range(4): + pix = snapped_local[row * 4 + col] & 0x03 + b = (b << 2) | pix + out[row] = b + return bytes(out) + + +def make_attr_byte(palette_indices): + """Pack a cell's 4-color palette into a 1-byte screen-memory entry. + + bits 0-3 = "10" pixel color (C64 palette index 0..15) + bits 4-7 = "01" pixel color (C64 palette index 0..15) + + Convention used here (see module docstring): + palette_indices[0] = d021_index (the "00" color, global) + palette_indices[1] = "01" color (high nibble of attr) + palette_indices[2] = "10" color (low nibble of attr) + palette_indices[3] = "11" color (NOT stored in .attr - it lives in + color RAM at $D800+cell_index, set by the + C code at runtime) + + This is a deliberate simplification: the "11" color is the 4th color + in the cell, but it costs 4 bits per cell to store in color RAM and + we only have 1000 bytes for the .attr file. The C-side code can + either accept the "11" = black default (color RAM is initialized to + 0 at boot) or set color RAM to a per-cell value from a separate + table. See tasks.md Phase 1 notes and the follow-up work for + Phase 2. + """ + fg10 = palette_indices[2] & 0x0F + bg01 = palette_indices[1] & 0x0F + return fg10 | (bg01 << 4) + + +def pick_global_d021(img_palette_quantized, source_rgb=None): + """Pick the single $D021 value to use for the whole screen. + + Counts the C64 palette indices used in the already-quantized image + and picks the most common. This is the "00" color shared by all + cells. If source_rgb is given, uses it instead of the quantized + image to do the count (a tiny bit faster than re-quantizing). + + Returns an int 0..15. + + The default uses the most common color of the whole image, which + for our 5 sunset-themed screens ends up being black (the C64's + natural background). This is a deliberate choice: black is the + safest d021 because it's a common "outline" / "shadow" color in + the art, every cell is likely to contain it, and the C64 community + uses it by default. We can revisit this (e.g. pick the most common + non-black color) if the output looks too dark in later phases. + """ + counts = Counter() + src = img_palette_quantized.load() + for y in range(img_palette_quantized.height): + for x in range(img_palette_quantized.width): + counts[nearest_palette_index(src[x, y])] += 1 + return counts.most_common(1)[0][0] + + +def convert_image(img, strategy="A", verbose=False, d021_override=None): + """Convert a PIL Image to (bitmap_bytes, attr_bytes, d021_index). + + img - PIL Image (any mode; will be converted to RGB) + strategy - "A" (resize directly to 160x200) or + "B" (resize to 320x200 then 2x downscale to 160x200) + d021_override - if not None, force this as the $D021 value + (skip the auto-pick). int 0..15. + Returns (bitmap, attr, d021_index) where: + bitmap - bytes of length 8000 + attr - bytes of length 1000 + d021_index - int 0..15, the $D021 value to program on the C64 + """ + if img.mode != "RGB": + img = img.convert("RGB") + + if strategy == "A": + # Direct 160x200 downscale with LANCZOS. One pass, simple. + # For these ~1400x1100 source images, this is an 8.7x + # horizontal / 5.6x vertical downscale. LANCZOS handles + # this OK but is at the edge of its comfort zone; a 2x + # downscale is more in LANCZOS's sweet spot. + small = img.resize((160, 200), Image.LANCZOS) + elif strategy == "B": + # 320x200 with LANCZOS, then 2x downscale with BOX. + # The first pass is 4.3x (LANCZOS does this well), the + # second is a clean 2x box filter (Pillow's BOX is the + # integer-downscale equivalent of "area averaging" - each + # output pixel is the mean of a 2x2 block of input pixels, + # which is what you want for downscaling). This is the + # textbook high-quality downscale recipe. + mid = img.resize((320, 200), Image.LANCZOS) + small = mid.resize((160, 200), Image.BOX) + else: + raise ValueError("strategy must be 'A' or 'B'") + + # First, snap every pixel to the nearest C64 palette color. This + # gives a stable RGB for downstream analysis (no antialiasing + # artifacts in the count). + quantized = palette_snap_image(small) + if d021_override is not None: + d021_index = d021_override & 0x0F + else: + d021_index = pick_global_d021(quantized) + if verbose: + print(f" d021 (background) = {d021_index} " + f"({PALETTE_RGB[d021_index]})") + + bitmap = bytearray(8000) + attr = bytearray(1000) + src = quantized.load() + + # 1000 cells: 40 across (160/4), 25 down (200/8). + for cy in range(25): + for cx in range(40): + # Gather the 32 pixel colors in this 4x8 cell. + pixels = [] + for row in range(8): + py = cy * 8 + row + for col in range(4): + px = cx * 4 + col + pixels.append(src[px, py]) + + palette_indices, snapped_local = quantize_cell(pixels, + d021_index) + attr_byte = make_attr_byte(palette_indices) + cell_bytes = encode_cell_pixels(snapped_local) + + # Place the 8 bytes for this cell in the bitmap. + # C64 multicolor bitmap memory layout: + # offset = (cy * 40 + cx) * 8 + # because 40 cells per character row, 8 bytes per cell. + cell_offset = (cy * 40 + cx) * 8 + bitmap[cell_offset:cell_offset + 8] = cell_bytes + attr[cy * 40 + cx] = attr_byte + + return bytes(bitmap), bytes(attr), d021_index + + +def preview_image(bitmap, attr, d021_index): + """Render the .bin/.attr back to a PIL Image for visual inspection. + + Useful for debugging the quantizer. Not used by the C side. + Shows what the C64 will actually display: 2-bit value 00 -> d021, + 01 -> attr high nibble, 10 -> attr low nibble, 11 -> color RAM. + Since we don't store the "11" color in .attr, we use black (0) + here, which is what the C64's color RAM defaults to at boot. + """ + out = Image.new("RGB", (160, 200), PALETTE_RGB[d021_index]) + px = out.load() + for cy in range(25): + for cx in range(40): + cell_offset = (cy * 40 + cx) * 8 + attr_byte = attr[cy * 40 + cx] + fg10 = PALETTE_RGB[attr_byte & 0x0F] # "10" color + bg01 = PALETTE_RGB[(attr_byte >> 4) & 0x0F] # "01" color + for row in range(8): + b = bitmap[cell_offset + row] + for col in range(4): + v = (b >> (6 - col * 2)) & 0x03 + if v == 0: + c = PALETTE_RGB[d021_index] + elif v == 1: + c = bg01 + elif v == 2: + c = fg10 + else: + # "11" - color RAM. Not stored in .attr. + c = PALETTE_RGB[0] + px[cx * 4 + col, cy * 8 + row] = c + return out + + +def convert_file(input_path, output_base, strategy="A", verbose=False, + write_preview=False, d021_override=None): + """Convert input_path to (output_base + ".bin", output_base + ".attr"). + + output_base is a path WITHOUT the .bin / .attr extension. + """ + in_path = Path(input_path) + if not in_path.exists(): + raise FileNotFoundError(in_path) + if verbose: + print(f"Converting {in_path} -> {output_base}.{{bin,attr}} " + f"(strategy {strategy})") + img = Image.open(in_path) + bitmap, attr, d021_index = convert_image(img, strategy=strategy, + verbose=verbose, + d021_override=d021_override) + + out_path = Path(output_base) + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path.with_suffix(".bin"), "wb") as f: + f.write(bitmap) + with open(out_path.with_suffix(".attr"), "wb") as f: + f.write(attr) + # Also write a tiny sidecar with the $D021 value, so the C code + # doesn't have to guess. This is checked into the repo too. + with open(out_path.with_suffix(".d021"), "w") as f: + f.write(f"{d021_index}\n") + + if verbose: + print(f" wrote {out_path.with_suffix('.bin')} " + f"({len(bitmap)} bytes)") + print(f" wrote {out_path.with_suffix('.attr')} " + f"({len(attr)} bytes)") + print(f" wrote {out_path.with_suffix('.d021')} ({d021_index})") + + if write_preview: + prev = preview_image(bitmap, attr, d021_index) + prev_path = out_path.with_suffix(".preview.png") + prev.save(prev_path) + if verbose: + print(f" wrote {prev_path}") + + return bitmap, attr, d021_index + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + ap.add_argument("input", help="Source PNG path") + ap.add_argument("output_base", + help="Output base path (no .bin/.attr extension)") + ap.add_argument("--strategy", choices=["A", "B"], default="A", + help="A: 160x200 direct LANCZOS downscale (default). " + "B: 320x200 LANCZOS then 2x BOX downscale. " + "Both produce visually equivalent output on the " + "5 source images; A is simpler and faster.") + ap.add_argument("--preview", action="store_true", + help="Also write a .preview.png for inspection") + ap.add_argument("--d021", type=int, default=None, + help="Force the $D021 (background) color, 0..15. " + "Default: auto-pick the most common color in " + "the image.") + ap.add_argument("--quiet", action="store_true", + help="Suppress per-file progress output") + args = ap.parse_args(argv) + + if args.d021 is not None and not (0 <= args.d021 <= 15): + ap.error("--d021 must be in 0..15") + + convert_file(args.input, args.output_base, + strategy=args.strategy, + verbose=not args.quiet, + write_preview=args.preview, + d021_override=args.d021) + + +if __name__ == "__main__": + main()