101 lines
4.1 KiB
Python
101 lines
4.1 KiB
Python
"""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
|