"""Assemble the Atari 7800 viewer with `xa` and lay out the 48K .a78 cartridge. MARIA reads the display-list list (DLL), the per-line display lists and the bitmap by DMA straight from cartridge ROM, so the packer just places them at fixed cart addresses and the viewer points MARIA at the DLL. ROM layout ($4000-$FFFF, 48K): $4000 viewer code $4100 colour register script (reg, value pairs, $FF-terminated) $8000 bitmap 192 lines x 40 bytes (2bpp) $A000 display lists 192 x 34 bytes (8 objects + end marker per line) $BA00 DLL 192 x 3 bytes (one 1-line zone per line) $FFFA 6502 vectors (NMI/RESET/IRQ -> $4000) """ from __future__ import annotations import os import shutil import struct import subprocess import tempfile VIEWER_DIR = os.path.dirname(os.path.abspath(__file__)) CART_BASE = 0x4000 CART_SIZE = 0xC000 # 48K SCRIPT_ADDR = 0x4100 BITMAP_ADDR = 0x8000 DL_BASE = 0xA000 DLL_ADDR = 0xBA00 WIDTH, LINES = 160, 192 BYTES_PER_LINE = 40 SEG_W_BYTES = 10 # 10 bytes = 40 px per object N_SEG = 4 SEG_W_PX = 40 DL_LEN = N_SEG * 4 + 2 # 4 objects (4 bytes) + 2-byte end = 18 # MARIA colour-register addresses in the order the converter emits them: # BACKGRND, then P0C1..P0C3, P1C1.., ... P7C1..P7C3. COLOR_REGS = [0x20] for _p in range(8): COLOR_REGS += [0x21 + 4 * _p, 0x22 + 4 * _p, 0x23 + 4 * _p] class AssemblerError(RuntimeError): pass def have_xa() -> bool: return shutil.which("xa") is not None def _assemble() -> bytes: if not have_xa(): raise AssemblerError("The 'xa' (xa65) assembler was not found on PATH.\n" "Install it with: sudo apt install xa65") wrapper = (f"#define SCRIPT ${SCRIPT_ADDR:04X}\n" f"#define DLL ${DLL_ADDR:04X}\n" '#include "viewer.s"\n') with tempfile.TemporaryDirectory() as td: out = os.path.join(td, "v.bin") fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR) try: with os.fdopen(fd, "w") as f: f.write(wrapper) proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)], capture_output=True, text=True, cwd=VIEWER_DIR) if proc.returncode != 0: raise AssemblerError(f"xa failed:\n{proc.stdout}{proc.stderr}") with open(out, "rb") as f: return f.read() finally: os.unlink(wrap) def _a78_header(rom_len: int, title: str) -> bytes: h = bytearray(128) h[0] = 1 # header version h[1:1 + 9] = b"ATARI7800" h[17:17 + 32] = title.encode("ascii", "replace")[:32].ljust(32, b"\x00") h[49:53] = struct.pack(">I", rom_len) # ROM size (excl. header) # cart type 0 = plain linear; controllers = joystick; NTSC h[55] = 1 h[56] = 1 h[57] = 0 h[100:100 + 28] = b"ACTUAL CART DATA STARTS HERE" return bytes(h) def build_cart(data: bytes, title: str = "8bitlenser") -> bytes: """data = converter blob: bitmap(7680) + seg_palettes(192*8) + colours(25).""" bitmap = data[:LINES * BYTES_PER_LINE] seg_pal = data[LINES * BYTES_PER_LINE:LINES * BYTES_PER_LINE + LINES * N_SEG] colours = data[LINES * BYTES_PER_LINE + LINES * N_SEG:] code = _assemble() rom = bytearray(b"\x00" * CART_SIZE) def place(addr, blob): off = addr - CART_BASE rom[off:off + len(blob)] = blob place(CART_BASE, code) # colour register script: (reg, value) pairs, $FF terminator script = bytearray() for reg, val in zip(COLOR_REGS, colours): script += bytes([reg, val]) script.append(0xFF) place(SCRIPT_ADDR, script) place(BITMAP_ADDR, bitmap) # per-line display lists (8 objects of 5 bytes, each its own palette) dls = bytearray() for line in range(LINES): for s in range(N_SEG): gfx = BITMAP_ADDR + line * BYTES_PER_LINE + s * SEG_W_BYTES pal = seg_pal[line * N_SEG + s] & 0x07 width = (-SEG_W_BYTES) & 0x1F # two's-complement byte count dls += bytes([gfx & 0xFF, (pal << 5) | width, gfx >> 8, s * SEG_W_PX]) dls += bytes([0x00, 0x00]) # end of DL place(DL_BASE, dls) # display-list list: one 1-line zone per line dll = bytearray() for line in range(LINES): dl = DL_BASE + line * DL_LEN dll += bytes([0x00, dl >> 8, dl & 0xFF]) # offset 0 (1 line), DL hi, lo place(DLL_ADDR, dll) # 6502 vectors -> viewer entry ($4000) for v in (0xFFFA, 0xFFFC, 0xFFFE): off = v - CART_BASE rom[off] = CART_BASE & 0xFF rom[off + 1] = CART_BASE >> 8 return _a78_header(len(rom), title) + bytes(rom)