First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
1
lenser/c128/__init__.py
Normal file
1
lenser/c128/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Commodore 128 target for lenser (VDC 8563 80-column display)."""
|
||||
21
lenser/c128/convert/__init__.py
Normal file
21
lenser/c128/convert/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Commodore 128 conversion dispatch (VDC 80-column)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import imageprep
|
||||
from . import mono
|
||||
from . import color
|
||||
from . import hicolor
|
||||
|
||||
_MODULES = {"mono": mono, "color": color, "hicolor": hicolor}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="mono", palette_name="vdc",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None,
|
||||
base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES.get(mode, mono)
|
||||
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)
|
||||
44
lenser/c128/convert/color.py
Normal file
44
lenser/c128/convert/color.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""C128 VDC chunky 80x100 16-colour mode.
|
||||
|
||||
MAME's 8563 draws the "bitmap" through the character/font path, so true
|
||||
per-pixel bitmap colour isn't possible -- but each 8x2 cell can carry its own
|
||||
colour via the attribute byte (fg = high nibble). Filling the character matrix
|
||||
with a solid glyph turns that into a per-cell SOLID colour image: a chunky
|
||||
80x100 picture using all 16 VDC colours. Lower resolution than `mono`'s
|
||||
640x200, but full colour (or smooth multi-level greyscale on the four greys).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as vdcpal
|
||||
|
||||
# 8x2 device-pixel cells over the 640x200 display = 80x100 colour cells. Each
|
||||
# cell is ~8*0.42 wide x 2 tall, so the 80x100 grid is ~4:3 -> aspect ~1.68.
|
||||
WIDTH, HEIGHT = 80, 100
|
||||
PIXEL_ASPECT = 1.68
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="vdc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
prgb = vdcpal.get_palette().astype(np.uint8)
|
||||
plab = vdcpal.palette_lab()
|
||||
|
||||
lab = c64pal.srgb_to_lab(img_rgb)
|
||||
allowed = np.tile(np.arange(16), (HEIGHT, WIDTH, 1))
|
||||
idx = dither.quantize(lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
|
||||
# one attribute byte per cell; colour in the HIGH nibble (VDC fg = attr>>4),
|
||||
# low nibble (bg) left 0 -- the solid glyph means only fg shows.
|
||||
attr = ((idx.reshape(-1).astype(np.uint8) & 0x0F) << 4)
|
||||
data = bytes(attr.tolist()) # 80*100 = 8000
|
||||
|
||||
return Conversion(
|
||||
mode="color", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=0, viewer="c128",
|
||||
preview_rgb=prgb[idx], error=perceptual_error(idx, lab, plab),
|
||||
meta={"palette": "vdc", "dither": dither_mode, "fgbg": 0x0F,
|
||||
"vdc_mode": "color"},
|
||||
)
|
||||
142
lenser/c128/convert/hicolor.py
Normal file
142
lenser/c128/convert/hicolor.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""C128 VDC high-resolution colour mode (640x200 via a custom character set).
|
||||
|
||||
MAME's 8563 (and real hardware) renders 80-column *character* mode with a custom
|
||||
font + per-cell attributes. That gives genuine per-pixel detail (8x8 glyph per
|
||||
cell) at 640x200, with one freely chosen INK colour per cell (attribute low
|
||||
nibble) over a single GLOBAL background (VDC register 26) -- a ZX-Spectrum-like
|
||||
colour model, but at double the Spectrum's horizontal resolution.
|
||||
|
||||
The picture is built by, for each 8x8 cell, choosing the ink colour that best
|
||||
represents it against the global background, dithering the cell to ink/background
|
||||
(a 1bpp glyph), then vector-quantising the ~2000 cell glyphs down to a 512-entry
|
||||
character set. The VDC addresses 256 glyphs per bank; a second bank is selected
|
||||
per cell by the attribute's ALTERNATE_CHARSET bit (bit 7), giving 512 glyphs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as vdcpal
|
||||
|
||||
WIDTH, HEIGHT = 640, 200
|
||||
PIXEL_ASPECT = 0.42 # same fine pixels as the mono bitmap
|
||||
CW, CH = 8, 8 # 8x8 character cells
|
||||
COLS, ROWS = WIDTH // CW, HEIGHT // CH # 80 x 25 = 2000 cells
|
||||
BANK = 256 # glyphs per VDC charset bank
|
||||
NGLYPH = 512 # two banks (selected per cell by attr bit 7)
|
||||
|
||||
# VDC RAM layout (matches the C128 default so registers stay default).
|
||||
CODES_ADDR = 0x0000
|
||||
ATTR_ADDR = 0x0800
|
||||
CHAR_ADDR = 0x2000 # bank 0 at $2000, bank 1 at $3000 (+$1000)
|
||||
VDC_LEN = 0x4000 # full 16K, copied to VDC RAM by the viewer
|
||||
|
||||
|
||||
def _cell_view(a):
|
||||
"""(ROWS*CH, COLS*CW, ...) -> (ROWS, COLS, CH, CW, ...) cell blocks."""
|
||||
r = a.reshape(ROWS, CH, COLS, CW, *a.shape[2:])
|
||||
return r.transpose(0, 2, 1, 3, *range(4, a.ndim + 2))
|
||||
|
||||
|
||||
def _kmeans_binary(patterns, k, iters=10, seed=0):
|
||||
"""Cluster 0/1 patterns (N, D) into k binary centroids (Hamming / majority)."""
|
||||
n = len(patterns)
|
||||
rng = np.random.default_rng(seed)
|
||||
uniq = np.unique(patterns, axis=0)
|
||||
if len(uniq) <= k:
|
||||
cb = np.zeros((k, patterns.shape[1]), np.uint8)
|
||||
cb[:len(uniq)] = uniq
|
||||
d = (patterns[:, None, :] != cb[None, :, :]).sum(2)
|
||||
return cb, d.argmin(1)
|
||||
cb = uniq[rng.choice(len(uniq), k, replace=False)].astype(np.uint8)
|
||||
assign = np.zeros(n, np.int32)
|
||||
for _ in range(iters):
|
||||
# nearest centroid by Hamming distance, in chunks to bound memory
|
||||
for s in range(0, n, 4096):
|
||||
blk = patterns[s:s + 4096]
|
||||
d = (blk[:, None, :] != cb[None, :, :]).sum(2)
|
||||
assign[s:s + 4096] = d.argmin(1)
|
||||
for j in range(k):
|
||||
members = patterns[assign == j]
|
||||
if len(members):
|
||||
cb[j] = (members.mean(0) >= 0.5).astype(np.uint8)
|
||||
return cb, assign
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="vdc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
"""Full 16-colour high-resolution mode."""
|
||||
return build(img_rgb, dither_mode, inks=list(range(16)),
|
||||
bg_list=list(range(16)), mode_name="hicolor")
|
||||
|
||||
|
||||
def build(img_rgb, dither_mode, inks, bg_list, mode_name):
|
||||
"""Shared 640x200 custom-charset encoder.
|
||||
|
||||
inks palette indices a cell's foreground may use (per-cell ink choice)
|
||||
bg_list palette indices to try as the single global background
|
||||
mode_name value for Conversion.mode (the VDC viewer is the same either way)
|
||||
"""
|
||||
prgb = vdcpal.get_palette().astype(np.uint8)
|
||||
plab = vdcpal.palette_lab()
|
||||
lab = c64pal.srgb_to_lab(img_rgb) # (H, W, 3)
|
||||
inks = np.asarray(inks)
|
||||
|
||||
# distance from every pixel to every palette colour
|
||||
dist = np.linalg.norm(lab[:, :, None, :] - plab[None, None, :, :], axis=3)
|
||||
dist_cells = _cell_view(dist) # (R,C,CH,CW,16)
|
||||
dist_inks = dist_cells[..., inks] # (R,C,CH,CW,len)
|
||||
|
||||
# choose the global background: the colour that, as the shared second tone,
|
||||
# lets each cell's best ink reproduce the image with least total error.
|
||||
best_bg, best_err, best_ink = bg_list[0], None, None
|
||||
for bg in bg_list:
|
||||
d_bg = dist_cells[..., bg:bg + 1] # (R,C,CH,CW,1)
|
||||
per_ink = np.minimum(d_bg, dist_inks).sum((2, 3)) # (R,C,len)
|
||||
sel = per_ink.argmin(2) # (R,C) index into inks
|
||||
err = np.take_along_axis(per_ink, sel[..., None], 2).sum()
|
||||
if best_err is None or err < best_err:
|
||||
best_bg, best_err, best_ink = bg, err, inks[sel]
|
||||
bg, ink_cell = best_bg, best_ink # ink_cell (R,C) palette
|
||||
|
||||
# dither each pixel between the global bg and its cell's ink
|
||||
ink_px = np.repeat(np.repeat(ink_cell, CH, 0), CW, 1) # (H,W)
|
||||
allowed = np.stack([np.full((HEIGHT, WIDTH), bg), ink_px], axis=2)
|
||||
idx = dither.quantize(lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
|
||||
# 1bpp glyph per cell (1 = ink), then vector-quantise to a 256-glyph charset
|
||||
bits = (idx == ink_px).astype(np.uint8) # (H,W)
|
||||
glyph_cells = _cell_view(bits).reshape(ROWS * COLS, CH * CW)
|
||||
codebook, assign = _kmeans_binary(glyph_cells, NGLYPH) # assign 0..NGLYPH-1
|
||||
code_map = assign.reshape(ROWS, COLS)
|
||||
|
||||
# rebuild the actually-displayed image from the quantised glyphs + colours
|
||||
glyphs_img = codebook[assign].reshape(ROWS, COLS, CH, CW)
|
||||
shown_ink = (glyphs_img == 1)
|
||||
final_idx = np.where(
|
||||
shown_ink, ink_cell[:, :, None, None], bg).astype(np.uint16)
|
||||
final_idx = final_idx.transpose(0, 2, 1, 3).reshape(HEIGHT, WIDTH)
|
||||
|
||||
# ---- assemble the VDC RAM image ----
|
||||
vdc = bytearray(VDC_LEN)
|
||||
glyph = code_map.reshape(-1)
|
||||
codes = (glyph & 0xFF).astype(np.uint8) # char code within bank
|
||||
# ink in the low nibble; bit 7 (ALTERNATE_CHARSET) selects bank 1
|
||||
attr = ((ink_cell.reshape(-1) & 0x0F) | ((glyph >> 8) << 7)).astype(np.uint8)
|
||||
vdc[CODES_ADDR:CODES_ADDR + codes.size] = codes.tobytes()
|
||||
vdc[ATTR_ADDR:ATTR_ADDR + attr.size] = attr.tobytes()
|
||||
for g in range(NGLYPH):
|
||||
rows = np.packbits(codebook[g].reshape(CH, CW), axis=1).reshape(-1)
|
||||
off = CHAR_ADDR + (g >> 8) * 0x1000 + ((g & 0xFF) << 4)
|
||||
vdc[off:off + CH] = rows.tobytes()
|
||||
|
||||
return Conversion(
|
||||
mode=mode_name, width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=final_idx, data=bytes(vdc), data_addr=0, viewer="c128",
|
||||
preview_rgb=prgb[final_idx],
|
||||
error=perceptual_error(final_idx, lab, plab),
|
||||
meta={"palette": "vdc", "dither": dither_mode, "fgbg": bg & 0x0F,
|
||||
"vdc_mode": "hicolor", "bg": bg},
|
||||
)
|
||||
29
lenser/c128/convert/mono.py
Normal file
29
lenser/c128/convert/mono.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""C128 VDC high-resolution greyscale / black-and-white (640x200).
|
||||
|
||||
MAME's 8563 has no true linear bitmap renderer (its R25-bit7 "bitmap" path only
|
||||
emits one bit per 8-pixel cell), so genuine 640x200 detail has to come through
|
||||
the character/font path -- exactly as the `hicolor` mode does. This mode reuses
|
||||
that machinery restricted to the VDC's four greys (black, dark grey, light grey,
|
||||
white): each 8x8 cell picks the grey that best matches it over a global grey
|
||||
background and dithers to a 1bpp glyph, giving smooth multi-level greyscale at
|
||||
full resolution. `--mono-base` swaps the grey ramp for a black->colour->white
|
||||
ramp to tint the result.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import hicolor
|
||||
|
||||
WIDTH, HEIGHT = hicolor.WIDTH, hicolor.HEIGHT
|
||||
PIXEL_ASPECT = hicolor.PIXEL_ASPECT
|
||||
|
||||
GREYS = [0, 1, 14, 15] # VDC black, dark grey, light grey, white
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="vdc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
if base_color in range(1, 16):
|
||||
inks = sorted({0, int(base_color), 15}) # black -> tint -> white
|
||||
else:
|
||||
inks = GREYS
|
||||
return hicolor.build(img_rgb, dither_mode, inks=inks, bg_list=inks,
|
||||
mode_name="mono")
|
||||
23
lenser/c128/exporter.py
Normal file
23
lenser/c128/exporter.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""Build a C128 autobooting .d64 (the VDC viewer PRG, loaded with RUN"PIC")."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from .. import diskimage
|
||||
from .viewer import assemble
|
||||
|
||||
|
||||
def export_d64(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="ntsc"):
|
||||
if not output_path.lower().endswith(".d64"):
|
||||
output_path += ".d64"
|
||||
# "color" = 80x100 chunky solid cells; "hicolor" (also used by mono) =
|
||||
# 640x200 custom-charset font mode.
|
||||
if conv.meta.get("vdc_mode") == "color":
|
||||
prg = assemble.build_prg_color(bytes(conv.data), conv.meta.get("fgbg", 0x0F))
|
||||
else:
|
||||
prg = assemble.build_prg_hicolor(bytes(conv.data), conv.meta.get("fgbg", 0x00))
|
||||
name = os.path.splitext(os.path.basename(source_path or output_path))[0]
|
||||
diskimage.build_disk(output_path, "d64", name[:16] or "8bitlenser", "cv",
|
||||
[("pic", prg)])
|
||||
return output_path
|
||||
38
lenser/c128/palette.py
Normal file
38
lenser/c128/palette.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"""Commodore 128 VDC (8563) 16-colour RGBI palette.
|
||||
|
||||
The VDC outputs digital RGBI, giving a CGA-like 16-colour set (quite different
|
||||
from the VIC-II's colours). Index 0 = black, 15 = white -- the only two the mono
|
||||
mode needs; the rest are provided for tinted-mono and future colour modes.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab
|
||||
|
||||
VDC = np.array([
|
||||
(0x00, 0x00, 0x00), # 0 black
|
||||
(0x55, 0x55, 0x55), # 1 dark grey
|
||||
(0x00, 0x00, 0xAA), # 2 blue
|
||||
(0x55, 0x55, 0xFF), # 3 light blue
|
||||
(0x00, 0xAA, 0x00), # 4 green
|
||||
(0x55, 0xFF, 0x55), # 5 light green
|
||||
(0x00, 0xAA, 0xAA), # 6 cyan
|
||||
(0x55, 0xFF, 0xFF), # 7 light cyan
|
||||
(0xAA, 0x00, 0x00), # 8 red
|
||||
(0xFF, 0x55, 0x55), # 9 light red
|
||||
(0xAA, 0x00, 0xAA), # 10 purple
|
||||
(0xFF, 0x55, 0xFF), # 11 light purple
|
||||
(0xAA, 0x55, 0x00), # 12 brown
|
||||
(0xFF, 0xFF, 0x55), # 13 light yellow
|
||||
(0xAA, 0xAA, 0xAA), # 14 light grey
|
||||
(0xFF, 0xFF, 0xFF), # 15 white
|
||||
], dtype=np.float64)
|
||||
|
||||
|
||||
def get_palette() -> np.ndarray:
|
||||
return VDC
|
||||
|
||||
|
||||
def palette_lab() -> np.ndarray:
|
||||
return srgb_to_lab(VDC)
|
||||
1
lenser/c128/viewer/__init__.py
Normal file
1
lenser/c128/viewer/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""C128 8502/VDC viewer (assembled by xa)."""
|
||||
122
lenser/c128/viewer/assemble.py
Normal file
122
lenser/c128/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""Assemble the C128 VDC viewer with `xa` and build the loadable PRG.
|
||||
|
||||
The PRG loads at the C128 BASIC start ($1C01): a tiny BASIC stub (`10 SYS7200`)
|
||||
followed by the 8502 viewer (at $1C20) and, from $2000, the 640x200 bitmap.
|
||||
Running it (RUN"PIC") executes the stub, which SYSes the viewer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
BASIC_START = 0x1C01
|
||||
ML_ORG = 0x1C20
|
||||
DATA_ORG = 0x2000 # bitmap goes here (viewer copies it to the VDC)
|
||||
|
||||
# BASIC: 10 SYS7200 ($1C20 = 7200) -- bytes as they sit from $1C01
|
||||
_STUB = bytes([0x0B, 0x1C, 0x0A, 0x00, 0x9E,
|
||||
0x37, 0x32, 0x30, 0x30, 0x00, 0x00, 0x00])
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def _xa(wrapper: str) -> bytes:
|
||||
"""Assemble a generated wrapper (xa runs in VIEWER_DIR so #includes resolve)."""
|
||||
if not have_xa():
|
||||
raise AssemblerError("The 'xa' (xa65) assembler was not found on PATH.\n"
|
||||
"Install it with: sudo apt install xa65")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
out = os.path.join(td, "v.bin")
|
||||
fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR)
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(wrapper)
|
||||
proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)],
|
||||
capture_output=True, text=True, cwd=VIEWER_DIR)
|
||||
if proc.returncode != 0:
|
||||
raise AssemblerError(f"xa failed:\n{proc.stdout}{proc.stderr}")
|
||||
with open(out, "rb") as f:
|
||||
return f.read()
|
||||
finally:
|
||||
os.unlink(wrap)
|
||||
|
||||
|
||||
def _assemble(fgbg: int, source: str) -> bytes:
|
||||
return _xa(f"#define SRC ${DATA_ORG:04X}\n"
|
||||
f"#define FGBG ${fgbg & 0xFF:02X}\n"
|
||||
f'#include "{source}"\n')
|
||||
|
||||
|
||||
def _wrap_prg(code: bytes, data: bytes) -> bytes:
|
||||
"""Lay out load-address prefix + BASIC stub + viewer code + data @ $2000."""
|
||||
mem = bytearray()
|
||||
mem += _STUB # $1C01..
|
||||
mem += b"\x00" * (ML_ORG - BASIC_START - len(_STUB))
|
||||
mem += code # $1C20..
|
||||
if len(mem) > DATA_ORG - BASIC_START:
|
||||
raise AssemblerError("viewer code overruns the $2000 data area")
|
||||
mem += b"\x00" * (DATA_ORG - BASIC_START - len(mem))
|
||||
mem += data # $2000..
|
||||
return bytes([BASIC_START & 0xFF, BASIC_START >> 8]) + bytes(mem)
|
||||
|
||||
|
||||
def build_prg_color(attributes: bytes, fgbg: int = 0x0F) -> bytes:
|
||||
"""Return the loadable PRG for the 80x100 chunky-colour viewer.
|
||||
|
||||
`attributes` is the 8000-byte per-cell colour map (colour in the high
|
||||
nibble); the viewer fills the character matrix with a solid glyph itself.
|
||||
"""
|
||||
return _wrap_prg(_assemble(fgbg, "color.s"), attributes)
|
||||
|
||||
|
||||
def build_prg_hicolor(vdc_image: bytes, fgbg: int = 0x00) -> bytes:
|
||||
"""Return the loadable PRG for the 640x200 custom-charset viewer (font mode).
|
||||
|
||||
Used by both the `hicolor` and `mono` modes. `vdc_image` is the full VDC RAM
|
||||
image (character codes, attributes and the custom character set already laid
|
||||
out); `fgbg` carries the global background in its low nibble. The viewer
|
||||
copies the image verbatim into VDC RAM.
|
||||
"""
|
||||
return _wrap_prg(_assemble(fgbg, "hicolor.s"), vdc_image)
|
||||
|
||||
|
||||
_SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
|
||||
|
||||
|
||||
def build_slideshow_prg(fgbg_list, advance: str = "both", seconds: int = 10,
|
||||
loop: bool = True, video: str = "pal") -> bytes:
|
||||
"""Return the bootable C128 slideshow viewer PRG (RUN\"PIC\").
|
||||
|
||||
``fgbg_list`` is one VDC R26 background byte per image (conv.meta["fgbg"]);
|
||||
the per-image pictures are separate "00".."NN" files the viewer loads. The
|
||||
viewer is code-only (no appended image) -- just the BASIC stub + the 8502
|
||||
loop + the ss_fgbg table.
|
||||
"""
|
||||
if not fgbg_list:
|
||||
raise AssemblerError("a slideshow needs at least one image")
|
||||
jiffyps = 60 if video == "ntsc" else 50
|
||||
table = ",".join(str(int(b) & 0xFF) for b in fgbg_list)
|
||||
code = _xa(
|
||||
f"#define WAITMODE {_SS_WAITMODE[advance]}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define JIFFYPS {jiffyps}\n"
|
||||
f"#define NIMAGES {len(fgbg_list)}\n"
|
||||
f"#define LOOPFLAG {1 if loop else 0}\n"
|
||||
'#include "slideshow.s"\n'
|
||||
"ss_fgbg:\n"
|
||||
f" .byte {table}\n")
|
||||
mem = bytearray()
|
||||
mem += _STUB # $1C01 BASIC stub (10 SYS7200)
|
||||
mem += b"\x00" * (ML_ORG - BASIC_START - len(_STUB))
|
||||
mem += code # $1C20 viewer + ss_fgbg table
|
||||
return bytes([BASIC_START & 0xFF, BASIC_START >> 8]) + bytes(mem)
|
||||
173
lenser/c128/viewer/color.s
Normal file
173
lenser/c128/viewer/color.s
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
; Commodore 128 VDC (8563) 80x100 chunky-colour image viewer.
|
||||
;
|
||||
; MAME's 8563 renders the bitmap through the character/font path- each display
|
||||
; byte is a CHARACTER CODE whose glyph is drawn with fg = attribute high nibble,
|
||||
; bg = attribute low nibble. So a per-cell SOLID colour image is produced by
|
||||
; filling the whole character matrix with $FF (a solid glyph) and giving every
|
||||
; cell its colour in the high nibble of its attribute byte.
|
||||
;
|
||||
; R9=1 (2 scan lines per char row) makes 80x100 cells- 8000 char bytes + 8000
|
||||
; attribute bytes = 16000, which fits the stock 16K VDC RAM.
|
||||
;
|
||||
; #defines from viewer/assemble.py --
|
||||
; SRC main-RAM address of the 8000 attribute bytes ($2000)
|
||||
; FGBG VDC register 26 default value (unused cells / background)
|
||||
|
||||
* = $1C20
|
||||
|
||||
src = $fb
|
||||
adlo = $fd
|
||||
adhi = $fe
|
||||
cntl = $02
|
||||
cnth = $03
|
||||
fill = $04
|
||||
|
||||
start:
|
||||
sei
|
||||
lda #$0e
|
||||
sta $ff00 ; bank in RAM $4000-$BFFF, keep I/O for the VDC
|
||||
lda #<nmi
|
||||
sta $0318
|
||||
lda #>nmi
|
||||
sta $0319 ; neutralise NMI (not masked by SEI)
|
||||
|
||||
jsr setregs
|
||||
|
||||
; fill the character matrix VDC $0000 with $FF (solid glyph), 8000 bytes
|
||||
lda #$00
|
||||
sta adlo
|
||||
sta adhi
|
||||
lda #$ff
|
||||
sta fill
|
||||
lda #<8000
|
||||
sta cntl
|
||||
lda #>8000
|
||||
sta cnth
|
||||
jsr fillblk
|
||||
|
||||
; copy 8000 attribute bytes main SRC -> VDC $2000
|
||||
lda #<SRC
|
||||
sta src
|
||||
lda #>SRC
|
||||
sta src+1
|
||||
lda #$00
|
||||
sta adlo
|
||||
lda #$20
|
||||
sta adhi
|
||||
lda #<8000
|
||||
sta cntl
|
||||
lda #>8000
|
||||
sta cnth
|
||||
jsr copyblk
|
||||
|
||||
; enable bitmap + attributes (display the picture)
|
||||
ldx #25
|
||||
lda #$c0
|
||||
jsr vdcw
|
||||
|
||||
loop:
|
||||
jmp loop
|
||||
|
||||
nmi:
|
||||
rti
|
||||
|
||||
; program the VDC display geometry + attribute base (everything except R25)
|
||||
setregs:
|
||||
ldx #9
|
||||
lda #$01
|
||||
jsr vdcw ; R9 = 1 -> 2 scan lines per char row (80x100)
|
||||
ldx #4
|
||||
lda #131
|
||||
jsr vdcw ; R4 vertical total -> (131+1)*2 = 264 lines
|
||||
ldx #5
|
||||
lda #$00
|
||||
jsr vdcw ; R5 vertical total adjust
|
||||
ldx #6
|
||||
lda #100
|
||||
jsr vdcw ; R6 vertical displayed = 100 rows (*2 = 200)
|
||||
ldx #7
|
||||
lda #116
|
||||
jsr vdcw ; R7 vsync position
|
||||
ldx #20
|
||||
lda #$20
|
||||
jsr vdcw ; R20 attribute base high = $2000
|
||||
ldx #21
|
||||
lda #$00
|
||||
jsr vdcw ; R21 attribute base low
|
||||
ldx #26
|
||||
lda #FGBG
|
||||
jsr vdcw ; R26 default fg/bg
|
||||
ldx #10
|
||||
lda #$20
|
||||
jsr vdcw ; R10 cursor off (bit5) - hide the blinking cursor
|
||||
ldx #12
|
||||
lda #$00
|
||||
jsr vdcw ; R12 display start high
|
||||
ldx #13
|
||||
lda #$00
|
||||
jsr vdcw ; R13 display start low
|
||||
rts
|
||||
|
||||
; fill cnt bytes = (fill) into VDC RAM from adhi/adlo (explicit address per byte)
|
||||
fillblk:
|
||||
ldx #18
|
||||
lda adhi
|
||||
jsr vdcw
|
||||
ldx #19
|
||||
lda adlo
|
||||
jsr vdcw
|
||||
ldx #31
|
||||
lda fill
|
||||
jsr vdcw
|
||||
inc adlo
|
||||
bne fb1
|
||||
inc adhi
|
||||
fb1:
|
||||
lda cntl
|
||||
bne fb2
|
||||
dec cnth
|
||||
fb2:
|
||||
dec cntl
|
||||
lda cntl
|
||||
ora cnth
|
||||
bne fillblk
|
||||
rts
|
||||
|
||||
; copy cnt bytes from (src) into VDC RAM from adhi/adlo (explicit address per byte)
|
||||
copyblk:
|
||||
ldx #18
|
||||
lda adhi
|
||||
jsr vdcw
|
||||
ldx #19
|
||||
lda adlo
|
||||
jsr vdcw
|
||||
ldx #31
|
||||
ldy #$00
|
||||
lda (src),y
|
||||
jsr vdcw
|
||||
inc src
|
||||
bne cb1
|
||||
inc src+1
|
||||
cb1:
|
||||
inc adlo
|
||||
bne cb2
|
||||
inc adhi
|
||||
cb2:
|
||||
lda cntl
|
||||
bne cb3
|
||||
dec cnth
|
||||
cb3:
|
||||
dec cntl
|
||||
lda cntl
|
||||
ora cnth
|
||||
bne copyblk
|
||||
rts
|
||||
|
||||
; write A to VDC register X
|
||||
vdcw:
|
||||
stx $d600
|
||||
vw:
|
||||
bit $d600
|
||||
bpl vw
|
||||
sta $d601
|
||||
rts
|
||||
92
lenser/c128/viewer/hicolor.s
Normal file
92
lenser/c128/viewer/hicolor.s
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
; Commodore 128 VDC (8563) 640x200 high-colour viewer (custom character set).
|
||||
;
|
||||
; Uses the VDC's normal 80-column CHARACTER mode (R25 left at the C128 default:
|
||||
; font + attribute, bit7=0) with a per-image custom font. Each 8x8 cell draws
|
||||
; its glyph in a per-cell INK colour (attribute low nibble) over a single GLOBAL
|
||||
; background (VDC register 26 low nibble) -> 640x200 detail with colour.
|
||||
;
|
||||
; Python lays out the whole VDC RAM image in main RAM from $2000 (codes @ $0000,
|
||||
; attributes @ $0800, character set bank 0 @ $2000, bank 1 @ $3000); this copies
|
||||
; the full 16K verbatim into the VDC's own RAM with an explicit address per byte.
|
||||
;
|
||||
; #defines from viewer/assemble.py --
|
||||
; SRC main-RAM address of the VDC image ($2000)
|
||||
; FGBG VDC register 26 value (global background in the low nibble)
|
||||
|
||||
* = $1C20
|
||||
src = $fb
|
||||
adlo = $fd
|
||||
adhi = $fe
|
||||
cntl = $02
|
||||
cnth = $03
|
||||
|
||||
start:
|
||||
sei
|
||||
lda #$0e
|
||||
sta $ff00 ; bank in RAM $4000-$BFFF, keep I/O for the VDC
|
||||
lda #<nmi
|
||||
sta $0318
|
||||
lda #>nmi
|
||||
sta $0319 ; neutralise NMI (not masked by SEI)
|
||||
|
||||
lda #<SRC ; copy $4000 (16384) bytes main SRC -> VDC $0000
|
||||
sta src
|
||||
lda #>SRC
|
||||
sta src+1
|
||||
lda #$00
|
||||
sta adlo
|
||||
sta adhi
|
||||
lda #$00
|
||||
sta cntl
|
||||
lda #$40
|
||||
sta cnth
|
||||
jsr copyblk
|
||||
|
||||
ldx #26
|
||||
lda #FGBG
|
||||
jsr vdcw ; R26 global background (low nibble)
|
||||
ldx #10
|
||||
lda #$20
|
||||
jsr vdcw ; cursor off
|
||||
; R25 deliberately untouched (C128 default = font + attribute mode)
|
||||
loop:
|
||||
jmp loop
|
||||
nmi:
|
||||
rti
|
||||
|
||||
copyblk:
|
||||
ldx #18
|
||||
lda adhi
|
||||
jsr vdcw
|
||||
ldx #19
|
||||
lda adlo
|
||||
jsr vdcw
|
||||
ldx #31
|
||||
ldy #$00
|
||||
lda (src),y
|
||||
jsr vdcw
|
||||
inc src
|
||||
bne cb1
|
||||
inc src+1
|
||||
cb1:
|
||||
inc adlo
|
||||
bne cb2
|
||||
inc adhi
|
||||
cb2:
|
||||
lda cntl
|
||||
bne cb3
|
||||
dec cnth
|
||||
cb3:
|
||||
dec cntl
|
||||
lda cntl
|
||||
ora cnth
|
||||
bne copyblk
|
||||
rts
|
||||
|
||||
vdcw:
|
||||
stx $d600
|
||||
vw:
|
||||
bit $d600
|
||||
bpl vw
|
||||
sta $d601
|
||||
rts
|
||||
211
lenser/c128/viewer/slideshow.s
Normal file
211
lenser/c128/viewer/slideshow.s
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
; Commodore 128 VDC (8563) slideshow viewer -- 640x200 high-colour mode.
|
||||
;
|
||||
; Steps through NIMAGES VDC images named "00".."NN" on the disk, each a 16K VDC
|
||||
; RAM image (codes/attributes/font, the same body the single hicolor/mono viewer
|
||||
; copies). For each slide it KERNAL-loads the file into RAM bank 0 at $4000,
|
||||
; copies the 16K verbatim into the VDC's own RAM, sets the global background
|
||||
; (R26) from the per-image ss_fgbg table, then waits (key / seconds / both)
|
||||
; before advancing. Boots via RUN"PIC" -> the BASIC stub SYSes here.
|
||||
;
|
||||
; #defines from the build wrapper --
|
||||
; WAITMODE 1 key / 2 seconds / 3 both WAITSECS timeout seconds
|
||||
; JIFFYPS 50 PAL / 60 NTSC NIMAGES image count
|
||||
; LOOPFLAG 1 wrap at end, 0 stop
|
||||
; ss_fgbg (one byte per image, VDC R26 background) is appended by the wrapper.
|
||||
|
||||
* = $1C20
|
||||
|
||||
src = $fb
|
||||
adlo = $fd
|
||||
adhi = $fe
|
||||
cntl = $02
|
||||
cnth = $03
|
||||
|
||||
start:
|
||||
lda #$0e
|
||||
sta $ff00 ; KERNAL + I/O + RAM $4000-$BFFF all visible
|
||||
lda #$00
|
||||
sta $9d ; suppress KERNAL LOAD messages
|
||||
sta ssidx
|
||||
|
||||
mainloop:
|
||||
jsr namebuild
|
||||
|
||||
; ---- C128 LOAD "NN",8,1 into RAM bank 0 at $4000 ----
|
||||
lda #$00
|
||||
ldx #$00
|
||||
jsr $ff68 ; SETBNK (load bank 0, name bank 0)
|
||||
lda #2
|
||||
ldx #<ssname
|
||||
ldy #>ssname
|
||||
jsr $ffbd ; SETNAM
|
||||
lda #1
|
||||
ldx #8
|
||||
ldy #1
|
||||
jsr $ffba ; SETLFS (secondary 1 = file's own address)
|
||||
lda #0
|
||||
jsr $ffd5 ; LOAD
|
||||
|
||||
; ---- copy 16384 bytes from $4000 -> VDC $0000 ----
|
||||
lda #$00
|
||||
sta src
|
||||
lda #$40
|
||||
sta src+1
|
||||
lda #$00
|
||||
sta adlo
|
||||
sta adhi
|
||||
sta cntl
|
||||
lda #$40
|
||||
sta cnth
|
||||
jsr copyblk
|
||||
|
||||
; ---- per-image global background (R26) + cursor off (R10) ----
|
||||
ldx ssidx
|
||||
lda ss_fgbg,x
|
||||
ldx #26
|
||||
jsr vdcw
|
||||
ldx #10
|
||||
lda #$20
|
||||
jsr vdcw
|
||||
|
||||
; ---- wait for the slide's dwell ----
|
||||
jsr ss_wait
|
||||
|
||||
; ---- advance ----
|
||||
inc ssidx
|
||||
lda ssidx
|
||||
cmp #NIMAGES
|
||||
bcc mainloop
|
||||
#if LOOPFLAG == 1
|
||||
lda #$00
|
||||
sta ssidx
|
||||
jmp mainloop
|
||||
#else
|
||||
rts
|
||||
#endif
|
||||
|
||||
; --------------------------------------------------------------------------- ;
|
||||
; wait -- key / seconds / both, KERNAL GETIN + jiffy clock ($a0-$a2, $a2 = LSB)
|
||||
; (reuses $fb-$fe as 16-bit scratch now the copy is done)
|
||||
cv_t0 = $fb
|
||||
cv_el = $fd
|
||||
|
||||
ss_wait:
|
||||
#if WAITMODE == 1
|
||||
ss_drain:
|
||||
jsr $ffe4
|
||||
bne ss_drain ; flush leftover keys
|
||||
ss_k:
|
||||
jsr $ffe4
|
||||
beq ss_k
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 2
|
||||
lda $a2
|
||||
sta cv_t0
|
||||
lda $a1
|
||||
sta cv_t0+1
|
||||
ss_s:
|
||||
sec
|
||||
lda $a2
|
||||
sbc cv_t0
|
||||
sta cv_el
|
||||
lda $a1
|
||||
sbc cv_t0+1
|
||||
sta cv_el+1
|
||||
lda cv_el+1
|
||||
cmp #>(WAITSECS*JIFFYPS)
|
||||
bcc ss_s
|
||||
bne ss_sd
|
||||
lda cv_el
|
||||
cmp #<(WAITSECS*JIFFYPS)
|
||||
bcc ss_s
|
||||
ss_sd:
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 3
|
||||
ss_drain:
|
||||
jsr $ffe4
|
||||
bne ss_drain
|
||||
lda $a2
|
||||
sta cv_t0
|
||||
lda $a1
|
||||
sta cv_t0+1
|
||||
ss_b:
|
||||
jsr $ffe4
|
||||
bne ss_bd ; any key ends the slide
|
||||
sec
|
||||
lda $a2
|
||||
sbc cv_t0
|
||||
sta cv_el
|
||||
lda $a1
|
||||
sbc cv_t0+1
|
||||
sta cv_el+1
|
||||
lda cv_el+1
|
||||
cmp #>(WAITSECS*JIFFYPS)
|
||||
bcc ss_b
|
||||
bne ss_bd
|
||||
lda cv_el
|
||||
cmp #<(WAITSECS*JIFFYPS)
|
||||
bcc ss_b
|
||||
ss_bd:
|
||||
rts
|
||||
#endif
|
||||
|
||||
; build the 2-char filename "NN" (PETSCII) from ssidx (0..99)
|
||||
namebuild:
|
||||
lda ssidx
|
||||
ldx #$2f
|
||||
sec
|
||||
nb_ten:
|
||||
inx
|
||||
sbc #10
|
||||
bcs nb_ten
|
||||
adc #10
|
||||
ora #$30
|
||||
sta ssname+1
|
||||
txa
|
||||
sta ssname
|
||||
rts
|
||||
|
||||
; copy cnth/cntl bytes from (src) to VDC RAM starting at adhi/adlo
|
||||
copyblk:
|
||||
ldx #18
|
||||
lda adhi
|
||||
jsr vdcw
|
||||
ldx #19
|
||||
lda adlo
|
||||
jsr vdcw
|
||||
ldx #31
|
||||
ldy #$00
|
||||
lda (src),y
|
||||
jsr vdcw
|
||||
inc src
|
||||
bne cb1
|
||||
inc src+1
|
||||
cb1:
|
||||
inc adlo
|
||||
bne cb2
|
||||
inc adhi
|
||||
cb2:
|
||||
lda cntl
|
||||
bne cb3
|
||||
dec cnth
|
||||
cb3:
|
||||
dec cntl
|
||||
lda cntl
|
||||
ora cnth
|
||||
bne copyblk
|
||||
rts
|
||||
|
||||
vdcw:
|
||||
stx $d600
|
||||
vw:
|
||||
bit $d600
|
||||
bpl vw
|
||||
sta $d601
|
||||
rts
|
||||
|
||||
ssidx: .byte 0
|
||||
ssname: .byte $30,$30
|
||||
; ss_fgbg table (one byte per image) appended by the build wrapper
|
||||
Loading…
Add table
Add a link
Reference in a new issue