First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
172
lenser/intv/cartridge.py
Normal file
172
lenser/intv/cartridge.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"""Builds a clean Mattel Intellivision cartridge ROM (maps at $5000) holding a
|
||||
CP1610 viewer plus the encoded image, with NO copyrighted EXEC/game data.
|
||||
|
||||
ROM layout (4096 16-bit words, big-endian; loose-file extension ``.int`` /
|
||||
``.bin`` / ``.rom`` -- MAME maps a headerless dump at $5000):
|
||||
|
||||
$5000 six BIDECLE header pointers + flags word
|
||||
MOB / process / bkgnd -> a $0000 word (empty, tolerated by the EXEC)
|
||||
start -> MAIN
|
||||
GRAM -> [$0001, 0x8] (1 blank card -- minimal list
|
||||
that does NOT crash the EXEC's loader; the
|
||||
ISR re-uploads the real tiles anyway)
|
||||
title -> (year-1900) word, ASCII, 0
|
||||
$5040 MAIN (install ISR, fill BACKTAB from the card table, then idle/duration)
|
||||
.... ISR (display handshake $0020, colour stack, GRAM upload)
|
||||
.... card table (240 words) and GRAM tile table (512 words)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
from .cp1610 import Asm
|
||||
|
||||
ROM_WORDS = 4096
|
||||
EMPTY = 0x500D
|
||||
TITLE = 0x500E
|
||||
GRAMLIST = 0x5018
|
||||
MAIN = 0x5040
|
||||
CARDROM = 0x5100
|
||||
GRAMROM = 0x5200
|
||||
# 8-bit Scratch RAM ($0100-$01EF) is separate from the EXEC stack (which roams
|
||||
# 16-bit System RAM $02F0+), so it is safe for our persistent ISR state.
|
||||
CHUNKCTR = 0x0108 # GRAM-upload progress (0..8 chunks of 64 words)
|
||||
FRAMELO = 0x0109 # display-duration frame counter (lo byte)
|
||||
FRAMEHI = 0x010A # ... hi byte
|
||||
N_CHUNKS = 8 # 8 x 64 = 512 GRAM words
|
||||
HAND_R = 0x01FF # right hand controller (PSG I/O port)
|
||||
|
||||
|
||||
def _bidecle(put, addr, target):
|
||||
put(addr, target & 0xFF)
|
||||
put(addr + 1, (target >> 8) & 0xFF)
|
||||
|
||||
|
||||
def build_rom(data: dict, title: str = "PHOTO", display: str = "forever",
|
||||
seconds: int = 0, video: str = "ntsc") -> bytes:
|
||||
gram = [int(w) & 0xFFFF for w in data["gram"]] # 512 words
|
||||
cards = [int(w) & 0xFFFF for w in data["cards"]] # 240 words
|
||||
|
||||
rom = [0] * ROM_WORDS
|
||||
|
||||
def put(a, w):
|
||||
rom[a - 0x5000] = w & 0xFFFF
|
||||
|
||||
# ---- header ----
|
||||
_bidecle(put, 0x5000, EMPTY) # MOB
|
||||
_bidecle(put, 0x5002, EMPTY) # process
|
||||
_bidecle(put, 0x5004, MAIN) # program start
|
||||
_bidecle(put, 0x5006, EMPTY) # background
|
||||
_bidecle(put, 0x5008, GRAMLIST) # GRAM
|
||||
_bidecle(put, 0x500A, TITLE) # title
|
||||
put(0x500C, 0x0000) # flags
|
||||
put(EMPTY, 0x0000)
|
||||
for i, w in enumerate([0x0001, 0, 0, 0, 0, 0, 0, 0, 0]):
|
||||
put(GRAMLIST + i, w)
|
||||
|
||||
# ---- title block ----
|
||||
a = TITLE
|
||||
put(a, 84) # year - 1900 (cosmetic)
|
||||
a += 1
|
||||
name = "".join(c for c in title.upper() if 32 <= ord(c) < 127)[:14] or "PHOTO"
|
||||
for ch in name:
|
||||
put(a, ord(ch))
|
||||
a += 1
|
||||
put(a, 0)
|
||||
|
||||
# ---- data tables ----
|
||||
for i, w in enumerate(cards):
|
||||
put(CARDROM + i, w)
|
||||
for i, w in enumerate(gram):
|
||||
put(GRAMROM + i, w)
|
||||
|
||||
# ---- code ----
|
||||
words, isr = _assemble(display, seconds, video)
|
||||
for i, w in enumerate(words):
|
||||
put(MAIN + i, w)
|
||||
|
||||
return b"".join(struct.pack(">H", w) for w in rom)
|
||||
|
||||
|
||||
def _duration_loop(z, display, seconds, fps):
|
||||
"""Emit the post-setup idle/duration loop."""
|
||||
nframes = max(1, int(seconds) * fps)
|
||||
if display == "key":
|
||||
z.label('loop')
|
||||
z.mvi(HAND_R, 0); z.cmpi(0xFF, 0); z.bnze('redisplay')
|
||||
z.b('loop')
|
||||
z.label('redisplay'); z.j(MAIN)
|
||||
elif display == "seconds":
|
||||
hi, lo = (nframes >> 8) & 0xFF, nframes & 0xFF
|
||||
z.label('loop')
|
||||
z.mvi(FRAMEHI, 0); z.cmpi(hi, 0); z.bnze('chkhi')
|
||||
z.mvi(FRAMELO, 0); z.cmpi(lo, 0); z.bc('redisplay')
|
||||
z.b('loop')
|
||||
z.label('chkhi'); z.bc('redisplay')
|
||||
z.b('loop')
|
||||
z.label('redisplay'); z.j(MAIN)
|
||||
else:
|
||||
z.label('loop'); z.b('loop')
|
||||
|
||||
|
||||
def _seconds_tick(z):
|
||||
z.mvi(FRAMELO, 1); z.incr(1); z.andi(0xFF, 1); z.mvo(1, FRAMELO)
|
||||
z.bnze('noco'); z.mvi(FRAMEHI, 1); z.incr(1); z.mvo(1, FRAMEHI)
|
||||
z.label('noco')
|
||||
|
||||
|
||||
def _gram_chunk(z, gramrom):
|
||||
"""ISR fragment: upload one 64-word GRAM chunk/frame over 8 frames."""
|
||||
z.mvi(CHUNKCTR, 2)
|
||||
z.cmpi(N_CHUNKS, 2); z.bc('grdone')
|
||||
z.movr(2, 3); z.sll(3, 2); z.sll(3, 2); z.sll(3, 2) # R3 = chunk * 64
|
||||
z.movr(3, 4); z.addi(0x3800, 4)
|
||||
z.movr(3, 5); z.addi(gramrom, 5)
|
||||
z.mvii(64, 1)
|
||||
z.label('up'); z.mvi_at(5, 0); z.mvo_at(0, 4); z.decr(1); z.bnze('up')
|
||||
z.incr(2); z.mvo(2, CHUNKCTR)
|
||||
z.label('grdone')
|
||||
|
||||
|
||||
def _patch_isr(z):
|
||||
"""Patch the two MVII immediates that install the ISR vector at $0100/$0101."""
|
||||
words = z.resolve()
|
||||
isr = z.labels['isr']
|
||||
words[2] = isr & 0xFF
|
||||
words[6] = (isr >> 8) & 0xFF
|
||||
return words, isr
|
||||
|
||||
|
||||
def _assemble(display, seconds, video):
|
||||
"""Emit MAIN + ISR for the flicker-free FGBG viewer. Returns (words, isr).
|
||||
|
||||
GRAM (512 words) is far too big to upload in one VBLANK (~3500 cycles), so
|
||||
the ISR uploads it 64 words at a time over 8 frames, tracking progress in
|
||||
Scratch RAM; once done the cards reference the fully-loaded tiles.
|
||||
"""
|
||||
fps = 50 if video == "pal" else 60
|
||||
z = Asm(MAIN)
|
||||
z.dis()
|
||||
z.mvii(0, 0); z.mvo(0, 0x0100) # install ISR (words 2,6 = imm lo/hi)
|
||||
z.mvii(0, 0); z.mvo(0, 0x0101)
|
||||
z.mvii(0, 0)
|
||||
z.mvo(0, CHUNKCTR); z.mvo(0, FRAMELO); z.mvo(0, FRAMEHI)
|
||||
z.mvii(0x0200, 4); z.mvii(CARDROM, 5); z.mvii(240, 1)
|
||||
z.label('fill'); z.mvi_at(5, 0); z.mvo_at(0, 4); z.decr(1); z.bnze('fill')
|
||||
z.eis()
|
||||
_duration_loop(z, display, seconds, fps)
|
||||
|
||||
# ---- ISR ----
|
||||
# GRAM writes are VBLANK-only and writing $0020 (display enable) closes that
|
||||
# window, so upload FIRST and handshake LAST. Writing $0021 selects FGBG mode
|
||||
# (each card carries its own fg+bg); never READ $0021 (-> Color-Stack mode).
|
||||
z.label('isr')
|
||||
z.pshr(5)
|
||||
_gram_chunk(z, GRAMROM)
|
||||
if display == "seconds":
|
||||
_seconds_tick(z)
|
||||
z.mvii(0, 0); z.mvo(0, 0x0021) # select FGBG mode
|
||||
z.mvii(1, 0); z.mvo(0, 0x0020) # display handshake LAST
|
||||
z.pulr(5); z.jr(5)
|
||||
return _patch_isr(z)
|
||||
Loading…
Add table
Add a link
Reference in a new issue