102 lines
3.9 KiB
Python
102 lines
3.9 KiB
Python
"""Assemble the BBC 6502 viewer with `xa` and patch in per-image parameters."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
LOAD_ADDR = 0x1900 # DFS PAGE
|
|
WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2}
|
|
|
|
|
|
class AssemblerError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def have_xa() -> bool:
|
|
return shutil.which("xa") is not None
|
|
|
|
|
|
def _xa(wrapper: str) -> bytes:
|
|
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)
|
|
|
|
|
|
SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
|
|
|
|
|
|
def build_slideshow_viewer(bbc_mode: int, ncol: int, base: int, palettes,
|
|
advance: str = "both", seconds: int = 10,
|
|
loop: bool = True, video: str = "pal") -> bytes:
|
|
"""Return the multi-image BBC loader (origin $1900).
|
|
|
|
``palettes`` is one physical-colour list per image (each truncated/padded to
|
|
ncol) -- emitted as the ss_pal table the loader indexes by slide; the screen
|
|
base hex is patched into the OSCLI *LOAD string.
|
|
"""
|
|
if not palettes:
|
|
raise AssemblerError("a slideshow needs at least one image")
|
|
rate = 60 if video == "ntsc" else 50
|
|
flat = []
|
|
for p in palettes:
|
|
row = list(p[:ncol])
|
|
flat += row + [0] * (ncol - len(row))
|
|
table = ",".join(str(b & 0xFF) for b in flat)
|
|
wrapper = (f"#define WAITMODE {SS_WAITMODE[advance]}\n"
|
|
f"#define WAITSECS {max(0, int(seconds))}\n"
|
|
f"#define RATE {rate}\n"
|
|
f"#define NIMAGES {len(palettes)}\n"
|
|
f"#define LOOPFLAG {1 if loop else 0}\n"
|
|
f"#define MODE {bbc_mode}\n"
|
|
f"#define NCOL {ncol}\n"
|
|
'#include "slideshow.s"\n'
|
|
"ss_pal:\n"
|
|
f" .byte {table}\n")
|
|
raw = bytearray(_xa(wrapper))
|
|
off = raw.find(b"LOAD 00 ")
|
|
if off < 0:
|
|
raise AssemblerError("could not locate the OSCLI string to patch")
|
|
raw[off + 8:off + 12] = f"{base:04X}".encode("ascii") # screen-base hex
|
|
return bytes(raw)
|
|
|
|
|
|
def build_viewer(bbc_mode: int, ncol: int, physicals, base: int,
|
|
display: str = "forever", seconds: int = 0,
|
|
video: str = "pal") -> bytes:
|
|
"""Return the assembled loader (origin $1900) with the mode/palette/screen-base
|
|
parameters patched in."""
|
|
waitmode = WAIT_MODES.get(display, 0)
|
|
rate = 60 if video == "ntsc" else 50 # bbcb is PAL (50 Hz)
|
|
wrapper = (f"#define WAITMODE {waitmode}\n"
|
|
f"#define WAITSECS {max(0, int(seconds))}\n"
|
|
f"#define RATE {rate}\n"
|
|
'#include "bbc.s"\n')
|
|
raw = bytearray(_xa(wrapper))
|
|
|
|
off = raw.find(b"LOAD IMG ")
|
|
if off < 0:
|
|
raise AssemblerError("could not locate the OSCLI string to patch")
|
|
raw[off + 9:off + 13] = f"{base:04X}".encode("ascii") # screen-base hex
|
|
raw[off + 14] = bbc_mode & 0xFF # p_mode
|
|
raw[off + 15] = ncol & 0xFF # p_ncol
|
|
for i, p in enumerate(physicals[:8]): # p_pal
|
|
raw[off + 16 + i] = p & 0xFF
|
|
return bytes(raw)
|