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

122 lines
4.8 KiB
Python

"""Assemble the C128 VDC viewer with `xa` and build the loadable PRG.
The PRG loads at the C128 BASIC start ($1C01): a tiny BASIC stub (`10 SYS7200`)
followed by the 8502 viewer (at $1C20) and, from $2000, the 640x200 bitmap.
Running it (RUN"PIC") executes the stub, which SYSes the viewer.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
BASIC_START = 0x1C01
ML_ORG = 0x1C20
DATA_ORG = 0x2000 # bitmap goes here (viewer copies it to the VDC)
# BASIC: 10 SYS7200 ($1C20 = 7200) -- bytes as they sit from $1C01
_STUB = bytes([0x0B, 0x1C, 0x0A, 0x00, 0x9E,
0x37, 0x32, 0x30, 0x30, 0x00, 0x00, 0x00])
class AssemblerError(RuntimeError):
pass
def have_xa() -> bool:
return shutil.which("xa") is not None
def _xa(wrapper: str) -> bytes:
"""Assemble a generated wrapper (xa runs in VIEWER_DIR so #includes resolve)."""
if not have_xa():
raise AssemblerError("The 'xa' (xa65) assembler was not found on PATH.\n"
"Install it with: sudo apt install xa65")
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 _assemble(fgbg: int, source: str) -> bytes:
return _xa(f"#define SRC ${DATA_ORG:04X}\n"
f"#define FGBG ${fgbg & 0xFF:02X}\n"
f'#include "{source}"\n')
def _wrap_prg(code: bytes, data: bytes) -> bytes:
"""Lay out load-address prefix + BASIC stub + viewer code + data @ $2000."""
mem = bytearray()
mem += _STUB # $1C01..
mem += b"\x00" * (ML_ORG - BASIC_START - len(_STUB))
mem += code # $1C20..
if len(mem) > DATA_ORG - BASIC_START:
raise AssemblerError("viewer code overruns the $2000 data area")
mem += b"\x00" * (DATA_ORG - BASIC_START - len(mem))
mem += data # $2000..
return bytes([BASIC_START & 0xFF, BASIC_START >> 8]) + bytes(mem)
def build_prg_color(attributes: bytes, fgbg: int = 0x0F) -> bytes:
"""Return the loadable PRG for the 80x100 chunky-colour viewer.
`attributes` is the 8000-byte per-cell colour map (colour in the high
nibble); the viewer fills the character matrix with a solid glyph itself.
"""
return _wrap_prg(_assemble(fgbg, "color.s"), attributes)
def build_prg_hicolor(vdc_image: bytes, fgbg: int = 0x00) -> bytes:
"""Return the loadable PRG for the 640x200 custom-charset viewer (font mode).
Used by both the `hicolor` and `mono` modes. `vdc_image` is the full VDC RAM
image (character codes, attributes and the custom character set already laid
out); `fgbg` carries the global background in its low nibble. The viewer
copies the image verbatim into VDC RAM.
"""
return _wrap_prg(_assemble(fgbg, "hicolor.s"), vdc_image)
_SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
def build_slideshow_prg(fgbg_list, advance: str = "both", seconds: int = 10,
loop: bool = True, video: str = "pal") -> bytes:
"""Return the bootable C128 slideshow viewer PRG (RUN\"PIC\").
``fgbg_list`` is one VDC R26 background byte per image (conv.meta["fgbg"]);
the per-image pictures are separate "00".."NN" files the viewer loads. The
viewer is code-only (no appended image) -- just the BASIC stub + the 8502
loop + the ss_fgbg table.
"""
if not fgbg_list:
raise AssemblerError("a slideshow needs at least one image")
jiffyps = 60 if video == "ntsc" else 50
table = ",".join(str(int(b) & 0xFF) for b in fgbg_list)
code = _xa(
f"#define WAITMODE {_SS_WAITMODE[advance]}\n"
f"#define WAITSECS {max(0, int(seconds))}\n"
f"#define JIFFYPS {jiffyps}\n"
f"#define NIMAGES {len(fgbg_list)}\n"
f"#define LOOPFLAG {1 if loop else 0}\n"
'#include "slideshow.s"\n'
"ss_fgbg:\n"
f" .byte {table}\n")
mem = bytearray()
mem += _STUB # $1C01 BASIC stub (10 SYS7200)
mem += b"\x00" * (ML_ORG - BASIC_START - len(_STUB))
mem += code # $1C20 viewer + ss_fgbg table
return bytes([BASIC_START & 0xFF, BASIC_START >> 8]) + bytes(mem)