8bitlenser/lenser/a7800/viewer/assemble.py
2026-07-03 19:35:35 -07:00

139 lines
4.7 KiB
Python

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