First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
1
lenser/ti99/__init__.py
Normal file
1
lenser/ti99/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""TI-99/4A (TMS9918A) image conversion and cartridge export."""
|
||||
92
lenser/ti99/cartridge.py
Normal file
92
lenser/ti99/cartridge.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""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)
|
||||
16
lenser/ti99/convert/__init__.py
Normal file
16
lenser/ti99/convert/__init__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""TI-99/4A conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
from ... import imageprep
|
||||
from . import gm2, mono
|
||||
|
||||
_MODULES = {"gm2": gm2, "mono": mono}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="gm2", palette_name="tms9918",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None, base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES.get(mode, gm2)
|
||||
img_rgb = imageprep.prepare(path_or_img, module.WIDTH, module.HEIGHT,
|
||||
module.PIXEL_ASPECT, prep_opt, border_rgb=(0, 0, 0))
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive, base_color=base_color)
|
||||
72
lenser/ti99/convert/gm2.py
Normal file
72
lenser/ti99/convert/gm2.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""TI-99/4A Graphics Mode 2: 256x192, 2 colours per 8x8 cell (15-colour palette).
|
||||
|
||||
Like C64 hires but on the TMS9918A. Produces the bitmap pattern table (6144 B)
|
||||
and one colour byte per cell (768 B); the cartridge viewer expands each cell's
|
||||
colour across its 8 rows of the VDP colour table.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert import base
|
||||
from .. import palette as tpal
|
||||
|
||||
WIDTH, HEIGHT = 256, 192
|
||||
CELL_W, CELL_H = 8, 8
|
||||
PIXEL_ASPECT = 1.0
|
||||
N_COLS, N_ROWS = 32, 24
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="tms9918", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = tpal.palette_lab()
|
||||
prgb = tpal.get_palette().astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
cells, rows, cols = base.cells_lab(img_lab, CELL_W, CELL_H)
|
||||
# Dither-aware colour selection for error-diffusion modes -- each cell's two
|
||||
# colours are chosen so the segment between them brackets the cell, letting
|
||||
# dithering blend to the true shade (far smoother than nearest-colour, which
|
||||
# bands). Ordered/none keep plain nearest-colour selection.
|
||||
if dither_mode in base.DIFFUSION_DITHERS:
|
||||
sets, _ = base.select_cell_sets_dither(cells, plab, tpal.USABLE, n_free=2)
|
||||
else:
|
||||
dist = base.cell_distance(cells, plab)
|
||||
sets, _ = base.select_cell_sets(dist, tpal.USABLE, n_free=2)
|
||||
|
||||
allowed = base.per_pixel_allowed(sets, rows, cols, CELL_W, CELL_H, HEIGHT, WIDTH)
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
|
||||
pattern, colors = _encode(idx, sets, rows, cols)
|
||||
data = bytes(pattern) + bytes(colors) # 6144 + 768
|
||||
|
||||
return base.Conversion(
|
||||
mode="gm2", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=0,
|
||||
viewer="gm2", preview_rgb=prgb[idx],
|
||||
error=base.perceptual_error(idx, img_lab, plab),
|
||||
meta={"palette": "tms9918", "dither": dither_mode},
|
||||
)
|
||||
|
||||
|
||||
def _encode(idx, sets, rows, cols):
|
||||
pattern = np.zeros(6144, dtype=np.uint8) # 768 cells x 8 rows
|
||||
colors = np.zeros(768, dtype=np.uint8) # 1 colour byte per cell
|
||||
for cr in range(rows):
|
||||
for cc in range(cols):
|
||||
ci = cr * cols + cc
|
||||
c0, c1 = int(sets[ci, 0]), int(sets[ci, 1])
|
||||
# brighter colour = foreground (bit 1); store fg in high nibble.
|
||||
bg, fg = (c0, c1)
|
||||
colors[ci] = ((fg & 0x0F) << 4) | (bg & 0x0F)
|
||||
block = idx[cr * 8:cr * 8 + 8, cc * 8:cc * 8 + 8]
|
||||
base = ci * 8
|
||||
for r in range(8):
|
||||
row = block[r]
|
||||
byte = 0
|
||||
for x in range(8):
|
||||
byte = (byte << 1) | (1 if row[x] == fg else 0)
|
||||
pattern[base + r] = byte
|
||||
return pattern, colors
|
||||
44
lenser/ti99/convert/mono.py
Normal file
44
lenser/ti99/convert/mono.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""TI-99/4A (TMS9918A) monochrome / tinted-mono mode.
|
||||
|
||||
Same 256x192, 2-colours-per-cell format as gm2, but matched by *luminance* to a
|
||||
grey ramp (black -> grey -> white) so every cell is neutral -- no colour clash,
|
||||
maximum perceived detail, a clean greyscale photo. Pick a base colour for a
|
||||
tinted monochrome instead. Reuses the gm2 byte packing and viewer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import palette as c64pal
|
||||
from ...convert import base
|
||||
from .. import palette as tpal
|
||||
from . import gm2
|
||||
|
||||
WIDTH, HEIGHT = gm2.WIDTH, gm2.HEIGHT
|
||||
CELL_W, CELL_H = gm2.CELL_W, gm2.CELL_H
|
||||
PIXEL_ASPECT = gm2.PIXEL_ASPECT
|
||||
|
||||
# Neutral ramp: black(1), grey(14), white(15). Lighter siblings for tinting.
|
||||
NEUTRAL = [1, 14, 15]
|
||||
SIBLINGS = {2: 3, 3: 2, 4: 5, 5: 4, 6: 9, 8: 9, 9: 8, 12: 14, 13: 9}
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="tms9918", dither_mode="atkinson",
|
||||
intensive=False, base_color=None):
|
||||
plab = tpal.palette_lab()
|
||||
prgb = tpal.get_palette().astype(np.uint8)
|
||||
ramp = base.luminance_ramp(plab, NEUTRAL, base_color, SIBLINGS)
|
||||
|
||||
idx, sets, rows, cols, err = base.mono_render(
|
||||
img_rgb, plab, ramp, WIDTH, HEIGHT, CELL_W, CELL_H, dither_mode, n_free=2)
|
||||
|
||||
pattern, colors = gm2._encode(idx, sets, rows, cols)
|
||||
data = bytes(pattern) + bytes(colors)
|
||||
|
||||
return base.Conversion(
|
||||
mode="mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=0,
|
||||
viewer="gm2", preview_rgb=prgb[idx], error=err,
|
||||
meta={"palette": "tms9918", "dither": dither_mode, "base_color": base_color},
|
||||
)
|
||||
15
lenser/ti99/exporter.py
Normal file
15
lenser/ti99/exporter.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""Build a TI-99/4A cartridge (.rpk) from a conversion."""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
from . import cartridge
|
||||
|
||||
|
||||
def export_rpk(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="ntsc"):
|
||||
if not output_path.lower().endswith(".rpk"):
|
||||
output_path += ".rpk"
|
||||
title = os.path.splitext(os.path.basename(source_path or output_path))[0]
|
||||
rom = cartridge.build_rom(conv.data, title, display=display,
|
||||
seconds=seconds, video=video)
|
||||
cartridge.write_rpk(rom, output_path, title)
|
||||
return output_path
|
||||
39
lenser/ti99/palette.py
Normal file
39
lenser/ti99/palette.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"""TI-99/4A TMS9918A Video Display Processor palette (15 colours + transparent)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab
|
||||
|
||||
# TMS9918A colours, index 0 = transparent (we never use it for pixels).
|
||||
# Common measured RGB values.
|
||||
TMS9918 = np.array([
|
||||
(0x00, 0x00, 0x00), # 0 transparent (treated as black for matching)
|
||||
(0x00, 0x00, 0x00), # 1 black
|
||||
(0x21, 0xc8, 0x42), # 2 medium green
|
||||
(0x5e, 0xdc, 0x78), # 3 light green
|
||||
(0x54, 0x55, 0xed), # 4 dark blue
|
||||
(0x7d, 0x76, 0xfc), # 5 light blue
|
||||
(0xd4, 0x52, 0x4d), # 6 dark red
|
||||
(0x42, 0xeb, 0xf5), # 7 cyan
|
||||
(0xfc, 0x55, 0x54), # 8 medium red
|
||||
(0xff, 0x79, 0x78), # 9 light red
|
||||
(0xd4, 0xc1, 0x54), # 10 dark yellow
|
||||
(0xe6, 0xce, 0x80), # 11 light yellow
|
||||
(0x21, 0xb0, 0x3b), # 12 dark green
|
||||
(0xc9, 0x5b, 0xba), # 13 magenta
|
||||
(0xcc, 0xcc, 0xcc), # 14 grey
|
||||
(0xff, 0xff, 0xff), # 15 white
|
||||
], dtype=np.float64)
|
||||
|
||||
# Palette indices usable as pixel colours (1..15).
|
||||
USABLE = list(range(1, 16))
|
||||
|
||||
|
||||
def get_palette() -> np.ndarray:
|
||||
return TMS9918
|
||||
|
||||
|
||||
def palette_lab() -> np.ndarray:
|
||||
return srgb_to_lab(TMS9918)
|
||||
87
lenser/ti99/tms9900.py
Normal file
87
lenser/ti99/tms9900.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""A tiny TMS9900 machine-code emitter (just the instructions the TI-99 viewer
|
||||
needs). Words are big-endian. Supports labels + relative-jump backpatching so
|
||||
the viewer's loops can be written readably.
|
||||
|
||||
This is the TI analogue of using `xa` for the 6502 viewers, but since no TMS9900
|
||||
assembler is installed we emit the opcodes directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class Asm:
|
||||
def __init__(self, base: int):
|
||||
self.base = base
|
||||
self.code = bytearray()
|
||||
self.labels: dict[str, int] = {}
|
||||
self._jfix: list[tuple[int, str]] = [] # (pos, label) for 8-bit jump disp
|
||||
|
||||
# ---- low level ----
|
||||
def pos(self) -> int:
|
||||
return self.base + len(self.code)
|
||||
|
||||
def w(self, word: int):
|
||||
self.code += bytes([(word >> 8) & 0xFF, word & 0xFF]) # big-endian
|
||||
|
||||
def label(self, name: str):
|
||||
self.labels[name] = self.pos()
|
||||
|
||||
# ---- immediate-format (format VIII) ----
|
||||
def li(self, reg, imm): self.w(0x0200 | reg); self.w(imm & 0xFFFF)
|
||||
def ai(self, reg, imm): self.w(0x0220 | reg); self.w(imm & 0xFFFF)
|
||||
def ci(self, reg, imm): self.w(0x0280 | reg); self.w(imm & 0xFFFF)
|
||||
def limi(self, imm): self.w(0x0300); self.w(imm & 0xFFFF)
|
||||
def lwpi(self, imm): self.w(0x02E0); self.w(imm & 0xFFFF)
|
||||
|
||||
# ---- single-register (format VI) ----
|
||||
def clr(self, reg): self.w(0x04C0 | reg)
|
||||
def inc(self, reg): self.w(0x0580 | reg)
|
||||
def inct(self, reg): self.w(0x05C0 | reg)
|
||||
def dec(self, reg): self.w(0x0600 | reg)
|
||||
def dect(self, reg): self.w(0x0640 | reg)
|
||||
def swpb(self, reg): self.w(0x06C0 | reg)
|
||||
|
||||
# ---- two-operand (format I); modes: 0=reg,1=*reg,2=@addr(reg),3=*reg+ ----
|
||||
def _fmt1(self, base, td, dreg, ts, sreg, saddr=None, daddr=None):
|
||||
self.w(base | ((td & 3) << 10) | ((dreg & 15) << 6)
|
||||
| ((ts & 3) << 4) | (sreg & 15))
|
||||
if ts == 2:
|
||||
self.w(saddr & 0xFFFF)
|
||||
if td == 2:
|
||||
self.w(daddr & 0xFFFF)
|
||||
|
||||
def mov_rr(self, s, d): self._fmt1(0xC000, 0, d, 0, s)
|
||||
def movb_r_sym(self, s, addr): self._fmt1(0xD000, 2, 0, 0, s, daddr=addr)
|
||||
def movb_sym_r(self, addr, d): self._fmt1(0xD000, 0, d, 2, 0, saddr=addr)
|
||||
def movb_sinc_sym(self, s, addr): self._fmt1(0xD000, 2, 0, 3, s, daddr=addr)
|
||||
def movb_sinc_r(self, s, d): self._fmt1(0xD000, 0, d, 3, s)
|
||||
def movb_r_r(self, s, d): self._fmt1(0xD000, 0, d, 0, s)
|
||||
|
||||
# ---- immediate logic / context switch ----
|
||||
def andi(self, reg, imm): self.w(0x0240 | reg); self.w(imm & 0xFFFF)
|
||||
def ori(self, reg, imm): self.w(0x0260 | reg); self.w(imm & 0xFFFF)
|
||||
def blwp_sym(self, addr): self.w(0x0420); self.w(addr & 0xFFFF) # BLWP @addr
|
||||
|
||||
# ---- CRU (keyboard scan): R12 holds the CRU base ----
|
||||
def ldcr(self, reg, count): self.w(0x3000 | ((count & 15) << 6) | reg)
|
||||
def stcr(self, reg, count): self.w(0x3400 | ((count & 15) << 6) | reg)
|
||||
|
||||
# ---- jumps (format II), 8-bit signed displacement ----
|
||||
def _jump(self, opbase, label):
|
||||
self._jfix.append((len(self.code), label))
|
||||
self.w(opbase) # disp filled in by resolve()
|
||||
|
||||
def jmp(self, label): self._jump(0x1000, label)
|
||||
def jeq(self, label): self._jump(0x1300, label)
|
||||
def jne(self, label): self._jump(0x1600, label)
|
||||
|
||||
# ---- finish ----
|
||||
def resolve(self) -> bytes:
|
||||
for pos, label in self._jfix:
|
||||
target = self.labels[label]
|
||||
here = self.base + pos # address of the jump word
|
||||
disp = (target - (here + 2)) // 2
|
||||
if not -128 <= disp <= 127:
|
||||
raise ValueError(f"jump to {label} out of range ({disp})")
|
||||
self.code[pos + 1] = disp & 0xFF
|
||||
return bytes(self.code)
|
||||
116
lenser/ti99/viewer.py
Normal file
116
lenser/ti99/viewer.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue