First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
156
lenser/atari/viewer/assemble.py
Normal file
156
lenser/atari/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""Assemble the Atari 6502 boot viewers with `xa` (origin $2000, no load prefix)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
SOURCES = {
|
||||
"gr15": "gr15.s",
|
||||
"gr9": "gr9.s",
|
||||
"gr8": "gr8.s",
|
||||
"gr15dli": "gr15dli.s",
|
||||
}
|
||||
|
||||
_cache: dict[tuple, bytes] = {}
|
||||
|
||||
# How long the viewer holds the picture (see atari/viewer/awyt.i).
|
||||
WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2}
|
||||
|
||||
# Slideshow advance behaviour and per-mode multi-image viewer parameters
|
||||
# (source, ANTIC mode byte, GPRIOR, colour-register layout).
|
||||
SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
|
||||
SLIDESHOW_PARAMS = {
|
||||
"gr15": (0x0E, 0x00, 0),
|
||||
"gr9": (0x0F, 0x40, 1),
|
||||
"gr8": (0x0F, 0x00, 2),
|
||||
}
|
||||
SLIDESHOW_SOURCES = dict.fromkeys(SLIDESHOW_PARAMS, "slideshow_static.s")
|
||||
SLIDESHOW_SOURCES["gr15dli"] = "slideshow_dli.s" # DLI mode, its own engine
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def assemble_stub(viewer_key: str, display: str = "forever", seconds: int = 0,
|
||||
video: str = "ntsc") -> bytes:
|
||||
waitmode = WAIT_MODES.get(display, 0)
|
||||
rate = 50 if video == "pal" else 60
|
||||
key = (viewer_key, waitmode, int(seconds), rate)
|
||||
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")
|
||||
if not os.path.exists(os.path.join(VIEWER_DIR, SOURCES[viewer_key])):
|
||||
raise AssemblerError(f"viewer source missing: {SOURCES[viewer_key]}")
|
||||
|
||||
# Wrapper sets the options then includes the real source; runs from VIEWER_DIR
|
||||
# so the source's #include "awyt.i" resolves (xa looks relative to cwd).
|
||||
wrapper = (
|
||||
f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define RATE {rate}\n"
|
||||
f'#include "{SOURCES[viewer_key]}"\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 for {viewer_key}:\n{proc.stdout}{proc.stderr}")
|
||||
with open(out, "rb") as f:
|
||||
raw = f.read()
|
||||
finally:
|
||||
os.unlink(wrap)
|
||||
_cache[key] = raw
|
||||
return raw
|
||||
|
||||
|
||||
def _xa(wrapper: str, what: str) -> bytes:
|
||||
"""Assemble a generated wrapper with xa (run from VIEWER_DIR so #includes
|
||||
resolve); return raw bytes."""
|
||||
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 for {what}:\n{proc.stdout}{proc.stderr}")
|
||||
with open(out, "rb") as f:
|
||||
return f.read()
|
||||
finally:
|
||||
os.unlink(wrap)
|
||||
|
||||
|
||||
def build_slideshow_stub(viewer_key: str, n_images: int, base_sec: int, spi: int,
|
||||
advance: str = "both", seconds: int = 10,
|
||||
loop: bool = True, video: str = "ntsc") -> bytes:
|
||||
"""Assemble the multi-image slideshow viewer (origin $2000, no load prefix).
|
||||
|
||||
``base_sec`` is the disk sector of image 0 and ``spi`` the sectors per image
|
||||
(both fixed by the ATR layout); the viewer SIO-reads image i from
|
||||
base_sec + i*spi. ``advance``/``seconds`` set the per-slide dwell, ``loop``
|
||||
whether it wraps.
|
||||
"""
|
||||
if viewer_key not in SLIDESHOW_SOURCES:
|
||||
raise AssemblerError(f"no Atari slideshow viewer for mode {viewer_key}")
|
||||
rate = 50 if video == "pal" else 60
|
||||
common = (f"#define WAITMODE {SS_WAITMODE[advance]}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define RATE {rate}\n"
|
||||
f"#define NIMAGES {n_images}\n"
|
||||
f"#define LOOPFLAG {1 if loop else 0}\n"
|
||||
f"#define BASESEC {base_sec}\n"
|
||||
f"#define SPI {spi}\n")
|
||||
if viewer_key in SLIDESHOW_PARAMS: # static gr15/gr9/gr8
|
||||
dlmode, gprior, colormode = SLIDESHOW_PARAMS[viewer_key]
|
||||
common += (f"#define DLMODE ${dlmode:02X}\n"
|
||||
f"#define GPRIOR ${gprior:02X}\n"
|
||||
f"#define COLORMODE {colormode}\n")
|
||||
return _xa(common + f'#include "{SLIDESHOW_SOURCES[viewer_key]}"\n',
|
||||
f"slideshow_{viewer_key}")
|
||||
|
||||
|
||||
CART_SIZE = 0x4000 # 16K Atari cartridge ROM at $8000-$BFFF
|
||||
|
||||
|
||||
def build_cart_rom(viewer_key: str, data: bytes, display: str = "forever",
|
||||
seconds: int = 0, video: str = "ntsc") -> bytes:
|
||||
"""Assemble the loader + the disk viewer stub + picture data into a 16K
|
||||
cartridge ROM with the Atari run/init footer at $BFFA."""
|
||||
stub = assemble_stub(viewer_key, display, seconds, video)
|
||||
wrapper = (
|
||||
f"#define STUB_PAGES {(len(stub) + 255) // 256}\n"
|
||||
f"#define DATA_PAGES {(len(data) + 255) // 256}\n"
|
||||
f"#define STUB_LEN {len(stub)}\n"
|
||||
'#include "cart.s"\n')
|
||||
loader = _xa(wrapper, "cart")
|
||||
rom = bytearray(loader + stub + bytes(data))
|
||||
if len(rom) > CART_SIZE:
|
||||
raise AssemblerError(
|
||||
f"viewer + image = {len(rom)} bytes, over the 16K cartridge limit")
|
||||
rom += bytes(CART_SIZE - len(rom))
|
||||
# Atari cartridge footer at $BFFA (ROM offset $3FFA).
|
||||
rom[0x3FFA] = 0x00; rom[0x3FFB] = 0x80 # CARTCS run address = $8000
|
||||
rom[0x3FFC] = 0x00 # cart present
|
||||
rom[0x3FFD] = 0x04 # option byte: start the cartridge
|
||||
rom[0x3FFE] = 0x00; rom[0x3FFF] = 0x80 # CARTAD init address = $8000
|
||||
return bytes(rom)
|
||||
Loading…
Add table
Add a link
Reference in a new issue