First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue