82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""Assemble the 6502 viewer stubs with `xa` and build self-contained viewer PRGs.
|
|
|
|
Each viewer is a small ML stub originating at $0801 (behind a BASIC SYS 2061
|
|
autostart). The picture data is appended after the stub, zero-padded so the
|
|
bitmap lands exactly at $2000, screen RAM at $3F40, etc. The whole thing loads
|
|
in a single pass -- no second disk access -- so it works identically on real
|
|
hardware and in any emulator regardless of device configuration.
|
|
|
|
`xa -o` emits raw bytes starting at the origin without the 2-byte CBM load
|
|
address, which we prepend here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
LOAD_ADDR = 0x0801
|
|
DATA_ADDR = 0x2000 # where appended picture data must land
|
|
|
|
# mode/viewer key -> source filename
|
|
SOURCES = {
|
|
"hires": "hires.s",
|
|
"multicolor": "multicolor.s",
|
|
"fli": "fli.s",
|
|
"fli_ntsc": "fli_ntsc.s",
|
|
"interlace": "interlace.s",
|
|
}
|
|
|
|
_cache: dict[str, bytes] = {}
|
|
|
|
|
|
class AssemblerError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def have_xa() -> bool:
|
|
return shutil.which("xa") is not None
|
|
|
|
|
|
def assemble_stub(viewer_key: str) -> bytes:
|
|
"""Assemble a viewer stub to raw bytes (origin $0801, no load-address prefix)."""
|
|
if viewer_key in _cache:
|
|
return _cache[viewer_key]
|
|
if not have_xa():
|
|
raise AssemblerError(
|
|
"The 'xa' (xa65) assembler was not found on PATH.\n"
|
|
"Install it with: sudo apt install xa65 (Debian/Ubuntu)\n"
|
|
"or build from https://www.floodgap.com/retrotech/xa/")
|
|
|
|
src = os.path.join(VIEWER_DIR, SOURCES[viewer_key])
|
|
if not os.path.exists(src):
|
|
raise AssemblerError(f"viewer source missing: {src}")
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
out = os.path.join(td, "viewer.bin")
|
|
proc = subprocess.run(["xa", "-o", out, src], capture_output=True, text=True)
|
|
if proc.returncode != 0:
|
|
raise AssemblerError(f"xa failed for {src}:\n{proc.stdout}{proc.stderr}")
|
|
with open(out, "rb") as f:
|
|
raw = f.read()
|
|
_cache[viewer_key] = raw
|
|
return raw
|
|
|
|
|
|
def build_viewer_prg(viewer_key: str, data: bytes, data_addr: int = DATA_ADDR) -> bytes:
|
|
"""Combine the assembled stub + padding + picture ``data`` into one PRG.
|
|
|
|
``data`` is the block that must reside from ``data_addr`` upward (bitmap,
|
|
screen, colour RAM, background, ...).
|
|
"""
|
|
stub = assemble_stub(viewer_key)
|
|
pad_len = (data_addr - LOAD_ADDR) - len(stub)
|
|
if pad_len < 0:
|
|
raise AssemblerError(
|
|
f"viewer stub {viewer_key} is {len(stub)} bytes, exceeds the "
|
|
f"{data_addr - LOAD_ADDR} bytes available before ${data_addr:04x}")
|
|
payload = stub + bytes(pad_len) + bytes(data)
|
|
return bytes([LOAD_ADDR & 0xFF, (LOAD_ADDR >> 8) & 0xFF]) + payload
|