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

114 lines
3.9 KiB
Python

"""Generates the ColecoVision/Adam cartridge viewer (Z80 machine code).
Sets the TMS9918A to Graphics Mode 2, 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 -- the same picture
the TI-99 viewer makes, but in Z80 with the ColecoVision VDP ports.
display: forever (hold), key (poll the controller's fire button then reset),
seconds (count VDP frame flags then reset). A cartridge has no OS to return to,
so key/seconds reset the machine (JP 0), which re-runs the cart and re-displays.
"""
from __future__ import annotations
from .z80 import Asm
VDP_DATA = 0xBE # data port (VRAM, auto-increment)
VDP_CTRL = 0xBF # control port (address / register / status)
VDP_REGS = [0x02, 0xC0, 0x0E, 0xFF, 0x03, 0x36, 0x07, 0x01] # Graphics Mode 2
PATTERN_BYTES = 6144
NCELLS = 768
RATE = 60 # NTSC frames/second
JOY_SEG = 0xC0 # OUT (>C0) selects the joystick / left-fire segment
JOY_PORT = 0xFC # IN (>FC) reads controller 1
SOUND_PORT = 0xFF # SN76489 sound chip
# SN76489 "attenuation = 15 (off)" latch byte for each of the 4 channels.
SOUND_OFF = [0x9F, 0xBF, 0xDF, 0xFF]
def _set_write_addr(a: Asm, addr: int):
a.ld_a(addr & 0xFF); a.out_n_a(VDP_CTRL)
a.ld_a(((addr >> 8) & 0x3F) | 0x40); a.out_n_a(VDP_CTRL)
def build(code_base: int, data_addr: int, display: str = "forever",
seconds: int = 0) -> bytes:
a = Asm(code_base)
a.di()
# ---- silence the SN76489 (our >55AA header skips the BIOS sound init, so the
# chip powers up holding a tone); set all 4 channels to attenuation off ----
for v in SOUND_OFF:
a.ld_a(v)
a.out_n_a(SOUND_PORT)
# ---- programme the 8 VDP registers ----
for reg, val in enumerate(VDP_REGS):
a.ld_a(val); a.out_n_a(VDP_CTRL)
a.ld_a(0x80 | reg); a.out_n_a(VDP_CTRL)
# ---- name table >3800 = 0,1,...,255 repeated 3 times (768 bytes) ----
_set_write_addr(a, 0x3800)
a.ld_a(0)
a.ld_c(3)
a.label("name_o")
a.ld_b(0) # B=0 -> DJNZ runs 256 times
a.label("name_i")
a.out_n_a(VDP_DATA)
a.inc_a()
a.djnz("name_i")
a.dec_c()
a.jr_nz("name_o")
# ---- pattern table >0000 = 6144 bytes from ROM (24 * 256) ----
_set_write_addr(a, 0x0000)
a.ld_hl(data_addr)
a.ld_c(PATTERN_BYTES // 256)
a.label("pat_o")
a.ld_b(0)
a.label("pat_i")
a.ld_a_hl()
a.out_n_a(VDP_DATA)
a.inc_hl()
a.djnz("pat_i")
a.dec_c()
a.jr_nz("pat_o")
# ---- colour table >2000 = each cell colour written 8x (768 -> 6144) ----
_set_write_addr(a, 0x2000)
a.ld_hl(data_addr + PATTERN_BYTES)
a.ld_c(NCELLS // 256)
a.label("col_o")
a.ld_b(0)
a.label("col_i")
a.ld_a_hl()
for _ in range(8):
a.out_n_a(VDP_DATA)
a.inc_hl()
a.djnz("col_i")
a.dec_c()
a.jr_nz("col_o")
# ---- hold the picture ----
if display == "key":
a.label("kwait")
a.ld_a(0); a.out_n_a(JOY_SEG) # select joystick / left-fire segment
a.in_a_n(JOY_PORT)
a.and_n(0x40) # bit 6 = fire button (active low)
a.jr_nz("kwait") # still high -> not pressed
a.jp(0x0000) # reset -> re-display
elif display == "seconds":
a.ld_de(max(1, min(0xFFFF, int(seconds) * RATE)))
a.label("swait")
a.in_a_n(VDP_CTRL) # read VDP status (clears frame flag)
a.and_n(0x80)
a.jr_z("swait") # no new frame yet
a.dec_de()
a.ld_a_d(); a.or_e()
a.jr_nz("swait")
a.jp(0x0000) # reset -> re-display
else: # forever
a.label("hang")
a.jr("hang")
return a.resolve()