First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
0
lenser/amiga/__init__.py
Normal file
0
lenser/amiga/__init__.py
Normal file
23
lenser/amiga/convert/__init__.py
Normal file
23
lenser/amiga/convert/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""Commodore Amiga conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import imageprep
|
||||
from . import lowres, mono
|
||||
|
||||
# NOTE: HAM6 (4096 colours) was implemented and looks superb, but MAME's
|
||||
# preliminary Amiga can't render 6-bitplane/HAM modes cleanly (fixed-position
|
||||
# black bands at the screen edges), so only the MAME-verified 5-plane low-res
|
||||
# (32 colours) and mono modes are shipped.
|
||||
_MODULES = {"lowres": lowres, "mono": mono}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="lowres", palette_name="amiga",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None,
|
||||
base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES.get(mode, lowres)
|
||||
img_rgb = imageprep.prepare(path_or_img, module.WIDTH, module.HEIGHT,
|
||||
module.PIXEL_ASPECT, prep_opt, border_rgb=(0, 0, 0))
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
130
lenser/amiga/convert/_common.py
Normal file
130
lenser/amiga/convert/_common.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Amiga encoders: HAM6 (4096 colours), flat low-res (<=32 of 4096), greyscale.
|
||||
|
||||
HAM (Hold-And-Modify) is the showpiece: 6 bitplanes where the top 2 bits choose
|
||||
whether a pixel is one of 16 base palette colours or modifies one R/G/B channel
|
||||
of the pixel to its left -- giving up to 4096 colours on screen. We pick 16 base
|
||||
colours, then walk each scanline left-to-right choosing per pixel the option
|
||||
(set / modify R / modify G / modify B) closest to the target.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import _box_blur
|
||||
from .. import palette as apal
|
||||
|
||||
W, H = 320, 200
|
||||
|
||||
|
||||
def _perr(final_rgb, img_rgb):
|
||||
a = _box_blur(c64pal.srgb_to_lab(final_rgb.astype(np.float64)))
|
||||
b = _box_blur(c64pal.srgb_to_lab(img_rgb.astype(np.float64)))
|
||||
return float(np.sqrt(((a - b) ** 2).sum(-1)).mean())
|
||||
|
||||
|
||||
def planar_split(codes, nplanes):
|
||||
"""codes (H,W) -> nplanes contiguous bitplanes (40 bytes/line, MSB left)."""
|
||||
out = bytearray()
|
||||
for p in range(nplanes):
|
||||
bit = ((codes >> p) & 1).astype(np.uint8)
|
||||
out += np.packbits(bit, axis=1).reshape(-1).tobytes() # (H,40)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _kmeans_keys(img_lab, plab, k, iters=12):
|
||||
rng = np.random.default_rng(0)
|
||||
flat = img_lab.reshape(-1, 3)
|
||||
pts = flat[rng.choice(len(flat), min(6000, len(flat)), replace=False)]
|
||||
# k-means++ style seeding so every centroid is a real, distinct colour
|
||||
cen = pts[rng.integers(len(pts))][None].copy()
|
||||
for _ in range(k - 1):
|
||||
d = np.min(((pts[:, None, :] - cen[None]) ** 2).sum(2), 1)
|
||||
cen = np.vstack([cen, pts[int(d.argmax())]]) # farthest-point seed
|
||||
for _ in range(iters):
|
||||
lab = np.argmin(((pts[:, None, :] - cen[None]) ** 2).sum(2), 1)
|
||||
for j in range(k):
|
||||
m = pts[lab == j]
|
||||
cen[j] = m.mean(0) if len(m) else pts[rng.integers(len(pts))]
|
||||
# snap each centroid to the nearest 4096 palette key
|
||||
keys = [int(np.argmin(((plab - c) ** 2).sum(1))) for c in cen]
|
||||
return keys
|
||||
|
||||
|
||||
def ham_encode(img_rgb, dither_mode):
|
||||
plab = apal.palette_lab() # (4096,3)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
base_keys = _kmeans_keys(img_lab, plab, 16)
|
||||
base_lab = plab[base_keys]
|
||||
base_rgb4 = [(k >> 8 & 15, k >> 4 & 15, k & 15) for k in base_keys]
|
||||
|
||||
t4 = np.clip(np.rint(img_rgb / 17.0), 0, 15).astype(np.int64) # 4-bit targets
|
||||
# base option (independent of the held colour) precomputed for all pixels
|
||||
bd = ((img_lab[:, :, None, :] - base_lab[None, None]) ** 2).sum(3) # (H,W,16)
|
||||
base_best = bd.argmin(2)
|
||||
base_cost = bd.min(2)
|
||||
|
||||
codes = np.zeros((H, W), np.uint8)
|
||||
final = np.zeros((H, W, 3), np.uint8)
|
||||
P = plab
|
||||
for y in range(H):
|
||||
tl = img_lab[y]; t4y = t4[y]
|
||||
bb = base_best[y]; bc = base_cost[y]
|
||||
pr = pg = pb = 0 # hardware holds black at line start
|
||||
for x in range(W):
|
||||
t0, t1, t2 = tl[x, 0], tl[x, 1], tl[x, 2]
|
||||
bi = int(bb[x]); best = bc[x]
|
||||
ctrl = 0; data = bi; nr, ng, nb = base_rgb4[bi]
|
||||
# force an absolute "set" on the first pixel so the line establishes a
|
||||
# colour regardless of the held-colour start (an all-modify run from
|
||||
# the wrong start would otherwise stay dark -- HAM left-edge bug).
|
||||
if x > 0:
|
||||
tr, tg, tb = int(t4y[x, 0]), int(t4y[x, 1]), int(t4y[x, 2])
|
||||
for c_ctrl, c_data, kr, kg, kb in (
|
||||
(2, tr, tr, pg, pb), (3, tg, pr, tg, pb), (1, tb, pr, pg, tb)):
|
||||
pk = P[(kr << 8) | (kg << 4) | kb]
|
||||
dr = pk[0] - t0; dg = pk[1] - t1; db = pk[2] - t2
|
||||
cc = dr * dr + dg * dg + db * db
|
||||
if cc < best:
|
||||
best = cc; ctrl = c_ctrl; data = c_data; nr, ng, nb = kr, kg, kb
|
||||
codes[y, x] = (ctrl << 4) | data
|
||||
pr, pg, pb = nr, ng, nb
|
||||
final[y, x, 0] = pr * 17; final[y, x, 1] = pg * 17; final[y, x, 2] = pb * 17
|
||||
|
||||
planes = planar_split(codes, 6)
|
||||
colors = [apal.color_word(k) for k in base_keys] # 16 base registers
|
||||
return planes, colors, final, _perr(final, img_rgb)
|
||||
|
||||
|
||||
def flat_encode(img_rgb, n_colors, dither_mode, mono=False, base_color=None):
|
||||
plab = apal.palette_lab()
|
||||
prgb = apal.get_palette().astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
nplanes = (n_colors - 1).bit_length() # 32->5, 16->4
|
||||
|
||||
if mono:
|
||||
keys = list(apal.GREYS) # 16 greys
|
||||
if base_color in range(4096):
|
||||
keys = sorted({keys[0], int(base_color), keys[-1]}, key=lambda i: plab[i, 0])
|
||||
keys = (keys + keys)[:n_colors]
|
||||
work = np.zeros_like(img_lab); work[..., 0] = img_lab[..., 0]
|
||||
pw = np.zeros_like(plab); pw[:, 0] = plab[:, 0]
|
||||
else:
|
||||
keys = _kmeans_keys(img_lab, plab, n_colors)
|
||||
work, pw = img_lab, plab
|
||||
|
||||
allowed = np.tile(np.array(keys[:n_colors]), (H, W, 1))
|
||||
qidx = dither.quantize(work, allowed, pw, dither_mode).astype(np.int64)
|
||||
# map palette key -> pen index
|
||||
lut = {k: i for i, k in enumerate(keys[:n_colors])}
|
||||
pen = np.vectorize(lut.get)(qidx).astype(np.uint8)
|
||||
|
||||
planes = planar_split(pen, nplanes)
|
||||
colors = [apal.color_word(k) for k in keys[:n_colors]]
|
||||
final = prgb[qidx]
|
||||
if mono: # measure greyscale against luminance
|
||||
g = img_rgb.mean(2, keepdims=True).repeat(3, 2)
|
||||
err = _perr(final, g.astype(np.uint8))
|
||||
else:
|
||||
err = _perr(final, img_rgb)
|
||||
return planes, colors, final, err
|
||||
21
lenser/amiga/convert/lowres.py
Normal file
21
lenser/amiga/convert/lowres.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Amiga low resolution: 320x200, 32 colours from the 4096-colour palette."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...convert.base import Conversion
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 320, 200
|
||||
PIXEL_ASPECT = 1.0
|
||||
NCOL = 32
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="amiga", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
planes, colors, preview, err = _common.flat_encode(img_rgb, NCOL, dither_mode)
|
||||
return Conversion(
|
||||
mode="lowres", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=None,
|
||||
data={"planes": planes, "colors": colors, "nplanes": 5, "ham": False},
|
||||
data_addr=0, viewer="amiga", preview_rgb=preview, error=err,
|
||||
meta={"palette": "amiga", "dither": dither_mode},
|
||||
)
|
||||
22
lenser/amiga/convert/mono.py
Normal file
22
lenser/amiga/convert/mono.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""Amiga monochrome: 320x200, 16 grey levels (the Amiga has true 16 greys)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...convert.base import Conversion
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 320, 200
|
||||
PIXEL_ASPECT = 1.0
|
||||
NCOL = 16
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="amiga", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
planes, colors, preview, err = _common.flat_encode(
|
||||
img_rgb, NCOL, dither_mode, mono=True, base_color=base_color)
|
||||
return Conversion(
|
||||
mode="mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=None,
|
||||
data={"planes": planes, "colors": colors, "nplanes": 4, "ham": False},
|
||||
data_addr=0, viewer="amiga", preview_rgb=preview, error=err,
|
||||
meta={"palette": "amiga", "dither": dither_mode},
|
||||
)
|
||||
41
lenser/amiga/copper.py
Normal file
41
lenser/amiga/copper.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"""Build an Amiga Copper list that displays a static 320x200 low-res screen.
|
||||
|
||||
The Copper runs every frame and re-loads the bitplane pointers + registers, so
|
||||
the picture stays stable. Each instruction is a MOVE (register offset, value);
|
||||
the list ends with WAIT $FFFF,$FFFE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
DIWSTRT, DIWSTOP, DDFSTRT, DDFSTOP = 0x08E, 0x090, 0x092, 0x094
|
||||
BPLCON0, BPLCON1, BPLCON2 = 0x100, 0x102, 0x104
|
||||
BPL1MOD, BPL2MOD = 0x108, 0x10A
|
||||
BPLPT = 0x0E0 # BPL1PTH; +4 per plane
|
||||
COLOR0 = 0x180
|
||||
|
||||
|
||||
def list_len(nplanes, ncolors) -> int:
|
||||
pairs = 9 + nplanes * 2 + ncolors # DIW*2, DDF*2, BPLCON0/1/2, BPLMOD*2 = 9
|
||||
return pairs * 4 + 4 # + end WAIT
|
||||
|
||||
|
||||
def build(nplanes, ham, plane_addrs, colors) -> bytes:
|
||||
bplcon0 = (nplanes << 12) | (0x0800 if ham else 0) | 0x0200
|
||||
moves = [
|
||||
(DIWSTRT, 0x2C81), (DIWSTOP, 0xF4C1),
|
||||
(DDFSTRT, 0x0038), (DDFSTOP, 0x00D0),
|
||||
(BPLCON0, bplcon0), (BPLCON1, 0x0000), (BPLCON2, 0x0024),
|
||||
(BPL1MOD, 0x0000), (BPL2MOD, 0x0000),
|
||||
]
|
||||
for p, addr in enumerate(plane_addrs):
|
||||
moves.append((BPLPT + p * 4, (addr >> 16) & 0xFFFF))
|
||||
moves.append((BPLPT + p * 4 + 2, addr & 0xFFFF))
|
||||
for i, c in enumerate(colors):
|
||||
moves.append((COLOR0 + i * 2, c & 0x0FFF))
|
||||
|
||||
out = bytearray()
|
||||
for reg, val in moves:
|
||||
out += struct.pack(">HH", reg & 0x1FE, val & 0xFFFF)
|
||||
out += struct.pack(">HH", 0xFFFF, 0xFFFE) # end of copper list
|
||||
return bytes(out)
|
||||
13
lenser/amiga/exporter.py
Normal file
13
lenser/amiga/exporter.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Build a bootable Amiga .adf floppy from a conversion."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import viewer
|
||||
|
||||
|
||||
def export_adf(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="ntsc"):
|
||||
if not output_path.lower().endswith(".adf"):
|
||||
output_path += ".adf"
|
||||
d = conv.data
|
||||
adf = viewer.build_adf(d["planes"], d["colors"], d["nplanes"], d["ham"])
|
||||
return viewer.write_adf(adf, output_path)
|
||||
30
lenser/amiga/palette.py
Normal file
30
lenser/amiga/palette.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""Commodore Amiga (OCS/ECS) colour palette.
|
||||
|
||||
12-bit colour: 4 bits per channel (x17 -> 0..255), 4096 colours. A colour
|
||||
register value is %0000 RRRR GGGG BBBB. We index the master palette by the
|
||||
packed 12-bit key (R<<8)|(G<<4)|B, which is also the register word.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab
|
||||
|
||||
_r = (np.arange(4096) >> 8) & 0xF
|
||||
_g = (np.arange(4096) >> 4) & 0xF
|
||||
_b = np.arange(4096) & 0xF
|
||||
PALETTE = (np.stack([_r, _g, _b], axis=1) * 17).astype(np.float64) # 4096 x 3
|
||||
|
||||
GREYS = [(v << 8) | (v << 4) | v for v in range(16)] # 16 grey keys
|
||||
|
||||
|
||||
def color_word(key: int) -> int:
|
||||
return key & 0x0FFF
|
||||
|
||||
|
||||
def get_palette() -> np.ndarray:
|
||||
return PALETTE
|
||||
|
||||
|
||||
def palette_lab() -> np.ndarray:
|
||||
return srgb_to_lab(PALETTE)
|
||||
242
lenser/amiga/viewer.py
Normal file
242
lenser/amiga/viewer.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
"""Build a bootable Amiga .adf that displays a low-res / HAM image.
|
||||
|
||||
The boot block (first 1024 bytes) holds a 68000 routine that Kickstart runs at
|
||||
boot: it reuses the boot trackdisk IORequest to read the Copper list + bitplanes
|
||||
from the floppy into chip RAM at $20000, points the Copper there, enables
|
||||
bitplane DMA, kills interrupts (so the OS can't reclaim the display), and idles.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
from . import copper
|
||||
|
||||
LOAD = 0x20000 # chip-RAM load address for copper list + bitplanes
|
||||
PLANE_SIZE = 40 * 200 # 8000 bytes per bitplane
|
||||
ADF_SIZE = 901120 # 880K (80*2*11*512)
|
||||
|
||||
|
||||
def _boot_code(datalen: int) -> bytes:
|
||||
"""68000 boot routine (entry: a1 = trackdisk IORequest)."""
|
||||
c = bytearray()
|
||||
c += bytes.fromhex("2C780004") # movea.l $4.w,a6 (ExecBase)
|
||||
c += bytes.fromhex("337C0002001C") # move.w #2,$1C(a1) CMD_READ
|
||||
c += b"\x23\x7C" + struct.pack(">I", datalen) + b"\x00\x24" # move.l #len,$24(a1)
|
||||
c += b"\x23\x7C" + struct.pack(">I", LOAD) + b"\x00\x28" # move.l #LOAD,$28(a1)
|
||||
c += b"\x23\x7C" + struct.pack(">I", 1024) + b"\x00\x2C" # move.l #1024,$2C(a1)
|
||||
c += bytes.fromhex("4EAEFE38") # jsr -456(a6) DoIO
|
||||
c += bytes.fromhex("4BF900DFF000") # lea $dff000,a5
|
||||
c += bytes.fromhex("3B7C7FFF009A") # move.w #$7FFF,$9A(a5) INTENA off
|
||||
c += bytes.fromhex("3B7C7FFF009C") # move.w #$7FFF,$9C(a5) INTREQ clr
|
||||
c += bytes.fromhex("3B7C7FFF0096") # move.w #$7FFF,$96(a5) DMACON off
|
||||
c += b"\x2B\x7C" + struct.pack(">I", LOAD) + b"\x00\x80" # move.l #LOAD,$80(a5) COP1LC
|
||||
c += bytes.fromhex("3B7C83800096") # move.w #$8380,$96(a5) DMACON on
|
||||
c += bytes.fromhex("3B7C00000088") # move.w #0,$88(a5) COPJMP1 strobe
|
||||
c += bytes.fromhex("60FE") # bra *
|
||||
return bytes(c)
|
||||
|
||||
|
||||
def _bootsum(block: bytes) -> int:
|
||||
s = 0
|
||||
for i in range(0, 1024, 4):
|
||||
s += int.from_bytes(block[i:i + 4], "big")
|
||||
if s > 0xFFFFFFFF:
|
||||
s = (s + 1) & 0xFFFFFFFF
|
||||
return (~s) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def build_adf(planes: bytes, colors, nplanes: int, ham: bool) -> bytes:
|
||||
clen = copper.list_len(nplanes, len(colors))
|
||||
plane_addrs = [LOAD + clen + p * PLANE_SIZE for p in range(nplanes)]
|
||||
cop = copper.build(nplanes, ham, plane_addrs, colors)
|
||||
assert len(cop) == clen
|
||||
blob = cop + planes
|
||||
datalen = (len(blob) + 511) // 512 * 512 # whole sectors for trackdisk
|
||||
|
||||
code = _boot_code(datalen)
|
||||
boot = bytearray(1024)
|
||||
boot[0:4] = b"DOS\x00"
|
||||
boot[12:12 + len(code)] = code # Kickstart jumps to offset 12
|
||||
boot[4:8] = b"\x00\x00\x00\x00"
|
||||
struct.pack_into(">I", boot, 4, _bootsum(bytes(boot)))
|
||||
|
||||
adf = bytearray(ADF_SIZE)
|
||||
adf[0:1024] = boot
|
||||
adf[1024:1024 + len(blob)] = blob # copper + bitplanes follow
|
||||
return bytes(adf)
|
||||
|
||||
|
||||
def write_adf(adf: bytes, path: str) -> str:
|
||||
with open(path, "wb") as f:
|
||||
f.write(adf)
|
||||
return path
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# slideshow
|
||||
# --------------------------------------------------------------------------- #
|
||||
SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
|
||||
|
||||
|
||||
class _M68k:
|
||||
"""Tiny 68000 emitter with label/branch fixups (hand-encoded opcodes, but the
|
||||
displacements are computed, not counted by hand)."""
|
||||
|
||||
def __init__(self):
|
||||
self.code = bytearray()
|
||||
self.labels: dict[str, int] = {}
|
||||
self.fixups: list[tuple[int, str]] = [] # (disp-word position, label)
|
||||
|
||||
def hexs(self, h): self.code.extend(bytes.fromhex(h)); return self
|
||||
def w(self, v): self.code.extend(struct.pack(">H", v & 0xFFFF)); return self
|
||||
def l(self, v): self.code.extend(struct.pack(">I", v & 0xFFFFFFFF)); return self
|
||||
def label(self, name): self.labels[name] = len(self.code); return self
|
||||
|
||||
def br(self, opc_hex, name): # Bcc.w / BSR.w / DBcc (opcode + disp16)
|
||||
self.code.extend(bytes.fromhex(opc_hex))
|
||||
self.fixups.append((len(self.code), name))
|
||||
self.w(0)
|
||||
return self
|
||||
|
||||
def resolve(self) -> bytes:
|
||||
for pos, name in self.fixups:
|
||||
disp = self.labels[name] - pos # PC base = the displacement word
|
||||
struct.pack_into(">h", self.code, pos, disp)
|
||||
return bytes(self.code)
|
||||
|
||||
|
||||
def _ss_boot_code(slot: int, n_images: int, waitmode: int, waitsecs: int,
|
||||
speed: int, loop: bool) -> bytes:
|
||||
"""68000 slideshow boot routine (entry: a1 = boot trackdisk IORequest).
|
||||
|
||||
Reads image i (slot bytes from floppy offset 1024 + i*slot) into chip RAM at
|
||||
$20000, points the Copper there and strobes it, waits, then advances.
|
||||
"""
|
||||
m = _M68k()
|
||||
m.hexs("2C780004") # movea.l $4.w,a6 (ExecBase)
|
||||
m.hexs("4BF900DFF000") # lea $dff000,a5
|
||||
# ---- read ALL images into chip RAM (interrupts still on, so DoIO works) ----
|
||||
# image i: slot bytes from floppy offset 1024+i*slot -> RAM $20000 + i*slot
|
||||
m.hexs("7E00") # moveq #0,d7 (image index)
|
||||
m.label("read")
|
||||
m.hexs("2007") # move.l d7,d0
|
||||
m.hexs("C0FC").w(slot) # mulu #slot,d0 d0 = i*slot
|
||||
m.hexs("2A00") # move.l d0,d5 save i*slot
|
||||
m.hexs("0680").l(LOAD) # add.l #$20000,d0
|
||||
m.hexs("23400028") # move.l d0,$28(a1) dest = $20000+i*slot
|
||||
m.hexs("2005") # move.l d5,d0
|
||||
m.hexs("0680").l(1024) # add.l #1024,d0
|
||||
m.hexs("2340002C") # move.l d0,$2C(a1) offset = 1024+i*slot
|
||||
m.hexs("337C0002001C") # move.w #2,$1C(a1) CMD_READ
|
||||
m.hexs("237C").l(slot).hexs("0024") # move.l #slot,$24(a1) length
|
||||
m.hexs("4EAEFE38") # jsr -456(a6) DoIO
|
||||
m.hexs("5287") # addq.l #1,d7
|
||||
m.hexs("0C87").l(n_images) # cmpi.l #n,d7
|
||||
m.br("6D00", "read") # blt.w read
|
||||
# ---- take over the display -- kill interrupts/DMA so the OS can't reclaim it
|
||||
m.hexs("3B7C7FFF009A") # INTENA off
|
||||
m.hexs("3B7C7FFF009C") # INTREQ clear
|
||||
m.hexs("3B7C7FFF0096") # DMACON off
|
||||
m.hexs("7E00") # moveq #0,d7
|
||||
m.label("main")
|
||||
m.hexs("2007") # move.l d7,d0
|
||||
m.hexs("C0FC").w(slot) # mulu #slot,d0
|
||||
m.hexs("0680").l(LOAD) # add.l #$20000,d0 COP list = $20000+i*slot
|
||||
m.hexs("2B400080") # move.l d0,$80(a5) COP1LC
|
||||
m.hexs("3B7C83800096") # move.w #$8380,$96(a5) DMACON on
|
||||
m.hexs("3B7C00000088") # move.w #0,$88(a5) COPJMP1 strobe
|
||||
m.br("6100", "wait") # bsr.w wait
|
||||
m.hexs("5287") # addq.l #1,d7
|
||||
m.hexs("0C87").l(n_images) # cmpi.l #n,d7
|
||||
m.br("6D00", "main") # blt.w main
|
||||
if loop:
|
||||
m.hexs("7E00") # moveq #0,d7
|
||||
m.br("6000", "main") # bra.w main
|
||||
else:
|
||||
m.label("idle")
|
||||
m.br("6000", "idle") # bra *
|
||||
|
||||
# ---- wait ----
|
||||
m.label("wait")
|
||||
if waitmode == 1: # key = left mouse button ($BFE001 bit6, 0=down)
|
||||
m.label("wk")
|
||||
m.hexs("123900BFE001") # move.b $bfe001,d1
|
||||
m.hexs("08010006") # btst #6,d1
|
||||
m.br("6600", "wk") # bne.w wk (loop while not pressed)
|
||||
m.hexs("4E75") # rts
|
||||
elif waitmode == 2: # seconds = delay loop
|
||||
m.hexs("243C").l(waitsecs) # move.l #secs,d2
|
||||
m.label("wso")
|
||||
m.hexs("363C").w(speed) # move.w #speed,d3
|
||||
m.label("wsm")
|
||||
m.hexs("323CFFFF") # move.w #$FFFF,d1
|
||||
m.label("wsi")
|
||||
m.br("51C9", "wsi") # dbra d1,wsi
|
||||
m.br("51CB", "wsm") # dbra d3,wsm
|
||||
m.hexs("5382") # subq.l #1,d2
|
||||
m.br("6600", "wso") # bne.w wso
|
||||
m.hexs("4E75") # rts
|
||||
else: # both = delay loop + mouse poll
|
||||
m.hexs("243C").l(waitsecs)
|
||||
m.label("wbo")
|
||||
m.hexs("363C").w(speed)
|
||||
m.label("wbm")
|
||||
m.hexs("123900BFE001") # move.b $bfe001,d1
|
||||
m.hexs("08010006") # btst #6,d1
|
||||
m.br("6700", "wbd") # beq.w wbd (pressed -> done)
|
||||
m.hexs("323CFFFF") # move.w #$FFFF,d1
|
||||
m.label("wbi")
|
||||
m.br("51C9", "wbi") # dbra d1,wbi
|
||||
m.br("51CB", "wbm") # dbra d3,wbm
|
||||
m.hexs("5382") # subq.l #1,d2
|
||||
m.br("6600", "wbo") # bne.w wbo
|
||||
m.label("wbd")
|
||||
m.hexs("4E75") # rts
|
||||
return m.resolve()
|
||||
|
||||
|
||||
def image_blob(planes: bytes, colors, nplanes: int, ham: bool,
|
||||
base: int = LOAD) -> bytes:
|
||||
"""The data for one image: copper list (with bitplane pointers and colours)
|
||||
followed by the bitplanes -- loaded verbatim to ``base``. The bitplane
|
||||
pointers in the copper are absolute, so each slide is built for the RAM
|
||||
address it will live at."""
|
||||
clen = copper.list_len(nplanes, len(colors))
|
||||
plane_addrs = [base + clen + p * PLANE_SIZE for p in range(nplanes)]
|
||||
return copper.build(nplanes, ham, plane_addrs, colors) + planes
|
||||
|
||||
|
||||
def build_slideshow_adf(images, advance="both", seconds=10, loop=True,
|
||||
video="ntsc") -> bytes:
|
||||
"""Build a bootable slideshow ADF. ``images`` is a list of conv.data dicts
|
||||
(planes/colors/nplanes/ham); each is laid out in its own sector-aligned slot
|
||||
so the boot can read image i from a fixed floppy offset."""
|
||||
# blob length is base-independent, so size first, then rebuild each blob for
|
||||
# the RAM address ($20000 + i*slot) it will be loaded to and cycled from.
|
||||
sizes = [len(image_blob(im["planes"], im["colors"], im["nplanes"], im["ham"]))
|
||||
for im in images]
|
||||
slot = (max(sizes) + 511) // 512 * 512
|
||||
if 1024 + len(images) * slot > ADF_SIZE:
|
||||
raise ValueError("slideshow exceeds the 880K floppy")
|
||||
if LOAD + len(images) * slot > 0x80000:
|
||||
raise ValueError("slideshow exceeds Amiga chip RAM")
|
||||
blobs = [image_blob(im["planes"], im["colors"], im["nplanes"], im["ham"],
|
||||
base=LOAD + i * slot)
|
||||
for i, im in enumerate(images)]
|
||||
speed = 11 if video != "pal" else 13 # delay outer ~1s at 7MHz
|
||||
code = _ss_boot_code(slot, len(blobs), SS_WAITMODE[advance],
|
||||
max(0, int(seconds)), speed, loop)
|
||||
if len(code) > 1024 - 12:
|
||||
raise ValueError("slideshow boot code overruns the 1024-byte boot block")
|
||||
|
||||
boot = bytearray(1024)
|
||||
boot[0:4] = b"DOS\x00"
|
||||
boot[12:12 + len(code)] = code # Kickstart jumps to offset 12
|
||||
struct.pack_into(">I", boot, 4, _bootsum(bytes(boot)))
|
||||
|
||||
adf = bytearray(ADF_SIZE)
|
||||
adf[0:1024] = boot
|
||||
for i, b in enumerate(blobs):
|
||||
off = 1024 + i * slot
|
||||
adf[off:off + len(b)] = b
|
||||
return bytes(adf)
|
||||
Loading…
Add table
Add a link
Reference in a new issue