92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
"""Builds a TI-99/4A 8KB cartridge ROM (>6000-7FFF) holding the viewer + image
|
|
data, and packages it as an RPK (the cartridge container MAME loads).
|
|
|
|
ROM layout
|
|
>6000 standard cartridge header (>AA magic, pointer to the program list)
|
|
>6010 program-list entry -> appears on the TI menu, points at the viewer
|
|
.... viewer machine code (TMS9900)
|
|
.... image data (6144-byte pattern + 768-byte cell colours)
|
|
pad to 8192 bytes
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import zipfile
|
|
|
|
from . import viewer
|
|
|
|
CART_BASE = 0x6000
|
|
ROM_SIZE = 0x2000 # 8 KB
|
|
PROG_LIST = 0x6010
|
|
|
|
|
|
def _ascii(name: str, limit: int) -> bytes:
|
|
"""TI menu names are uppercase ASCII; strip anything else."""
|
|
out = "".join(c for c in name.upper() if 32 <= ord(c) < 127)
|
|
return out[:limit].encode("ascii") or b"PHOTO"
|
|
|
|
|
|
def _even(x: int) -> int:
|
|
return x + (x & 1)
|
|
|
|
|
|
def build_rom(data: bytes, title: str = "PHOTO", display: str = "forever",
|
|
seconds: int = 0, video: str = "ntsc") -> bytes:
|
|
if len(data) != viewer.PATTERN_BYTES + viewer.NCELLS:
|
|
raise ValueError(f"unexpected data length {len(data)}")
|
|
name = _ascii(title, 16)
|
|
|
|
vk = dict(display=display, seconds=seconds, video=video)
|
|
entry = _even(PROG_LIST + 2 + 2 + 1 + len(name)) # code starts after the name
|
|
code = viewer.build(entry, 0, **vk) # pass 1: measure length
|
|
data_addr = _even(entry + len(code))
|
|
code = viewer.build(entry, data_addr, **vk) # pass 2: real data address
|
|
|
|
if data_addr - CART_BASE + len(data) > ROM_SIZE:
|
|
raise ValueError("image + viewer exceed 8KB cartridge")
|
|
|
|
rom = bytearray(b"\x00" * ROM_SIZE)
|
|
|
|
def put(addr, payload):
|
|
off = addr - CART_BASE
|
|
rom[off:off + len(payload)] = payload
|
|
|
|
def putw(addr, word):
|
|
put(addr, bytes([(word >> 8) & 0xFF, word & 0xFF]))
|
|
|
|
# cartridge header
|
|
rom[0] = 0xAA # valid
|
|
rom[1] = 0x01 # version
|
|
putw(0x6006, PROG_LIST)
|
|
|
|
# program-list entry (single item)
|
|
putw(PROG_LIST + 0, 0x0000) # no next entry
|
|
putw(PROG_LIST + 2, entry) # viewer entry point
|
|
rom[PROG_LIST + 4 - CART_BASE] = len(name)
|
|
put(PROG_LIST + 5, name)
|
|
|
|
put(entry, code)
|
|
put(data_addr, data)
|
|
return bytes(rom)
|
|
|
|
|
|
def write_rpk(rom: bytes, path: str, title: str = "photo"):
|
|
"""Write an MAME RPK (zip with a standard single-ROM layout)."""
|
|
binname = "viewer.bin"
|
|
layout = (
|
|
'<?xml version="1.0" encoding="utf-8"?>\n'
|
|
'<romset version="1.0">\n'
|
|
' <resources>\n'
|
|
f' <rom id="romimage" file="{binname}"/>\n'
|
|
' </resources>\n'
|
|
' <configuration>\n'
|
|
' <pcb type="standard">\n'
|
|
' <socket id="rom_socket" uses="romimage"/>\n'
|
|
' </pcb>\n'
|
|
' </configuration>\n'
|
|
'</romset>\n'
|
|
)
|
|
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
|
|
z.writestr(binname, rom)
|
|
z.writestr("layout.xml", layout)
|