First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
206
lenser/viewer/assemble.py
Normal file
206
lenser/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""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",
|
||||
"slideshow": "slideshow.s",
|
||||
"slideshow_fli": "slideshow_fli.s",
|
||||
"slideshow_interlace": "slideshow_interlace.s",
|
||||
}
|
||||
|
||||
# slideshow advance behaviour -> WAITMODE (see viewer/wait.i)
|
||||
SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
|
||||
|
||||
_cache: dict[tuple, bytes] = {}
|
||||
|
||||
# How long the viewer holds the picture (see viewer/wait.i).
|
||||
WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2}
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def assemble_stub(viewer_key: str, display: str = "key", seconds: int = 0,
|
||||
video: str = "pal", separate: bool = False) -> bytes:
|
||||
"""Assemble a viewer stub to raw bytes (origin $0801, no load-address prefix).
|
||||
|
||||
``display`` / ``seconds`` choose the wait behaviour (viewer/wait.i); ``video``
|
||||
sets the jiffy rate the seconds timer counts (50 PAL / 60 NTSC). ``separate``
|
||||
makes the viewer KERNAL-load the picture from a "data" file instead of having
|
||||
it appended (viewer/loaddata.i).
|
||||
"""
|
||||
waitmode = WAIT_MODES.get(display, 1)
|
||||
jiffyps = 60 if video == "ntsc" else 50
|
||||
sep = 1 if separate else 0
|
||||
key = (viewer_key, waitmode, int(seconds), jiffyps, sep)
|
||||
if key in _cache:
|
||||
return _cache[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/")
|
||||
|
||||
if not os.path.exists(os.path.join(VIEWER_DIR, SOURCES[viewer_key])):
|
||||
raise AssemblerError(f"viewer source missing: {SOURCES[viewer_key]}")
|
||||
|
||||
# A generated wrapper sets the options then includes the real source.
|
||||
wrapper = (
|
||||
f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define JIFFYPS {jiffyps}\n"
|
||||
f"#define SEPARATE {sep}\n"
|
||||
f'#include "{SOURCES[viewer_key]}"\n')
|
||||
raw = _xa(wrapper, viewer_key)
|
||||
_cache[key] = raw
|
||||
return raw
|
||||
|
||||
|
||||
def _xa(wrapper: str, what: str) -> bytes:
|
||||
"""Assemble a generated wrapper with xa; return raw bytes (no load prefix).
|
||||
|
||||
The wrapper lives in VIEWER_DIR and xa runs there so its #include "...s" and
|
||||
the source's #include "wait.i" both resolve (xa looks relative to cwd)."""
|
||||
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)")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
out = os.path.join(td, "viewer.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 for {what}:\n{proc.stdout}{proc.stderr}")
|
||||
with open(out, "rb") as f:
|
||||
return f.read()
|
||||
finally:
|
||||
os.unlink(wrap)
|
||||
|
||||
|
||||
CART_SIZE = 0x4000 # 16K C64 cartridge ROM at $8000-$BFFF
|
||||
|
||||
|
||||
def build_cart_rom(viewer_key: str, data: bytes, display: str = "forever",
|
||||
seconds: int = 0, video: str = "pal") -> bytes:
|
||||
"""Assemble cart.s for `viewer_key` (hires/multicolor), append the image
|
||||
data, and pad to a 16K cart ROM. Raises if the mode/size can't be a cart."""
|
||||
if viewer_key not in ("hires", "multicolor"):
|
||||
raise AssemblerError(
|
||||
f"the {viewer_key} mode is too large for a 16K cartridge -- "
|
||||
"use a disk image, or pick hires/multicolor/mono")
|
||||
mcmode = 1 if viewer_key == "multicolor" else 0
|
||||
waitmode = WAIT_MODES.get(display, 0)
|
||||
rate = 60 if video == "ntsc" else 50
|
||||
npages = (len(data) + 255) // 256
|
||||
wrapper = (
|
||||
f"#define MCMODE {mcmode}\n"
|
||||
f"#define NPAGES {npages}\n"
|
||||
f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define RATE {rate}\n"
|
||||
'#include "cart.s"\n')
|
||||
rom = _xa(wrapper, "cart") + bytes(data)
|
||||
if len(rom) > CART_SIZE:
|
||||
raise AssemblerError(
|
||||
f"image + cart viewer = {len(rom)} bytes, over the 16K cartridge "
|
||||
"limit -- use a disk image for this mode")
|
||||
return rom + bytes(CART_SIZE - len(rom))
|
||||
|
||||
|
||||
def build_viewer_prg(viewer_key: str, data: bytes, data_addr: int = DATA_ADDR,
|
||||
display: str = "key", seconds: int = 0,
|
||||
video: str = "pal", separate: bool = False) -> 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, ...). When ``separate`` is set the picture
|
||||
is NOT appended -- the viewer KERNAL-loads it from a "data" file at run time
|
||||
(use ``build_data_prg`` to write that file) -- so this returns just the code.
|
||||
"""
|
||||
stub = assemble_stub(viewer_key, display, seconds, video, separate)
|
||||
if separate:
|
||||
return bytes([LOAD_ADDR & 0xFF, (LOAD_ADDR >> 8) & 0xFF]) + stub
|
||||
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
|
||||
|
||||
|
||||
def build_data_prg(data: bytes, data_addr: int = DATA_ADDR) -> bytes:
|
||||
"""The picture as a standalone PRG (2-byte load address + data), for the
|
||||
separate-binary layout's "data" file."""
|
||||
return bytes([data_addr & 0xFF, (data_addr >> 8) & 0xFF]) + bytes(data)
|
||||
|
||||
|
||||
def build_slideshow_prg(mode_bytes, advance: str = "both", seconds: int = 10,
|
||||
loop: bool = True, video: str = "pal",
|
||||
flavor: str = "simple") -> bytes:
|
||||
"""Assemble a slideshow viewer PRG (code only; pictures are separate files).
|
||||
|
||||
``flavor`` selects the engine: "simple" (mixed hires/multicolor/mono, one
|
||||
mode byte per image), "fli" (all FLI), or "interlace" (all IFLI). For the
|
||||
simple flavor ``mode_bytes`` is one byte per image (0 hires/mono, 1
|
||||
multicolor), emitted as the ss_modes table; for fli/interlace only its
|
||||
length (the image count) matters. ``advance`` picks the wait behaviour
|
||||
(key/seconds/both), ``seconds`` the timeout, ``loop`` whether it wraps.
|
||||
"""
|
||||
if not mode_bytes:
|
||||
raise AssemblerError("a slideshow needs at least one image")
|
||||
waitmode = SS_WAITMODE[advance]
|
||||
jiffyps = 60 if video == "ntsc" else 50
|
||||
defines = (
|
||||
f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define JIFFYPS {jiffyps}\n"
|
||||
f"#define NIMAGES {len(mode_bytes)}\n"
|
||||
f"#define LOOPFLAG {1 if loop else 0}\n")
|
||||
if flavor == "simple":
|
||||
table = ",".join(str(int(b) & 1) for b in mode_bytes)
|
||||
wrapper = (defines + '#include "slideshow.s"\n'
|
||||
"ss_modes:\n" f" .byte {table}\n")
|
||||
elif flavor == "fli":
|
||||
wrapper = (defines + f"#define NTSC {1 if video == 'ntsc' else 0}\n"
|
||||
'#include "slideshow_fli.s"\n')
|
||||
elif flavor == "interlace":
|
||||
wrapper = defines + '#include "slideshow_interlace.s"\n'
|
||||
else:
|
||||
raise AssemblerError(f"unknown slideshow flavor: {flavor}")
|
||||
raw = _xa(wrapper, f"slideshow_{flavor}")
|
||||
return bytes([LOAD_ADDR & 0xFF, (LOAD_ADDR >> 8) & 0xFF]) + raw
|
||||
Loading…
Add table
Add a link
Reference in a new issue