First public commit.

This commit is contained in:
The Dust Council 2026-07-03 19:35:35 -07:00
parent 2a48f52979
commit 4bac9d83ed
288 changed files with 18417 additions and 1076 deletions

1
lenser/c128/__init__.py Normal file
View file

@ -0,0 +1 @@
"""Commodore 128 target for lenser (VDC 8563 80-column display)."""

View 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)

View 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"},
)

View 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},
)

View 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
View 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
View 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)

View file

@ -0,0 +1 @@
"""C128 8502/VDC viewer (assembled by xa)."""

View 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
View 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

View 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

View 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