First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
1
lenser/atari/__init__.py
Normal file
1
lenser/atari/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Atari 8-bit (Atari 400/800/XL/XE) image conversion and bootable disk export."""
|
||||
101
lenser/atari/atr.py
Normal file
101
lenser/atari/atr.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""Write a bootable Atari ``.atr`` disk image natively (no external tools).
|
||||
|
||||
A self-booting Atari disk needs no DOS: sector 1 begins with a 6-byte boot header
|
||||
(flags, sector-count, load-address, init-address); the OS loads ``count`` 128-byte
|
||||
sectors to the load address and JSRs ``load+6``. We pack the whole viewer+picture
|
||||
blob that way, so inserting the disk shows the picture.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
SECTOR_SIZE = 128
|
||||
TOTAL_SECTORS = 720 # single density, ~90K
|
||||
LOAD_ADDR = 0x2000 # boot load address (blob origin)
|
||||
DATA_ADDR = 0x4000 # where the bitmap must land (4K-aligned for ANTIC)
|
||||
|
||||
|
||||
class AtrError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def build_blob(stub: bytes, data: bytes) -> bytes:
|
||||
"""Combine the assembled viewer ``stub`` (origin $2000, starts with the 6-byte
|
||||
boot header) + padding + picture ``data`` (which must reside from $4000)."""
|
||||
pad = (DATA_ADDR - LOAD_ADDR) - len(stub)
|
||||
if pad < 0:
|
||||
raise AtrError(f"viewer stub {len(stub)} bytes exceeds "
|
||||
f"{DATA_ADDR - LOAD_ADDR} before $4000")
|
||||
return stub + bytes(pad) + bytes(data)
|
||||
|
||||
|
||||
def _write_atr_sectors(path: str, data: bytes) -> str:
|
||||
"""Write ``data`` (already laid out as raw sectors) as a single-density ATR,
|
||||
padded to the full 720-sector disk with the 16-byte ATR header."""
|
||||
data = bytearray(data)
|
||||
if len(data) > TOTAL_SECTORS * SECTOR_SIZE:
|
||||
raise AtrError("slideshow exceeds the 720-sector disk capacity")
|
||||
data += bytes(TOTAL_SECTORS * SECTOR_SIZE - len(data))
|
||||
paragraphs = (TOTAL_SECTORS * SECTOR_SIZE) // 16
|
||||
header = bytes([
|
||||
0x96, 0x02,
|
||||
paragraphs & 0xFF, (paragraphs >> 8) & 0xFF,
|
||||
SECTOR_SIZE & 0xFF, (SECTOR_SIZE >> 8) & 0xFF,
|
||||
(paragraphs >> 16) & 0xFF, (paragraphs >> 24) & 0xFF,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
])
|
||||
with open(path, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(data)
|
||||
return path
|
||||
|
||||
|
||||
def write_slideshow_atr(path: str, stub: bytes, images: list[bytes],
|
||||
boot_sectors: int, spi: int) -> str:
|
||||
"""Write a self-booting slideshow ATR.
|
||||
|
||||
Sectors 1..boot_sectors hold ``stub`` (boot header byte 1 patched to
|
||||
boot_sectors so the OS loads it all); each image then occupies ``spi``
|
||||
consecutive 128-byte sectors (image i at sector boot_sectors + 1 + i*spi),
|
||||
matching what the viewer SIO-reads.
|
||||
"""
|
||||
if len(stub) > boot_sectors * SECTOR_SIZE:
|
||||
raise AtrError(f"slideshow viewer {len(stub)} bytes exceeds "
|
||||
f"{boot_sectors} boot sectors")
|
||||
blob = bytearray(stub)
|
||||
blob[1] = boot_sectors # OS loads this many sectors
|
||||
blob += bytes(boot_sectors * SECTOR_SIZE - len(blob))
|
||||
for img in images:
|
||||
if len(img) > spi * SECTOR_SIZE:
|
||||
raise AtrError("image larger than its sector allotment")
|
||||
blob += bytes(img) + bytes(spi * SECTOR_SIZE - len(img))
|
||||
return _write_atr_sectors(path, bytes(blob))
|
||||
|
||||
|
||||
def write_boot_atr(path: str, blob: bytes) -> str:
|
||||
"""Write ``blob`` as a bootable single-density ATR. Patches the boot sector
|
||||
count (byte 1) from the blob length."""
|
||||
nsec = (len(blob) + SECTOR_SIZE - 1) // SECTOR_SIZE
|
||||
if nsec > 255:
|
||||
raise AtrError(f"boot blob needs {nsec} sectors (max 255)")
|
||||
if nsec > TOTAL_SECTORS:
|
||||
raise AtrError("boot blob exceeds disk capacity")
|
||||
|
||||
blob = bytearray(blob)
|
||||
blob[1] = nsec # boot header sector count
|
||||
blob += bytes((-len(blob)) % SECTOR_SIZE) # pad to whole sectors
|
||||
|
||||
data = bytearray(blob)
|
||||
data += bytes(TOTAL_SECTORS * SECTOR_SIZE - len(data)) # pad disk to 720 sectors
|
||||
|
||||
paragraphs = (TOTAL_SECTORS * SECTOR_SIZE) // 16
|
||||
header = bytes([
|
||||
0x96, 0x02, # magic
|
||||
paragraphs & 0xFF, (paragraphs >> 8) & 0xFF, # size (paragraphs) low
|
||||
SECTOR_SIZE & 0xFF, (SECTOR_SIZE >> 8) & 0xFF,
|
||||
(paragraphs >> 16) & 0xFF, (paragraphs >> 24) & 0xFF,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
])
|
||||
with open(path, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(data)
|
||||
return path
|
||||
20
lenser/atari/car.py
Normal file
20
lenser/atari/car.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Write an Atari .car cartridge image (CART header + ROM)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
TYPE_STD_16K = 2 # "Standard 16 KB cartridge"
|
||||
|
||||
|
||||
def write_car(rom: bytes, path: str, cart_type: int = TYPE_STD_16K) -> str:
|
||||
"""Wrap a cart `rom` in the .car container (16-byte CART header + ROM).
|
||||
The header holds the cartridge type and a checksum (sum of all ROM bytes)."""
|
||||
header = bytearray(16)
|
||||
header[0:4] = b"CART"
|
||||
struct.pack_into(">I", header, 4, cart_type)
|
||||
struct.pack_into(">I", header, 8, sum(rom) & 0xFFFFFFFF)
|
||||
with open(path, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(rom)
|
||||
return path
|
||||
31
lenser/atari/convert/__init__.py
Normal file
31
lenser/atari/convert/__init__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Atari conversion dispatch."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import imageprep
|
||||
from .. import palette as apal
|
||||
from . import gr15
|
||||
|
||||
_MODULES = {"gr15": gr15}
|
||||
for _name in ("gr9", "gr8", "gr15dli", "mono"):
|
||||
try:
|
||||
_mod = __import__(f"lenser.atari.convert.{_name}", fromlist=[_name])
|
||||
_MODULES[_name] = _mod
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="gr15", palette_name="ntsc",
|
||||
dither_mode="floyd", intensive=False,
|
||||
prep_opt: imageprep.PrepOptions | None = None, base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES[mode]
|
||||
border_rgb = apal.get_palette(palette_name)[0]
|
||||
img_rgb = imageprep.prepare(
|
||||
path_or_img, module.WIDTH, module.HEIGHT, module.PIXEL_ASPECT,
|
||||
prep_opt, border_rgb=border_rgb,
|
||||
)
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
176
lenser/atari/convert/_common.py
Normal file
176
lenser/atari/convert/_common.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""Shared helpers for the Atari encoders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
DATA_ADDR = 0x4000 # bitmap base
|
||||
COLOR_ADDR = 0x6000 # colour data base (fixed, after the bitmap)
|
||||
SPLIT_LINE = 102 # lines that fit in the first 4K ($4000-$4FEF)
|
||||
BYTES_PER_LINE = 40
|
||||
LINES = 192
|
||||
|
||||
|
||||
def split_screen(line_bytes: list[bytes]) -> bytes:
|
||||
"""Lay out 192 screen lines with the 16-byte gap that pushes line 102 onto
|
||||
the $5000 boundary (so no ANTIC line crosses a 4K boundary), then pad up to
|
||||
COLOR_ADDR so colour data can follow at a fixed address."""
|
||||
first = b"".join(line_bytes[:SPLIT_LINE]) # 4080 bytes -> $4000
|
||||
second = b"".join(line_bytes[SPLIT_LINE:]) # 3600 bytes -> $5000
|
||||
body = first + bytes(0x1000 - len(first)) + second # gap fills to $5000
|
||||
pad = (COLOR_ADDR - DATA_ADDR) - len(body)
|
||||
return body + bytes(pad)
|
||||
|
||||
|
||||
def luminance_lab(img_rgb, plab):
|
||||
"""Return (image, palette) recast into luminance-only CIELAB (L, 0, 0), so
|
||||
matching is by brightness alone -- used by the single-hue modes."""
|
||||
from ...palette import srgb_to_lab
|
||||
L = srgb_to_lab(img_rgb)[..., 0]
|
||||
img_mono = np.zeros(img_rgb.shape[:2] + (3,))
|
||||
img_mono[..., 0] = L
|
||||
plab_mono = np.zeros_like(plab)
|
||||
plab_mono[:, 0] = plab[:, 0]
|
||||
return img_mono, plab_mono
|
||||
|
||||
|
||||
def choose_palette(img_lab: np.ndarray, plab: np.ndarray, k: int,
|
||||
iters: int = 12) -> list[int]:
|
||||
"""Pick the ``k`` palette register values (0..255) that best represent the
|
||||
image, by palette-constrained k-means in CIELAB."""
|
||||
flat = img_lab.reshape(-1, 3).astype(np.float32)
|
||||
D = np.sum((flat[:, None, :] - plab[None, :, :].astype(np.float32)) ** 2, axis=-1) # (N,256)
|
||||
|
||||
# k-means++-ish greedy init.
|
||||
chosen = [int(np.argmin(np.sum((plab - flat.mean(0)) ** 2, axis=-1)))]
|
||||
for _ in range(k - 1):
|
||||
md = D[:, chosen].min(axis=1)
|
||||
improv = np.maximum(0.0, md[:, None] - D).sum(axis=0)
|
||||
improv[chosen] = -1.0
|
||||
chosen.append(int(np.argmax(improv)))
|
||||
|
||||
# Lloyd refinement, each centroid snapped to its best palette colour.
|
||||
for _ in range(iters):
|
||||
assign = np.argmin(D[:, chosen], axis=1)
|
||||
new = []
|
||||
for j in range(k):
|
||||
mask = assign == j
|
||||
if not mask.any():
|
||||
new.append(chosen[j])
|
||||
else:
|
||||
new.append(int(np.argmin(D[mask].sum(axis=0))))
|
||||
# keep distinct where possible
|
||||
if new == chosen:
|
||||
break
|
||||
chosen = new
|
||||
return chosen
|
||||
|
||||
|
||||
def _seg_all(sub, c1all, c2):
|
||||
"""Distance from each ``sub`` pixel to the segment between every palette colour
|
||||
(c1all, shape (256,3)) and a fixed endpoint c2. Returns (256, Nsub)."""
|
||||
seg = c2 - c1all # (256,3)
|
||||
L = np.sum(seg * seg, axis=1) + 1e-9 # (256,)
|
||||
rel = sub[None, :, :] - c1all[:, None, :] # (256,Nsub,3)
|
||||
t = np.clip(np.sum(rel * seg[:, None, :], axis=2) / L[:, None], 0.0, 1.0)
|
||||
proj = c1all[:, None, :] + t[:, :, None] * seg[:, None, :]
|
||||
return np.sum((sub[None, :, :] - proj) ** 2, axis=2)
|
||||
|
||||
|
||||
def relevant_candidates(img_lab, plab):
|
||||
"""Palette colours that are the nearest match to some image pixel -- a small
|
||||
set (the image's own gamut) to restrict the dither-aware search to."""
|
||||
flat = img_lab.reshape(-1, 3).astype(np.float32)
|
||||
if len(flat) > 4000:
|
||||
flat = flat[::len(flat) // 4000]
|
||||
d = np.sum((flat[:, None, :] - plab[None, :, :].astype(np.float32)) ** 2, axis=-1)
|
||||
return np.unique(np.argmin(d, axis=1)).astype(np.int64)
|
||||
|
||||
|
||||
def choose_palette_dither(img_lab, plab, k, init=None, n_sample=900, iters=5,
|
||||
candidates=None):
|
||||
"""Dither-aware palette: pick the ``k`` colours whose pairwise *segment* blends
|
||||
(what error diffusion can reproduce) best cover the image -- so the colours
|
||||
span the gamut instead of sitting at k-means centroids. Vectorised local
|
||||
search (all candidates per slot at once) from a k-means start."""
|
||||
from itertools import combinations
|
||||
flat = img_lab.reshape(-1, 3)
|
||||
sub = flat[::max(1, len(flat) // n_sample)] if len(flat) > n_sample else flat
|
||||
colors = list(init) if init is not None else choose_palette(img_lab, plab, k)
|
||||
cand = np.asarray(candidates if candidates is not None else range(256), np.int64)
|
||||
cand_lab = plab[cand].astype(np.float64) # (C,3)
|
||||
for _ in range(iters):
|
||||
changed = False
|
||||
for i in range(k):
|
||||
others = [colors[j] for j in range(k) if j != i]
|
||||
fixed = None
|
||||
for x, y in combinations(others, 2):
|
||||
s = _seg_all(sub, plab[x][None], plab[y])[0]
|
||||
fixed = s if fixed is None else np.minimum(fixed, s)
|
||||
m = None
|
||||
for o in others:
|
||||
d = _seg_all(sub, cand_lab, plab[o]) # (C, Nsub)
|
||||
m = d if m is None else np.minimum(m, d)
|
||||
if fixed is not None:
|
||||
m = np.minimum(m, fixed[None, :])
|
||||
err = m.sum(axis=1) # (C,)
|
||||
for ci, c in enumerate(cand):
|
||||
if c in others:
|
||||
err[ci] = np.inf # avoid duplicate colours
|
||||
best = int(cand[np.argmin(err)])
|
||||
if best != colors[i]:
|
||||
colors[i] = best
|
||||
changed = True
|
||||
if not changed:
|
||||
break
|
||||
return colors
|
||||
|
||||
|
||||
def quantize_global(img_lab, plab, colors, dither_mode):
|
||||
"""Dither the whole image to a fixed global set of palette indices."""
|
||||
from ... import dither
|
||||
H, W, _ = img_lab.shape
|
||||
allowed = np.tile(np.array(colors, dtype=np.int64), (H, W, 1))
|
||||
return dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
|
||||
def pack_2bpp(val_image: np.ndarray) -> list[bytes]:
|
||||
"""160-wide 2-bits-per-pixel -> list of 192 x 40-byte lines."""
|
||||
H, W = val_image.shape
|
||||
lines = []
|
||||
for y in range(H):
|
||||
row = val_image[y]
|
||||
out = bytearray()
|
||||
for x in range(0, W, 4):
|
||||
out.append((row[x] << 6) | (row[x + 1] << 4) | (row[x + 2] << 2) | row[x + 3])
|
||||
lines.append(bytes(out))
|
||||
return lines
|
||||
|
||||
|
||||
def pack_4bpp(val_image: np.ndarray) -> list[bytes]:
|
||||
"""80-wide 4-bits-per-pixel -> list of 192 x 40-byte lines."""
|
||||
H, W = val_image.shape
|
||||
lines = []
|
||||
for y in range(H):
|
||||
row = val_image[y]
|
||||
out = bytearray()
|
||||
for x in range(0, W, 2):
|
||||
out.append((row[x] << 4) | row[x + 1])
|
||||
lines.append(bytes(out))
|
||||
return lines
|
||||
|
||||
|
||||
def pack_1bpp(val_image: np.ndarray) -> list[bytes]:
|
||||
"""320-wide 1-bit-per-pixel -> list of 192 x 40-byte lines."""
|
||||
H, W = val_image.shape
|
||||
lines = []
|
||||
for y in range(H):
|
||||
row = val_image[y]
|
||||
out = bytearray()
|
||||
for x in range(0, W, 8):
|
||||
b = 0
|
||||
for i in range(8):
|
||||
b = (b << 1) | int(row[x + i])
|
||||
out.append(b)
|
||||
lines.append(bytes(out))
|
||||
return lines
|
||||
51
lenser/atari/convert/gr15.py
Normal file
51
lenser/atari/convert/gr15.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Atari GR.15 (ANTIC mode E): 160x192, 4 colours chosen globally from 256.
|
||||
|
||||
No per-cell colour limit, so this is a clean 4-colour dithered image.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import palette as c64pal # for srgb_to_lab (shared)
|
||||
from ...convert.base import (Conversion, mean_error, perceptual_error,
|
||||
DIFFUSION_DITHERS)
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 160, 192
|
||||
PIXEL_ASPECT = 2.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
# Dither-aware palette for error-diffusion modes: pick 4 colours whose blends
|
||||
# span the image gamut (so dithering reproduces saturated/intermediate shades)
|
||||
# instead of k-means centroids the dither can't reach.
|
||||
if dither_mode in DIFFUSION_DITHERS:
|
||||
colors = _common.choose_palette_dither(img_lab, plab, k=4)
|
||||
else:
|
||||
colors = _common.choose_palette(img_lab, plab, k=4)
|
||||
colors.sort(key=lambda c: plab[c, 0]) # value 0 = darkest (background)
|
||||
|
||||
idx = _common.quantize_global(img_lab, plab, colors, dither_mode)
|
||||
value_of = {c: v for v, c in enumerate(colors)}
|
||||
val_image = np.vectorize(value_of.get)(idx).astype(np.uint8)
|
||||
|
||||
lines = _common.pack_2bpp(val_image)
|
||||
data = _common.split_screen(lines) + bytes(colors) # 4 colour regs at $6000
|
||||
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
|
||||
err = (perceptual_error if dither_mode in DIFFUSION_DITHERS else mean_error)
|
||||
return Conversion(
|
||||
mode="gr15", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr15", preview_rgb=preview,
|
||||
error=err(idx, img_lab, plab),
|
||||
meta={"palette": palette_name, "dither": dither_mode, "colors": colors},
|
||||
)
|
||||
84
lenser/atari/convert/gr15dli.py
Normal file
84
lenser/atari/convert/gr15dli.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""Atari GR.15 + DLI: 160x192, a fresh set of 4 colours every 2 scanlines.
|
||||
|
||||
A display-list interrupt rewrites the four colour registers for each 2-line band
|
||||
(96 bands). Every *single* scanline is impossible -- four register writes don't
|
||||
fit the inter-DLI window -- but every 2 lines leaves a comfortable budget, and
|
||||
96x4 colours is still far beyond flat GR.15. The display list (with DLI bits on
|
||||
the right lines) is generated here and shipped in the data block.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import (Conversion, mean_error, perceptual_error,
|
||||
DIFFUSION_DITHERS)
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 160, 192
|
||||
PIXEL_ASPECT = 2.0
|
||||
BAND_H = 2
|
||||
N_BANDS = HEIGHT // BAND_H # 96
|
||||
COLOR_ADDR = 0x6000
|
||||
DL_ADDR = 0x6400 # display list, after the colour table
|
||||
|
||||
|
||||
def make_dlist() -> bytes:
|
||||
"""ANTIC mode-E display list, 4K-split, DLI bit on the last line of each
|
||||
2-line band (odd lines 1..189) so the handler sets up the next band."""
|
||||
dl = bytearray([0x70, 0x70, 0x70]) # 24 blank lines
|
||||
dl += bytes([0x4e, 0x00, 0x40]) # line 0: LMS $4000 (no DLI)
|
||||
for ln in range(1, 102): # lines 1..101
|
||||
dl.append(0x8e if ln % 2 == 1 else 0x0e)
|
||||
dl += bytes([0x4e, 0x00, 0x50]) # line 102: LMS $5000 (no DLI)
|
||||
for ln in range(103, 192): # lines 103..191
|
||||
dl.append(0x8e if (ln % 2 == 1 and ln != 191) else 0x0e)
|
||||
dl += bytes([0x41, DL_ADDR & 0xFF, DL_ADDR >> 8]) # JVB -> start
|
||||
return bytes(dl)
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
band_sets = np.zeros((N_BANDS, 4), dtype=np.int64)
|
||||
aware = dither_mode in DIFFUSION_DITHERS
|
||||
iters = 10 if intensive else 5
|
||||
# restrict the per-band dither-aware search to the image's own gamut (fast).
|
||||
cand = _common.relevant_candidates(img_lab, plab) if aware else None
|
||||
for b in range(N_BANDS):
|
||||
block = img_lab[b * BAND_H:(b + 1) * BAND_H].reshape(-1, 3)
|
||||
cols = _common.choose_palette(block, plab, k=4, iters=iters)
|
||||
if aware: # span each band's gamut so dithering blends to the true shade
|
||||
cols = _common.choose_palette_dither(block, plab, k=4, init=cols,
|
||||
iters=4 if intensive else 3,
|
||||
candidates=cand)
|
||||
cols.sort(key=lambda c: plab[c, 0])
|
||||
band_sets[b] = cols
|
||||
|
||||
allowed = np.repeat((band_sets[np.arange(HEIGHT) // BAND_H])[:, None, :],
|
||||
WIDTH, axis=1)
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
val = np.zeros((HEIGHT, WIDTH), dtype=np.uint8)
|
||||
for y in range(HEIGHT):
|
||||
lut = {int(c): v for v, c in enumerate(band_sets[y // BAND_H])}
|
||||
val[y] = [lut.get(int(p), 0) for p in idx[y]]
|
||||
|
||||
bitmap = _common.split_screen(_common.pack_2bpp(val)) # 8192 -> $4000..$5FFF
|
||||
coltab = band_sets.astype(np.uint8).tobytes() # 384 -> $6000
|
||||
region = coltab + bytes((DL_ADDR - COLOR_ADDR) - len(coltab)) + make_dlist()
|
||||
data = bitmap + region
|
||||
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
return Conversion(
|
||||
mode="gr15dli", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr15dli", preview_rgb=preview,
|
||||
error=(perceptual_error if aware else mean_error)(idx, img_lab, plab),
|
||||
meta={"palette": palette_name, "dither": dither_mode},
|
||||
)
|
||||
40
lenser/atari/convert/gr8.py
Normal file
40
lenser/atari/convert/gr8.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""Atari GR.8 (ANTIC mode F): 320x192 hi-res, two tones of one hue.
|
||||
|
||||
Highest spatial resolution; carries tone by dithering between background and a
|
||||
foreground luminance. ``base_color`` picks the hue (None = greyscale).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 320, 192
|
||||
PIXEL_ASPECT = 1.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
hue = 0 if base_color is None else (int(base_color) & 0x0F)
|
||||
bg_reg = (hue << 4) | 0x00 # darkest of the hue
|
||||
fg_reg = (hue << 4) | 0x0E # brightest of the hue
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
|
||||
img_mono, plab_mono = _common.luminance_lab(img_rgb, plab)
|
||||
idx = _common.quantize_global(img_mono, plab_mono, [bg_reg, fg_reg], dither_mode)
|
||||
val = (idx == fg_reg).astype(np.uint8)
|
||||
|
||||
data = _common.split_screen(_common.pack_1bpp(val)) + bytes([bg_reg, fg_reg])
|
||||
preview = prgb[idx] # already 320 wide
|
||||
|
||||
return Conversion(
|
||||
mode="gr8", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr8", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_mono, plab_mono),
|
||||
meta={"palette": palette_name, "dither": dither_mode, "hue": hue},
|
||||
)
|
||||
48
lenser/atari/convert/gr9.py
Normal file
48
lenser/atari/convert/gr9.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""Atari GR.9 (GTIA): 80x192, 16 luminance shades of one hue.
|
||||
|
||||
Excellent greyscale (hue 0) or tinted monochrome (any of 16 hues) -- 16 real
|
||||
shades, not just dithered. ``base_color`` selects the hue (0..15); None = grey.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 80, 192
|
||||
PIXEL_ASPECT = 4.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
hue = 0 if base_color is None else (int(base_color) & 0x0F)
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
|
||||
img_mono, plab_mono = _common.luminance_lab(img_rgb, plab)
|
||||
ramp = apal.hue_ramp(hue) # 16 register values of this hue
|
||||
idx = _common.quantize_global(img_mono, plab_mono, ramp, dither_mode)
|
||||
val = (idx & 0x0F).astype(np.uint8) # GR.9 pixel = 4-bit luminance
|
||||
|
||||
# GTIA mode 9 takes the hue from COLBK and the luminance from each pixel. A
|
||||
# COLBK of exactly $00, though, blanks the whole playfield to black -- the
|
||||
# register must be non-zero to enable the display. For a tinted hue that is
|
||||
# automatic ((hue<<4) != 0); for greyscale (hue 0) force a non-zero luminance
|
||||
# nibble, which the mode ignores for output (luminance still comes from the
|
||||
# pixels) but which switches the 16-shade display on.
|
||||
colbk = (hue & 0x0F) << 4
|
||||
if colbk == 0:
|
||||
colbk = 0x0E
|
||||
data = _common.split_screen(_common.pack_4bpp(val)) + bytes([colbk])
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
|
||||
return Conversion(
|
||||
mode="gr9", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr9", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_mono, plab_mono),
|
||||
meta={"palette": palette_name, "dither": dither_mode, "hue": hue},
|
||||
)
|
||||
16
lenser/atari/convert/mono.py
Normal file
16
lenser/atari/convert/mono.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Atari monochrome -- GR.9's 16 luminance shades, exposed as the standard
|
||||
``mono`` mode for cross-platform parity. Greyscale by default; ``--mono-base``
|
||||
tints it to one of the GTIA hues (16 real shades of that hue)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import gr9
|
||||
|
||||
WIDTH, HEIGHT, PIXEL_ASPECT = gr9.WIDTH, gr9.HEIGHT, gr9.PIXEL_ASPECT
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
conv = gr9.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
conv.mode = "mono"
|
||||
return conv
|
||||
32
lenser/atari/exporter.py
Normal file
32
lenser/atari/exporter.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Build a bootable Atari .atr from a conversion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..convert.base import Conversion
|
||||
from . import atr, car
|
||||
from .viewer.assemble import assemble_stub, build_cart_rom
|
||||
|
||||
|
||||
def export_atr(conv: Conversion, output_path: str, source_path: str | None = None,
|
||||
display: str = "forever", seconds: int = 0, video: str = "ntsc") -> str:
|
||||
"""Write ``conv`` as a self-booting .atr at ``output_path``.
|
||||
|
||||
``display`` (forever/key/seconds) + ``seconds`` choose how long the viewer
|
||||
holds the picture; on key/seconds it warm-starts the OS. ``video`` sets the
|
||||
frame rate the seconds timer counts (50 PAL / 60 NTSC)."""
|
||||
if not output_path.lower().endswith(".atr"):
|
||||
output_path += ".atr"
|
||||
stub = assemble_stub(conv.viewer, display=display, seconds=seconds, video=video)
|
||||
blob = atr.build_blob(stub, conv.data)
|
||||
return atr.write_boot_atr(output_path, blob)
|
||||
|
||||
|
||||
def export_car(conv: Conversion, output_path: str, source_path: str | None = None,
|
||||
display: str = "forever", seconds: int = 0, video: str = "ntsc") -> str:
|
||||
"""Write ``conv`` as an autostarting 16K Atari .car cartridge (reuses the
|
||||
disk viewer, so display-duration works the same)."""
|
||||
if not output_path.lower().endswith(".car"):
|
||||
output_path += ".car"
|
||||
rom = build_cart_rom(conv.viewer, conv.data, display=display,
|
||||
seconds=seconds, video=video)
|
||||
return car.write_car(rom, output_path)
|
||||
96
lenser/atari/palette.py
Normal file
96
lenser/atari/palette.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Atari 8-bit (GTIA) colour palette.
|
||||
|
||||
The GTIA produces 256 colour-register values: the high nibble is the hue (0 =
|
||||
grey, 1..15 = colours around the NTSC wheel) and the low nibble is the luminance.
|
||||
We generate an NTSC palette with a standard YIQ formula, but if the atari800
|
||||
emulator's palette file is present we load that instead so the preview matches
|
||||
exactly what the emulator displays.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab # reuse the CIELAB conversion
|
||||
|
||||
# Candidate locations for atari800's bundled NTSC palette (768 raw RGB bytes).
|
||||
_PAL_FILES = [
|
||||
"/usr/share/atari800/Palettes/Real.act",
|
||||
"/usr/share/atari800/default.pal",
|
||||
"/usr/local/share/atari800/default.pal",
|
||||
]
|
||||
|
||||
|
||||
def _generate_ntsc() -> np.ndarray:
|
||||
"""Generate a 256x3 (uint8) NTSC palette via a YIQ approximation."""
|
||||
pal = np.zeros((256, 3), dtype=np.float64)
|
||||
# Calibration roughly matching the common Atari NTSC look.
|
||||
sat = 0.30
|
||||
hue0 = -58.0 # phase of hue 1, degrees
|
||||
for reg in range(256):
|
||||
hue = (reg >> 4) & 0x0F
|
||||
lum = reg & 0x0F
|
||||
y = lum / 15.0
|
||||
if hue == 0:
|
||||
i = q = 0.0
|
||||
else:
|
||||
angle = math.radians(hue0 + (hue - 1) * (360.0 / 15.0))
|
||||
i = sat * math.cos(angle)
|
||||
q = sat * math.sin(angle)
|
||||
r = y + 0.956 * i + 0.621 * q
|
||||
g = y - 0.272 * i - 0.647 * q
|
||||
b = y - 1.106 * i + 1.703 * q
|
||||
pal[reg] = [r, g, b]
|
||||
return np.clip(pal * 255.0 + 0.5, 0, 255).astype(np.uint8).astype(np.float64)
|
||||
|
||||
|
||||
def _load_pal_file() -> np.ndarray | None:
|
||||
for path in _PAL_FILES:
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if len(data) >= 768:
|
||||
return np.frombuffer(data[:768], dtype=np.uint8).reshape(256, 3).astype(np.float64)
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
_CACHE: dict[str, np.ndarray] = {}
|
||||
|
||||
|
||||
def get_palette(name: str = "ntsc") -> np.ndarray:
|
||||
"""Return the 256x3 sRGB palette (float64, 0..255)."""
|
||||
if "rgb" not in _CACHE:
|
||||
_CACHE["rgb"] = _load_pal_file()
|
||||
if _CACHE["rgb"] is None:
|
||||
_CACHE["rgb"] = _generate_ntsc()
|
||||
return _CACHE["rgb"]
|
||||
|
||||
|
||||
def palette_lab(name: str = "ntsc") -> np.ndarray:
|
||||
"""Return the 256 palette colours in CIELAB (256x3)."""
|
||||
if "lab" not in _CACHE:
|
||||
_CACHE["lab"] = srgb_to_lab(get_palette(name))
|
||||
return _CACHE["lab"]
|
||||
|
||||
|
||||
def hue_ramp(hue: int) -> list[int]:
|
||||
"""The 16 register values of one hue (luminance 0..15) -- for GR.9 / mono."""
|
||||
return [((hue & 0x0F) << 4) | lum for lum in range(16)]
|
||||
|
||||
|
||||
def nearest_hue(rgb) -> int:
|
||||
"""Atari hue (0..15) whose mid-luminance colour best matches ``rgb``."""
|
||||
from ..palette import srgb_to_lab
|
||||
lab = srgb_to_lab(np.asarray(rgb, dtype=np.float64))
|
||||
pl = palette_lab()
|
||||
best, best_d = 0, float("inf")
|
||||
for h in range(16):
|
||||
d = float(np.sum((pl[(h << 4) | 8] - lab) ** 2))
|
||||
if d < best_d:
|
||||
best, best_d = h, d
|
||||
return best
|
||||
1
lenser/atari/viewer/__init__.py
Normal file
1
lenser/atari/viewer/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .assemble import AssemblerError, SOURCES, assemble_stub, have_xa # noqa: F401
|
||||
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)
|
||||
48
lenser/atari/viewer/awyt.i
Normal file
48
lenser/atari/viewer/awyt.i
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
; Shared "how long to show the picture" epilogue for the Atari viewers.
|
||||
; Selected at assembly time by WAITMODE (set by atari/viewer/assemble.py):
|
||||
; 0 forever -- loop, just defeating attract mode
|
||||
; 1 until a key -- poll CH ($2FC), then warm-start the OS
|
||||
; 2 WAITSECS secs -- count RTCLOK frames (RATE per second), then warm-start
|
||||
; "Exit" warm-starts the OS ($E474); on the XL that brings back a usable system.
|
||||
|
||||
#if WAITMODE == 0
|
||||
awloop:
|
||||
lda #$00
|
||||
sta $4d ; defeat attract mode
|
||||
jmp awloop
|
||||
#endif
|
||||
|
||||
#if WAITMODE == 1
|
||||
lda #$ff
|
||||
sta $2fc ; clear CH (last-key register; $FF = no key)
|
||||
awloop:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $2fc
|
||||
cmp #$ff
|
||||
beq awloop
|
||||
lda #$00
|
||||
sta $09 ; clear BOOT? so warmstart enters BASIC, not re-boot
|
||||
jmp $e474 ; warm-start
|
||||
#endif
|
||||
|
||||
#if WAITMODE == 2
|
||||
lda #$00
|
||||
sta $12
|
||||
sta $13
|
||||
sta $14 ; reset RTCLOK frame counter (16-bit in $13,$14)
|
||||
awloop:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $13
|
||||
cmp #>(WAITSECS*RATE)
|
||||
bcc awloop
|
||||
bne awdone
|
||||
lda $14
|
||||
cmp #<(WAITSECS*RATE)
|
||||
bcc awloop
|
||||
awdone:
|
||||
lda #$00
|
||||
sta $09 ; clear BOOT? so warmstart enters BASIC, not re-boot
|
||||
jmp $e474 ; warm-start
|
||||
#endif
|
||||
67
lenser/atari/viewer/cart.s
Normal file
67
lenser/atari/viewer/cart.s
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
; lenser -- Atari 16K cartridge loader ($8000-$BFFF).
|
||||
;
|
||||
; Reuses the disk viewer unchanged. The disk viewer "stub" (origin $2000, which
|
||||
; begins with the 6-byte boot header and contains cont + the display list) and
|
||||
; the picture data (origin $4000) are stored back-to-back in the cartridge ROM
|
||||
; after this loader. We copy the stub to $2000 and the data to $4000, then jump
|
||||
; to cont ($2006) -- exactly the state a disk boot would have produced.
|
||||
;
|
||||
; #defines set by viewer/assemble.py --
|
||||
; STUB_PAGES / DATA_PAGES 256-byte page counts to copy
|
||||
; STUB_LEN exact stub length (data follows it in ROM)
|
||||
;
|
||||
; assembled by atari/viewer/assemble.py via xa
|
||||
|
||||
* = $8000
|
||||
|
||||
S = $80
|
||||
D = $82
|
||||
|
||||
entry:
|
||||
; ---- copy the viewer stub to $2000 ----
|
||||
lda #<stubsrc
|
||||
sta S
|
||||
lda #>stubsrc
|
||||
sta S+1
|
||||
lda #$00
|
||||
sta D
|
||||
lda #$20
|
||||
sta D+1
|
||||
ldx #STUB_PAGES
|
||||
ldy #$00
|
||||
sc:
|
||||
lda (S),y
|
||||
sta (D),y
|
||||
iny
|
||||
bne sc
|
||||
inc S+1
|
||||
inc D+1
|
||||
dex
|
||||
bne sc
|
||||
|
||||
; ---- copy the picture data to $4000 ----
|
||||
lda #<datasrc
|
||||
sta S
|
||||
lda #>datasrc
|
||||
sta S+1
|
||||
lda #$00
|
||||
sta D
|
||||
lda #$40
|
||||
sta D+1
|
||||
ldx #DATA_PAGES
|
||||
ldy #$00
|
||||
dc:
|
||||
lda (S),y
|
||||
sta (D),y
|
||||
iny
|
||||
bne dc
|
||||
inc S+1
|
||||
inc D+1
|
||||
dex
|
||||
bne dc
|
||||
|
||||
jmp $2006 ; enter the disk viewer (cont)
|
||||
|
||||
stubsrc:
|
||||
; viewer stub + picture data appended here by the packager
|
||||
datasrc = stubsrc + STUB_LEN
|
||||
50
lenser/atari/viewer/gr15.s
Normal file
50
lenser/atari/viewer/gr15.s
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
; lenser -- Atari GR.15 (ANTIC mode E) viewer, self-booting
|
||||
;
|
||||
; 160x192, 4 colours chosen globally (no per-cell limit). Boots from sector 1:
|
||||
; the OS loads this blob to $2000 and JSRs $2006. Appended data (from $4000):
|
||||
; $4000 bitmap lines 0-101 (4080 bytes)
|
||||
; $5000 bitmap lines 102-191 (3600 bytes) [split at the 4K ANTIC boundary]
|
||||
; $6000 4 colour register values (value 0..3)
|
||||
;
|
||||
; assembled by atari/viewer/assemble.py via xa
|
||||
|
||||
* = $2000
|
||||
boot:
|
||||
.byte 0 ; flags
|
||||
.byte 0 ; sector count (patched by the ATR writer)
|
||||
.word $2000 ; load address
|
||||
.word binit ; init address (DOSINI)
|
||||
|
||||
cont: ; $2006 -- OS JSRs here after loading
|
||||
lda #$22
|
||||
sta $22f ; SDMCTL = normal playfield + DL DMA
|
||||
lda #<dlist
|
||||
sta $230 ; SDLSTL
|
||||
lda #>dlist
|
||||
sta $231 ; SDLSTH
|
||||
lda #$00
|
||||
sta $26f ; GPRIOR = 0 (no GTIA mode)
|
||||
; copy the 4 colour registers from $6000
|
||||
lda $6000
|
||||
sta $2c8 ; COLBAK (pixel value 0)
|
||||
lda $6001
|
||||
sta $2c4 ; COLPF0 (value 1)
|
||||
lda $6002
|
||||
sta $2c5 ; COLPF1 (value 2)
|
||||
lda $6003
|
||||
sta $2c6 ; COLPF2 (value 3)
|
||||
#include "awyt.i"
|
||||
|
||||
binit:
|
||||
rts
|
||||
|
||||
dlist:
|
||||
.byte $70,$70,$70 ; 24 blank scan lines
|
||||
.byte $4e ; LMS + mode E
|
||||
.word $4000
|
||||
.dsb 101,$0e ; 101 more mode E lines (102 total from $4000)
|
||||
.byte $4e ; LMS + mode E
|
||||
.word $5000
|
||||
.dsb 89,$0e ; 89 more mode E lines (90 total from $5000)
|
||||
.byte $41 ; JVB
|
||||
.word dlist
|
||||
100
lenser/atari/viewer/gr15dli.s
Normal file
100
lenser/atari/viewer/gr15dli.s
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
; lenser -- Atari GR.15 + DLI viewer, self-booting
|
||||
; 160x192, 4 colours rewritten every 2 scanlines by a display-list interrupt.
|
||||
; Appended data from $4000 ...
|
||||
; $4000/$5000 bitmap (2 bits per pixel, 4K split)
|
||||
; $6000 colour table, 96 bands x 4 register values
|
||||
; $6400 display list (DLI bit on the last line of each 2-line band)
|
||||
;
|
||||
; assembled by atari/viewer/assemble.py via xa
|
||||
|
||||
CP = $cb ; zero-page pointer into the colour table
|
||||
|
||||
* = $2000
|
||||
boot:
|
||||
.byte 0
|
||||
.byte 0 ; sector count (patched)
|
||||
.word $2000
|
||||
.word binit
|
||||
|
||||
cont:
|
||||
sei
|
||||
lda #$22
|
||||
sta $22f ; SDMCTL
|
||||
lda #$00
|
||||
sta $26f ; GPRIOR = 0
|
||||
lda #$00
|
||||
sta $230 ; SDLSTL = $6400 (DL shipped in the data)
|
||||
lda #$64
|
||||
sta $231
|
||||
; DLI vector
|
||||
lda #<dli
|
||||
sta $200
|
||||
lda #>dli
|
||||
sta $201
|
||||
; colour pointer starts at band 1 (band 0 set by the VBI)
|
||||
lda #$04
|
||||
sta CP
|
||||
lda #$60
|
||||
sta CP+1
|
||||
lda $6000
|
||||
sta $2c8
|
||||
lda $6001
|
||||
sta $2c4
|
||||
lda $6002
|
||||
sta $2c5
|
||||
lda $6003
|
||||
sta $2c6
|
||||
ldy #<vbi
|
||||
ldx #>vbi
|
||||
lda #$07
|
||||
jsr $e45c ; SETVBV (deferred)
|
||||
lda #$c0
|
||||
sta $d40e ; NMIEN = DLI + VBI
|
||||
cli
|
||||
#include "awyt.i"
|
||||
binit:
|
||||
rts
|
||||
|
||||
vbi:
|
||||
lda #$04
|
||||
sta CP
|
||||
lda #$60
|
||||
sta CP+1
|
||||
lda $6000
|
||||
sta $d01a
|
||||
lda $6001
|
||||
sta $d016
|
||||
lda $6002
|
||||
sta $d017
|
||||
lda $6003
|
||||
sta $d018
|
||||
jmp $e462 ; XITVBV
|
||||
|
||||
dli:
|
||||
pha
|
||||
tya
|
||||
pha
|
||||
ldy #$00
|
||||
sta $d40a ; WSYNC
|
||||
lda (CP),y
|
||||
sta $d01a
|
||||
iny
|
||||
lda (CP),y
|
||||
sta $d016
|
||||
iny
|
||||
lda (CP),y
|
||||
sta $d017
|
||||
iny
|
||||
lda (CP),y
|
||||
sta $d018
|
||||
lda CP
|
||||
clc
|
||||
adc #$04
|
||||
sta CP
|
||||
bcc nocarry
|
||||
inc CP+1
|
||||
nocarry:
|
||||
pla
|
||||
tay
|
||||
pla
|
||||
rti
|
||||
42
lenser/atari/viewer/gr8.s
Normal file
42
lenser/atari/viewer/gr8.s
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
; lenser -- Atari GR.8 (ANTIC mode F) hi-res viewer, self-booting
|
||||
; 320x192, two tones. Appended data from $4000 ...
|
||||
; $4000/$5000 bitmap (1 bit per pixel, 4K split)
|
||||
; $6000 two bytes, background reg then foreground reg
|
||||
;
|
||||
; assembled by atari/viewer/assemble.py via xa
|
||||
|
||||
* = $2000
|
||||
boot:
|
||||
.byte 0
|
||||
.byte 0 ; sector count (patched)
|
||||
.word $2000
|
||||
.word binit
|
||||
|
||||
cont:
|
||||
lda #$22
|
||||
sta $22f ; SDMCTL
|
||||
lda #<dlist
|
||||
sta $230
|
||||
lda #>dlist
|
||||
sta $231
|
||||
lda #$00
|
||||
sta $26f ; GPRIOR = 0
|
||||
lda $6000
|
||||
sta $2c6 ; COLPF2 (background)
|
||||
sta $2c8 ; COLBAK (border) = background
|
||||
lda $6001
|
||||
sta $2c5 ; COLPF1 (foreground luminance)
|
||||
#include "awyt.i"
|
||||
binit:
|
||||
rts
|
||||
|
||||
dlist:
|
||||
.byte $70,$70,$70
|
||||
.byte $4f ; LMS + mode F
|
||||
.word $4000
|
||||
.dsb 101,$0f
|
||||
.byte $4f
|
||||
.word $5000
|
||||
.dsb 89,$0f
|
||||
.byte $41
|
||||
.word dlist
|
||||
39
lenser/atari/viewer/gr9.s
Normal file
39
lenser/atari/viewer/gr9.s
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
; lenser -- Atari GR.9 (GTIA 16-luminance) viewer, self-booting
|
||||
; 80x192, 16 shades of one hue. Appended data from $4000 ...
|
||||
; $4000/$5000 bitmap (4 bits per pixel, 4K split)
|
||||
; $6000 one byte, COLBAK = hue times 16
|
||||
;
|
||||
; assembled by atari/viewer/assemble.py via xa
|
||||
|
||||
* = $2000
|
||||
boot:
|
||||
.byte 0
|
||||
.byte 0 ; sector count (patched)
|
||||
.word $2000
|
||||
.word binit
|
||||
|
||||
cont:
|
||||
lda #$22
|
||||
sta $22f ; SDMCTL
|
||||
lda #<dlist
|
||||
sta $230
|
||||
lda #>dlist
|
||||
sta $231
|
||||
lda #$40
|
||||
sta $26f ; GPRIOR = GTIA mode 1 (GR.9)
|
||||
lda $6000
|
||||
sta $2c8 ; COLBAK sets the hue
|
||||
#include "awyt.i"
|
||||
binit:
|
||||
rts
|
||||
|
||||
dlist:
|
||||
.byte $70,$70,$70
|
||||
.byte $4f ; LMS + mode F
|
||||
.word $4000
|
||||
.dsb 101,$0f
|
||||
.byte $4f
|
||||
.word $5000
|
||||
.dsb 89,$0f
|
||||
.byte $41
|
||||
.word dlist
|
||||
248
lenser/atari/viewer/slideshow_dli.s
Normal file
248
lenser/atari/viewer/slideshow_dli.s
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
; lenser -- Atari GR.15 + DLI slideshow viewer (per-line colour).
|
||||
;
|
||||
; Self-booting (OS loads this to $2000, JSRs $2006). Steps through NIMAGES
|
||||
; pictures stored as raw sectors -- image i at sectors BASESEC + i*SPI -- SIO-
|
||||
; reading SPI sectors into $4000 (bitmap $4000/$5000, colour table $6000 of
|
||||
; 96 bands x 4, display list $6400) and showing each with a display-list
|
||||
; interrupt that rewrites the four colour registers every two scanlines.
|
||||
;
|
||||
; build-time #defines -- WAITMODE WAITSECS RATE NIMAGES LOOPFLAG BASESEC SPI.
|
||||
|
||||
CP = $cb ; zero-page pointer into the colour table
|
||||
|
||||
* = $2000
|
||||
boot:
|
||||
.byte 0 ; flags
|
||||
.byte 0 ; sector count (patched)
|
||||
.word $2000
|
||||
.word binit
|
||||
|
||||
cont: ; $2006 -- OS JSRs here after loading the stub
|
||||
sei
|
||||
lda #$00
|
||||
sta $26f ; GPRIOR = 0
|
||||
lda #$00
|
||||
sta $230 ; SDLSTL = $6400 (the loaded display list)
|
||||
lda #$64
|
||||
sta $231
|
||||
lda #<dli
|
||||
sta $200
|
||||
lda #>dli
|
||||
sta $201
|
||||
ldy #<vbi
|
||||
ldx #>vbi
|
||||
lda #$07
|
||||
jsr $e45c ; SETVBV (deferred VBI)
|
||||
lda #$00
|
||||
sta ssidx
|
||||
cli
|
||||
|
||||
ssmain:
|
||||
lda #$00
|
||||
sta $d40e ; NMIEN off (no DLI/VBI while loading)
|
||||
sta $22f ; SDMCTL off (blank)
|
||||
jsr readimg ; SIO-load image ssidx (IRQ stays on for SIOV)
|
||||
; re-init the colour pointer + band-0 colours for the new image
|
||||
lda #$04
|
||||
sta CP
|
||||
lda #$60
|
||||
sta CP+1
|
||||
lda $6000
|
||||
sta $2c8
|
||||
lda $6001
|
||||
sta $2c4
|
||||
lda $6002
|
||||
sta $2c5
|
||||
lda $6003
|
||||
sta $2c6
|
||||
lda #$c0
|
||||
sta $d40e ; NMIEN = DLI + VBI
|
||||
lda #$22
|
||||
sta $22f ; SDMCTL = playfield + DL DMA
|
||||
jsr sswait
|
||||
inc ssidx
|
||||
lda ssidx
|
||||
cmp #NIMAGES
|
||||
bcc ssmain
|
||||
#if LOOPFLAG == 1
|
||||
lda #$00
|
||||
sta ssidx
|
||||
jmp ssmain
|
||||
#else
|
||||
sei
|
||||
lda #$00
|
||||
sta $d40e ; NMIEN off
|
||||
lda #$40
|
||||
sta $d40e ; restore VBI only (OS housekeeping)
|
||||
lda #$00
|
||||
sta $09
|
||||
cli
|
||||
jmp $e474 ; warm-start (exit)
|
||||
#endif
|
||||
|
||||
binit:
|
||||
rts
|
||||
|
||||
; ---- SIO read SPI sectors of image ssidx into $4000 ----
|
||||
readimg:
|
||||
lda #<BASESEC
|
||||
sta secn
|
||||
lda #>BASESEC
|
||||
sta secn+1
|
||||
ldx ssidx
|
||||
beq rsbuf
|
||||
radd:
|
||||
clc
|
||||
lda secn
|
||||
adc #SPI
|
||||
sta secn
|
||||
bcc ra1
|
||||
inc secn+1
|
||||
ra1:
|
||||
dex
|
||||
bne radd
|
||||
rsbuf:
|
||||
lda #$00
|
||||
sta $0304
|
||||
lda #$40
|
||||
sta $0305
|
||||
lda #SPI
|
||||
sta cnt
|
||||
rloop:
|
||||
lda #$31
|
||||
sta $0300
|
||||
lda #$01
|
||||
sta $0301
|
||||
lda #$52
|
||||
sta $0302
|
||||
lda #$40
|
||||
sta $0303
|
||||
lda #$1f
|
||||
sta $0306
|
||||
lda #$80
|
||||
sta $0308
|
||||
lda #$00
|
||||
sta $0309
|
||||
lda secn
|
||||
sta $030a
|
||||
lda secn+1
|
||||
sta $030b
|
||||
jsr $e459 ; SIOV
|
||||
clc
|
||||
lda $0304
|
||||
adc #$80
|
||||
sta $0304
|
||||
bcc rb1
|
||||
inc $0305
|
||||
rb1:
|
||||
inc secn
|
||||
bne rb2
|
||||
inc secn+1
|
||||
rb2:
|
||||
dec cnt
|
||||
bne rloop
|
||||
rts
|
||||
|
||||
; ---- wait (returns; defeats attract mode via $4d) ----
|
||||
sswait:
|
||||
#if WAITMODE == 1
|
||||
lda #$ff
|
||||
sta $2fc
|
||||
sw1:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $2fc
|
||||
cmp #$ff
|
||||
beq sw1
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 2
|
||||
lda #$00
|
||||
sta $12
|
||||
sta $13
|
||||
sta $14
|
||||
sw2:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $13
|
||||
cmp #>(WAITSECS*RATE)
|
||||
bcc sw2
|
||||
bne sw2d
|
||||
lda $14
|
||||
cmp #<(WAITSECS*RATE)
|
||||
bcc sw2
|
||||
sw2d:
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 3
|
||||
lda #$ff
|
||||
sta $2fc
|
||||
lda #$00
|
||||
sta $12
|
||||
sta $13
|
||||
sta $14
|
||||
sw3:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $2fc
|
||||
cmp #$ff
|
||||
bne sw3d
|
||||
lda $13
|
||||
cmp #>(WAITSECS*RATE)
|
||||
bcc sw3
|
||||
bne sw3d
|
||||
lda $14
|
||||
cmp #<(WAITSECS*RATE)
|
||||
bcc sw3
|
||||
sw3d:
|
||||
rts
|
||||
#endif
|
||||
|
||||
ssidx: .byte 0
|
||||
secn: .word 0
|
||||
cnt: .byte 0
|
||||
|
||||
; reset colour pointer + band-0 colours each frame (from the loaded $6000 table)
|
||||
vbi:
|
||||
lda #$04
|
||||
sta CP
|
||||
lda #$60
|
||||
sta CP+1
|
||||
lda $6000
|
||||
sta $d01a
|
||||
lda $6001
|
||||
sta $d016
|
||||
lda $6002
|
||||
sta $d017
|
||||
lda $6003
|
||||
sta $d018
|
||||
jmp $e462 ; XITVBV
|
||||
|
||||
dli:
|
||||
pha
|
||||
tya
|
||||
pha
|
||||
ldy #$00
|
||||
sta $d40a ; WSYNC
|
||||
lda (CP),y
|
||||
sta $d01a
|
||||
iny
|
||||
lda (CP),y
|
||||
sta $d016
|
||||
iny
|
||||
lda (CP),y
|
||||
sta $d017
|
||||
iny
|
||||
lda (CP),y
|
||||
sta $d018
|
||||
lda CP
|
||||
clc
|
||||
adc #$04
|
||||
sta CP
|
||||
bcc nocarry
|
||||
inc CP+1
|
||||
nocarry:
|
||||
pla
|
||||
tay
|
||||
pla
|
||||
rti
|
||||
272
lenser/atari/viewer/slideshow_static.s
Normal file
272
lenser/atari/viewer/slideshow_static.s
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
; lenser -- Atari static-playfield slideshow viewer (GR.15 / GR.9 / GR.8).
|
||||
;
|
||||
; Self-booting (OS loads this to $2000, JSRs $2006). Steps through NIMAGES
|
||||
; pictures stored as raw sectors -- image i at sectors BASESEC + i*SPI -- SIO-
|
||||
; reading SPI sectors and showing each before advancing (key / seconds / both).
|
||||
;
|
||||
; DOUBLE BUFFERED so the previous slide stays on screen while the next loads (no
|
||||
; blank between slides). Two RAM buffers alternate as front/back --
|
||||
; buffer 0 -- bitmap $4000/$5000, colour bytes $6000
|
||||
; buffer 1 -- bitmap $7000/$8000, colour bytes $9000
|
||||
; both kept below $A000 so they are RAM even when the BASIC ROM is enabled. The
|
||||
; display list's two LMS addresses point at the front buffer; each slide is SIO-
|
||||
; read into the *back* buffer while the front (the previous picture) keeps
|
||||
; displaying, then -- during a vertical blank so the change isn't torn -- the LMS
|
||||
; addresses and colour registers are switched to it. ss_hi = ssbuf*$30 is the
|
||||
; high-byte offset ($00 buffer 0, $30 buffer 1) added to every buffer address.
|
||||
;
|
||||
; The mode is fixed for the whole slideshow, chosen by build-time #defines --
|
||||
; DLMODE ANTIC mode byte ($0e GR.15, $0f GR.9/GR.8)
|
||||
; GPRIOR GTIA priority/mode ($00, or $40 for GR.9)
|
||||
; COLORMODE colour-register layout (0 GR.15 / 1 GR.9 / 2 GR.8)
|
||||
; plus WAITMODE/WAITSECS/RATE, NIMAGES, LOOPFLAG, BASESEC, SPI (see assemble.py).
|
||||
|
||||
colptr = $cb ; zero-page pointer to the back buffer's colours
|
||||
|
||||
* = $2000
|
||||
boot:
|
||||
.byte 0 ; flags
|
||||
.byte 0 ; sector count (patched by the ATR writer)
|
||||
.word $2000 ; load address
|
||||
.word binit ; init address (DOSINI)
|
||||
|
||||
cont: ; $2006 -- OS JSRs here after loading the stub
|
||||
lda #<dlist
|
||||
sta $230 ; SDLSTL
|
||||
lda #>dlist
|
||||
sta $231 ; SDLSTH
|
||||
lda #GPRIOR
|
||||
sta $26f ; GPRIOR
|
||||
lda #$00
|
||||
sta $22f ; SDMCTL = 0 (blank before the first image only)
|
||||
sta ssidx
|
||||
sta ssbuf ; first slide loads into buffer 0
|
||||
|
||||
ssmain:
|
||||
jsr sethi ; ss_hi = ssbuf * $30
|
||||
jsr readimg ; SIO-load image ssidx into the back buffer
|
||||
|
||||
; ---- wait for a vertical blank, then flip to the back buffer ----
|
||||
; (the front buffer -- the previous slide -- has stayed on screen)
|
||||
lda $14
|
||||
vbwait:
|
||||
cmp $14
|
||||
beq vbwait ; RTCLOK ticked -> we are in the vertical blank
|
||||
|
||||
; display-list LMS -- region 1 = base, region 2 = base + $1000
|
||||
lda #$00
|
||||
sta lms1
|
||||
sta lms2
|
||||
lda #$40
|
||||
clc
|
||||
adc ss_hi
|
||||
sta lms1+1 ; $40 / $70
|
||||
lda #$50
|
||||
clc
|
||||
adc ss_hi
|
||||
sta lms2+1 ; $50 / $80
|
||||
|
||||
; colour bytes are at base + $2000
|
||||
lda #$00
|
||||
sta colptr
|
||||
lda #$60
|
||||
clc
|
||||
adc ss_hi
|
||||
sta colptr+1 ; $60 / $90
|
||||
#if COLORMODE == 0
|
||||
ldy #$00
|
||||
lda (colptr),y
|
||||
sta $2c8 ; COLBAK (value 0)
|
||||
ldy #$01
|
||||
lda (colptr),y
|
||||
sta $2c4 ; COLPF0 (value 1)
|
||||
ldy #$02
|
||||
lda (colptr),y
|
||||
sta $2c5 ; COLPF1 (value 2)
|
||||
ldy #$03
|
||||
lda (colptr),y
|
||||
sta $2c6 ; COLPF2 (value 3)
|
||||
#endif
|
||||
#if COLORMODE == 1
|
||||
ldy #$00
|
||||
lda (colptr),y
|
||||
sta $2c8 ; COLBAK = hue (GR.9)
|
||||
#endif
|
||||
#if COLORMODE == 2
|
||||
ldy #$00
|
||||
lda (colptr),y
|
||||
sta $2c6 ; COLPF2 background
|
||||
sta $2c8 ; COLBAK border = background
|
||||
ldy #$01
|
||||
lda (colptr),y
|
||||
sta $2c5 ; COLPF1 foreground
|
||||
#endif
|
||||
lda #$22
|
||||
sta $22f ; SDMCTL on (a no-op after the first slide)
|
||||
|
||||
jsr sswait
|
||||
lda ssbuf
|
||||
eor #$01
|
||||
sta ssbuf ; next slide loads into the other buffer
|
||||
inc ssidx
|
||||
lda ssidx
|
||||
cmp #NIMAGES
|
||||
bcc ssmain
|
||||
#if LOOPFLAG == 1
|
||||
lda #$00
|
||||
sta ssidx
|
||||
jmp ssmain
|
||||
#else
|
||||
lda #$00
|
||||
sta $09 ; clear BOOT? so warmstart enters BASIC
|
||||
jmp $e474 ; warm-start (exit)
|
||||
#endif
|
||||
|
||||
binit:
|
||||
rts
|
||||
|
||||
; ss_hi = ssbuf * $30 (back-buffer high-byte offset -- $00 buffer 0, $30 buffer 1)
|
||||
sethi:
|
||||
ldx #$00
|
||||
lda ssbuf
|
||||
beq sh0
|
||||
ldx #$30
|
||||
sh0:
|
||||
stx ss_hi
|
||||
rts
|
||||
|
||||
; ---- SIO read SPI sectors of image ssidx into the back buffer (base+$0000) ----
|
||||
readimg:
|
||||
lda #<BASESEC
|
||||
sta secn
|
||||
lda #>BASESEC
|
||||
sta secn+1
|
||||
ldx ssidx
|
||||
beq rsbuf
|
||||
radd:
|
||||
clc
|
||||
lda secn
|
||||
adc #SPI
|
||||
sta secn
|
||||
bcc ra1
|
||||
inc secn+1
|
||||
ra1:
|
||||
dex
|
||||
bne radd
|
||||
rsbuf:
|
||||
lda #$00
|
||||
sta $0304 ; DBUFLO = $00
|
||||
lda #$40
|
||||
clc
|
||||
adc ss_hi
|
||||
sta $0305 ; DBUFHI = $40 / $70 (back buffer base)
|
||||
lda #SPI
|
||||
sta cnt
|
||||
rloop:
|
||||
lda #$31
|
||||
sta $0300 ; DDEVIC = disk
|
||||
lda #$01
|
||||
sta $0301 ; DUNIT 1
|
||||
lda #$52
|
||||
sta $0302 ; DCOMND R (read)
|
||||
lda #$40
|
||||
sta $0303 ; DSTATS = read direction
|
||||
lda #$1f
|
||||
sta $0306 ; DTIMLO
|
||||
lda #$80
|
||||
sta $0308 ; DBYTLO = 128
|
||||
lda #$00
|
||||
sta $0309 ; DBYTHI
|
||||
lda secn
|
||||
sta $030a ; DAUX1 sector low
|
||||
lda secn+1
|
||||
sta $030b ; DAUX2 sector high
|
||||
jsr $e459 ; SIOV
|
||||
clc
|
||||
lda $0304
|
||||
adc #$80
|
||||
sta $0304 ; buffer += 128
|
||||
bcc rb1
|
||||
inc $0305
|
||||
rb1:
|
||||
inc secn
|
||||
bne rb2
|
||||
inc secn+1
|
||||
rb2:
|
||||
dec cnt
|
||||
bne rloop
|
||||
rts
|
||||
|
||||
; ---- wait (returns; defeats attract mode via $4d) ----
|
||||
sswait:
|
||||
#if WAITMODE == 1
|
||||
lda #$ff
|
||||
sta $2fc ; clear CH
|
||||
sw1:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $2fc
|
||||
cmp #$ff
|
||||
beq sw1
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 2
|
||||
lda #$00
|
||||
sta $12
|
||||
sta $13
|
||||
sta $14 ; reset RTCLOK
|
||||
sw2:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $13
|
||||
cmp #>(WAITSECS*RATE)
|
||||
bcc sw2
|
||||
bne sw2d
|
||||
lda $14
|
||||
cmp #<(WAITSECS*RATE)
|
||||
bcc sw2
|
||||
sw2d:
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 3
|
||||
lda #$ff
|
||||
sta $2fc
|
||||
lda #$00
|
||||
sta $12
|
||||
sta $13
|
||||
sta $14
|
||||
sw3:
|
||||
lda #$00
|
||||
sta $4d
|
||||
lda $2fc
|
||||
cmp #$ff
|
||||
bne sw3d ; any key ends the slide
|
||||
lda $13
|
||||
cmp #>(WAITSECS*RATE)
|
||||
bcc sw3
|
||||
bne sw3d
|
||||
lda $14
|
||||
cmp #<(WAITSECS*RATE)
|
||||
bcc sw3
|
||||
sw3d:
|
||||
rts
|
||||
#endif
|
||||
|
||||
ssidx: .byte 0
|
||||
ssbuf: .byte 0 ; 0 or 1 -- which buffer the next slide loads into
|
||||
ss_hi: .byte 0 ; ssbuf * $30 (back-buffer high-byte offset)
|
||||
secn: .word 0
|
||||
cnt: .byte 0
|
||||
|
||||
dlist:
|
||||
.byte $70,$70,$70 ; 24 blank scan lines
|
||||
.byte DLMODE+$40 ; LMS + mode
|
||||
lms1:
|
||||
.word $4000 ; region 1 base (patched to the front buffer)
|
||||
.dsb 101,DLMODE
|
||||
.byte DLMODE+$40 ; LMS + mode
|
||||
lms2:
|
||||
.word $5000 ; region 2 base (patched to the front buffer)
|
||||
.dsb 89,DLMODE
|
||||
.byte $41 ; JVB
|
||||
.word dlist
|
||||
Loading…
Add table
Add a link
Reference in a new issue