242 lines
11 KiB
Python
242 lines
11 KiB
Python
"""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)
|