8bitlenser/lenser/ti99/cartridge.py
2026-07-03 19:35:35 -07:00

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)