First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
139
lenser/a7800/viewer/assemble.py
Normal file
139
lenser/a7800/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue