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

116 lines
4.3 KiB
Python

"""Generates the TI-99/4A cartridge viewer (TMS9900 machine code).
Sets the TMS9918A to Graphics Mode 2 (256x192 bitmap), builds the name table,
copies the 6144-byte pattern from cartridge ROM to VRAM >0000, and expands the
768 per-cell colour bytes x8 into the 6144-byte colour table at VRAM >2000.
"""
from __future__ import annotations
from .tms9900 import Asm
VDP_DATA = 0x8C00 # write VRAM data
VDP_CTRL = 0x8C02 # write address / register
VDP_STATUS = 0x8802 # read VDP status (bit 7 = frame flag, cleared on read)
WORKSPACE = 0x8300 # scratchpad RAM
# VDP register values for Graphics Mode 2.
VDP_REGS = [
0x02, # R0 M3=1 (bitmap)
0xC0, # R1 16K + display on, interrupts off
0x0E, # R2 name table >3800
0xFF, # R3 colour table >2000 (full 768 rows)
0x03, # R4 pattern table >0000 (full 768 rows)
0x36, # R5 sprite attr >1B00 (parked)
0x07, # R6 sprite patt >3800 (parked)
0x01, # R7 backdrop = black
]
PATTERN_BYTES = 6144
NCELLS = 768
def _set_write_addr(a: Asm, addr: int):
"""Point the VDP write address at VRAM `addr` (low byte then high|>40)."""
a.li(0, (addr & 0xFF) << 8); a.movb_r_sym(0, VDP_CTRL)
a.li(0, (((addr >> 8) | 0x40) & 0xFF) << 8); a.movb_r_sym(0, VDP_CTRL)
def build(code_base: int, data_addr: int, display: str = "forever",
seconds: int = 0, video: str = "ntsc") -> bytes:
"""Emit the viewer. `data_addr` = ROM address of the 6912-byte image data.
`display` (forever/key/seconds) chooses how long the picture is held; on
key/seconds the console is reset (BLWP @>0000) back to the TI title screen.
"""
a = Asm(code_base)
a.limi(0x0000) # interrupts off
a.lwpi(WORKSPACE) # our register file in scratchpad
# ---- programme the 8 VDP registers ----
for reg, val in enumerate(VDP_REGS):
a.li(0, val << 8)
a.movb_r_sym(0, VDP_CTRL)
a.li(0, (0x80 | reg) << 8)
a.movb_r_sym(0, VDP_CTRL)
# ---- name table >3800 = 0,1,...,255 repeated three times (768 bytes) ----
_set_write_addr(a, 0x3800)
a.clr(3) # R3 high byte = current value
a.li(2, NCELLS) # 768 entries
a.label("nameloop")
a.movb_r_sym(3, VDP_DATA)
a.ai(3, 0x0100) # value += 1 (in high byte, wraps mod 256)
a.dec(2)
a.jne("nameloop")
# ---- pattern table >0000 = 6144 bytes copied from ROM ----
_set_write_addr(a, 0x0000)
a.li(1, data_addr)
a.li(2, PATTERN_BYTES)
a.label("patloop")
a.movb_sinc_sym(1, VDP_DATA)
a.dec(2)
a.jne("patloop")
# ---- colour table >2000 = each cell colour written 8x (768 -> 6144) ----
_set_write_addr(a, 0x2000)
a.li(1, data_addr + PATTERN_BYTES)
a.li(2, NCELLS)
a.label("colloop")
a.movb_sinc_r(1, 4) # R4 high byte = colour byte, advance ROM ptr
for _ in range(8):
a.movb_r_sym(4, VDP_DATA)
a.dec(2)
a.jne("colloop")
# ---- how long to hold the picture ----
if display == "seconds":
rate = 50 if video == "pal" else 60
frames = max(1, min(0xFFFF, int(seconds) * rate))
a.li(2, frames) # R2 = frames to wait
a.label("fwait")
a.movb_sym_r(VDP_STATUS, 1) # read status (clears frame flag)
a.andi(1, 0x8000) # bit 7 = a new frame elapsed
a.jeq("fwait")
a.dec(2)
a.jne("fwait")
a.blwp_sym(0x0000) # reset -> TI title screen
elif display == "key":
# scan keyboard columns 0..5; any pressed key (a row reads 0) -> reset.
a.label("kscan")
for col in range(6):
a.li(12, 0x0024) # CRU base for the column select
a.li(5, col << 8) # column in the low 3 bits of the high byte
a.ldcr(5, 3) # drive the 3 column-select lines
a.li(12, 0x0006) # CRU base for the 8 row inputs
a.stcr(6, 8) # read rows into R6 high byte (idle = >FF)
a.ci(6, 0xFF00) # any key down makes a row 0
a.jne("kdone")
a.jmp("kscan")
a.label("kdone")
a.blwp_sym(0x0000) # reset -> TI title screen
else: # forever
a.label("halt")
a.jmp("halt")
return a.resolve()