First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
0
lenser/c16/__init__.py
Normal file
0
lenser/c16/__init__.py
Normal file
19
lenser/c16/convert/__init__.py
Normal file
19
lenser/c16/convert/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Commodore 16 (TED) conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import imageprep
|
||||
from . import hires, mono
|
||||
|
||||
_MODULES = {"hires": hires, "mono": mono}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="hires", palette_name="ted",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None,
|
||||
base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES.get(mode, hires)
|
||||
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)
|
||||
93
lenser/c16/convert/hires.py
Normal file
93
lenser/c16/convert/hires.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""C16 TED hires bitmap mode: 320x200, two colours per 8x8 cell.
|
||||
|
||||
Unlike the VIC-II, each of the two per-cell colours may be any of the TED's 128
|
||||
colours. The colours are stored across two 1K matrices (see MAME's mos7360
|
||||
draw_bitmap):
|
||||
ch byte (video base + $400): high nibble = fg hue, low nibble = bg hue
|
||||
attr byte (video base): bits 0-2 = fg luminance, bits 4-6 = bg lum
|
||||
A bitmap bit of 1 selects the foreground colour, 0 the background.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert import base
|
||||
from .. import palette as tedpal
|
||||
|
||||
WIDTH, HEIGHT = 320, 200
|
||||
CELL_W, CELL_H = 8, 8
|
||||
PIXEL_ASPECT = 1.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ted", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = tedpal.palette_lab()
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
cells, rows, cols = base.cells_lab(img_lab, CELL_W, CELL_H)
|
||||
dist = base.cell_distance(cells, plab)
|
||||
sets = _select_pairs(dist)
|
||||
|
||||
allowed = base.per_pixel_allowed(sets, rows, cols, CELL_W, CELL_H, HEIGHT, WIDTH)
|
||||
index_image = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint16)
|
||||
|
||||
bitmap, attr, ch = _encode(index_image, sets, rows, cols)
|
||||
payload = bytes(bitmap) + bytes(attr) + bytes(ch)
|
||||
|
||||
prgb = tedpal.get_palette().astype(np.uint8)
|
||||
return base.Conversion(
|
||||
mode="hires", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=index_image, data=payload, viewer="c16",
|
||||
preview_rgb=prgb[index_image],
|
||||
error=base.perceptual_error(index_image, img_lab, plab),
|
||||
meta={"palette": "ted", "dither": dither_mode, "border": 0},
|
||||
)
|
||||
|
||||
|
||||
def _select_pairs(dist, k=16):
|
||||
"""Per-cell best 2 colours from the 128-colour TED palette.
|
||||
|
||||
A full C(128,2) search is 8128 combos/cell; instead we restrict each cell to
|
||||
its ``k`` best single colours (the optimal pair is almost always among them)
|
||||
and search only C(k,2) pairs -- ~100x faster with effectively identical
|
||||
results. dist: (n_cells, P, 128) squared CIELAB distances.
|
||||
"""
|
||||
n_cells = dist.shape[0]
|
||||
single = dist.sum(1) # (n_cells, 128)
|
||||
cand = np.argsort(single, axis=1)[:, :k] # (n_cells, k)
|
||||
dist_c = np.take_along_axis(dist, cand[:, None, :], axis=2) # (n_cells, P, k)
|
||||
best_err = np.full(n_cells, np.inf)
|
||||
best = np.zeros((n_cells, 2), dtype=np.int64)
|
||||
for i in range(k):
|
||||
for j in range(i + 1, k):
|
||||
m = np.minimum(dist_c[:, :, i], dist_c[:, :, j]).sum(1)
|
||||
upd = m < best_err
|
||||
best_err[upd] = m[upd]
|
||||
best[upd, 0] = cand[upd, i]
|
||||
best[upd, 1] = cand[upd, j]
|
||||
return best
|
||||
|
||||
|
||||
def _encode(index_image, sets, rows, cols):
|
||||
"""Return (bitmap 8000, attr 1000, ch 1000) for the TED hires layout."""
|
||||
bitmap = np.zeros(8000, dtype=np.uint8)
|
||||
attr = np.zeros(1000, dtype=np.uint8)
|
||||
ch = np.zeros(1000, dtype=np.uint8)
|
||||
for cr in range(rows):
|
||||
for cc in range(cols):
|
||||
ci = cr * cols + cc
|
||||
bg, fg = int(sets[ci, 0]), int(sets[ci, 1])
|
||||
fg_hue, fg_lum = fg & 0x0F, (fg >> 4) & 0x07
|
||||
bg_hue, bg_lum = bg & 0x0F, (bg >> 4) & 0x07
|
||||
ch[ci] = (fg_hue << 4) | bg_hue
|
||||
attr[ci] = (bg_lum << 4) | fg_lum
|
||||
base_addr = cr * 320 + cc * 8
|
||||
block = index_image[cr * 8:cr * 8 + 8, cc * 8:cc * 8 + 8]
|
||||
for r in range(8):
|
||||
row = block[r]
|
||||
byte = 0
|
||||
for x in range(8):
|
||||
byte = (byte << 1) | (1 if int(row[x]) == fg else 0)
|
||||
bitmap[base_addr + r] = byte
|
||||
return bitmap, attr, ch
|
||||
35
lenser/c16/convert/mono.py
Normal file
35
lenser/c16/convert/mono.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""C16 TED monochrome: 320x200, restricted to the TED's neutral grey ramp (8
|
||||
luminance levels of hue 1, plus black) for smooth greyscale. ``--mono-base``
|
||||
tints it toward one hue (black -> hue -> white). Two greys per 8x8 cell, packed
|
||||
into the same TED hires layout as the colour mode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...convert import base
|
||||
from .. import palette as tedpal
|
||||
from . import hires
|
||||
|
||||
WIDTH, HEIGHT = hires.WIDTH, hires.HEIGHT
|
||||
CELL_W, CELL_H = hires.CELL_W, hires.CELL_H
|
||||
PIXEL_ASPECT = hires.PIXEL_ASPECT
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ted", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = tedpal.palette_lab()
|
||||
prgb = tedpal.get_palette().astype(np.uint8)
|
||||
|
||||
ramp = base.luminance_ramp(plab, tedpal.GREYS, base_color)
|
||||
idx, sets, rows, cols, err = base.mono_render(
|
||||
img_rgb, plab, ramp, WIDTH, HEIGHT, CELL_W, CELL_H, dither_mode, n_free=2)
|
||||
|
||||
bitmap, attr, ch = hires._encode(idx, sets, rows, cols)
|
||||
payload = bytes(bitmap) + bytes(attr) + bytes(ch)
|
||||
|
||||
return base.Conversion(
|
||||
mode="mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=payload, viewer="c16",
|
||||
preview_rgb=prgb[idx], error=err,
|
||||
meta={"palette": "ted", "dither": dither_mode, "border": 0},
|
||||
)
|
||||
16
lenser/c16/exporter.py
Normal file
16
lenser/c16/exporter.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Build a Commodore 16 .prg (loaded + run via MAME quickload / on real HW)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from .viewer import assemble
|
||||
|
||||
_EXTS = (".prg", ".p00")
|
||||
|
||||
|
||||
def export_prg(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="ntsc"):
|
||||
if not output_path.lower().endswith(_EXTS):
|
||||
output_path += ".prg"
|
||||
prg = assemble.build_prg(bytes(conv.data))
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(prg)
|
||||
return output_path
|
||||
62
lenser/c16/palette.py
Normal file
62
lenser/c16/palette.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Commodore 16 / Plus4 TED (7360/8360) colour palette.
|
||||
|
||||
The TED produces 128 colours arranged as 8 luminance levels x 16 hues. A colour
|
||||
value is 7-bit: bits 0-3 = hue (0-15), bits 4-6 = luminance (0-7), so the palette
|
||||
index is ``(luminance << 4) | hue``. Hue 0 is (near) black at every luminance and
|
||||
hue 1 is the neutral grey ramp, so true greys are available too.
|
||||
|
||||
RGB values are taken verbatim from MAME's ``mos7360`` PALETTE_MOS table so the
|
||||
encoder matches what the emulator renders.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab
|
||||
|
||||
# 128 entries in (luminance<<4)|hue order, copied from MAME's mos7360.cpp.
|
||||
TED = np.array([
|
||||
(0x06, 0x01, 0x03), (0x2b, 0x2b, 0x2b), (0x67, 0x0e, 0x0f), (0x00, 0x3f, 0x42),
|
||||
(0x57, 0x00, 0x6d), (0x00, 0x4e, 0x00), (0x19, 0x1c, 0x94), (0x38, 0x38, 0x00),
|
||||
(0x56, 0x20, 0x00), (0x4b, 0x28, 0x00), (0x16, 0x48, 0x00), (0x69, 0x07, 0x2f),
|
||||
(0x00, 0x46, 0x26), (0x06, 0x2a, 0x80), (0x2a, 0x14, 0x9b), (0x0b, 0x49, 0x00),
|
||||
(0x00, 0x03, 0x02), (0x3d, 0x3d, 0x3d), (0x75, 0x1e, 0x20), (0x00, 0x50, 0x4f),
|
||||
(0x6a, 0x10, 0x78), (0x04, 0x5c, 0x00), (0x2a, 0x2a, 0xa3), (0x4c, 0x47, 0x00),
|
||||
(0x69, 0x2f, 0x00), (0x59, 0x38, 0x00), (0x26, 0x56, 0x00), (0x75, 0x15, 0x41),
|
||||
(0x00, 0x58, 0x3d), (0x15, 0x3d, 0x8f), (0x39, 0x22, 0xae), (0x19, 0x59, 0x00),
|
||||
(0x00, 0x03, 0x04), (0x42, 0x42, 0x42), (0x7b, 0x28, 0x20), (0x02, 0x56, 0x59),
|
||||
(0x6f, 0x1a, 0x82), (0x0a, 0x65, 0x09), (0x30, 0x34, 0xa7), (0x50, 0x51, 0x00),
|
||||
(0x6e, 0x36, 0x00), (0x65, 0x40, 0x00), (0x2c, 0x5c, 0x00), (0x7d, 0x1e, 0x45),
|
||||
(0x01, 0x61, 0x45), (0x1c, 0x45, 0x99), (0x42, 0x2d, 0xad), (0x1d, 0x62, 0x00),
|
||||
(0x05, 0x00, 0x02), (0x56, 0x55, 0x5a), (0x90, 0x3c, 0x3b), (0x17, 0x6d, 0x72),
|
||||
(0x87, 0x2d, 0x99), (0x1f, 0x7b, 0x15), (0x46, 0x49, 0xc1), (0x66, 0x63, 0x00),
|
||||
(0x84, 0x4c, 0x0d), (0x73, 0x55, 0x00), (0x40, 0x72, 0x00), (0x91, 0x33, 0x5e),
|
||||
(0x19, 0x74, 0x5c), (0x32, 0x59, 0xae), (0x59, 0x3f, 0xc3), (0x32, 0x76, 0x00),
|
||||
(0x02, 0x01, 0x06), (0x84, 0x7e, 0x85), (0xbb, 0x67, 0x68), (0x45, 0x96, 0x96),
|
||||
(0xaf, 0x58, 0xc3), (0x4a, 0xa7, 0x3e), (0x73, 0x73, 0xec), (0x92, 0x8d, 0x11),
|
||||
(0xaf, 0x78, 0x32), (0xa1, 0x80, 0x20), (0x6c, 0x9e, 0x12), (0xba, 0x5f, 0x89),
|
||||
(0x46, 0x9f, 0x83), (0x61, 0x85, 0xdd), (0x84, 0x6c, 0xef), (0x5d, 0xa3, 0x29),
|
||||
(0x02, 0x00, 0x0a), (0xb2, 0xac, 0xb3), (0xe9, 0x92, 0x92), (0x6c, 0xc3, 0xc1),
|
||||
(0xd9, 0x86, 0xf0), (0x79, 0xd1, 0x76), (0x9d, 0xa1, 0xff), (0xbd, 0xbe, 0x40),
|
||||
(0xdc, 0xa2, 0x61), (0xd1, 0xa9, 0x4c), (0x93, 0xc8, 0x3d), (0xe9, 0x8a, 0xb1),
|
||||
(0x6f, 0xcd, 0xab), (0x8a, 0xb4, 0xff), (0xb2, 0x9a, 0xff), (0x88, 0xcb, 0x59),
|
||||
(0x02, 0x00, 0x0a), (0xc7, 0xca, 0xc9), (0xff, 0xac, 0xac), (0x85, 0xd8, 0xe0),
|
||||
(0xf3, 0x9c, 0xff), (0x92, 0xea, 0x8a), (0xb7, 0xba, 0xff), (0xd6, 0xd3, 0x5b),
|
||||
(0xf3, 0xbe, 0x79), (0xe6, 0xc5, 0x65), (0xb0, 0xe0, 0x57), (0xff, 0xa4, 0xcf),
|
||||
(0x89, 0xe5, 0xc8), (0xa4, 0xca, 0xff), (0xca, 0xb3, 0xff), (0xa2, 0xe5, 0x7a),
|
||||
(0x01, 0x01, 0x01), (0xff, 0xff, 0xff), (0xff, 0xf6, 0xf2), (0xd1, 0xff, 0xff),
|
||||
(0xff, 0xe9, 0xff), (0xdb, 0xff, 0xd3), (0xfd, 0xff, 0xff), (0xff, 0xff, 0xa3),
|
||||
(0xff, 0xff, 0xc1), (0xff, 0xff, 0xb2), (0xfc, 0xff, 0xa2), (0xff, 0xee, 0xff),
|
||||
(0xd1, 0xff, 0xff), (0xeb, 0xff, 0xff), (0xff, 0xf8, 0xff), (0xed, 0xff, 0xbc),
|
||||
], dtype=np.float64)
|
||||
|
||||
# Neutral grey ramp (hue 1) plus black -- used by the monochrome mode.
|
||||
GREYS = [0, 0x11, 0x21, 0x31, 0x41, 0x51, 0x61, 0x71] # (lum<<4)|1, lum 0..7
|
||||
|
||||
|
||||
def get_palette() -> np.ndarray:
|
||||
return TED
|
||||
|
||||
|
||||
def palette_lab() -> np.ndarray:
|
||||
return srgb_to_lab(TED)
|
||||
0
lenser/c16/viewer/__init__.py
Normal file
0
lenser/c16/viewer/__init__.py
Normal file
62
lenser/c16/viewer/assemble.py
Normal file
62
lenser/c16/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Assemble the C16 TED viewer with `xa` and build the loadable PRG.
|
||||
|
||||
The PRG loads at the C16 BASIC start ($1001): a BASIC stub (`10 SYS4128`)
|
||||
followed by the 7501 viewer (at $1020), the two colour matrices ($1800 / $1C00)
|
||||
and the bitmap ($2000). MAME's quickload runs it, the stub 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 = 0x1001
|
||||
ML_ORG = 0x1020
|
||||
ATTR_ORG = 0x1800 # luminance matrix (video matrix base)
|
||||
CH_ORG = 0x1C00 # hue matrix (video base | $400)
|
||||
BITMAP_ORG = 0x2000
|
||||
|
||||
# BASIC: 10 SYS4128 ($1020 = 4128) -- bytes as they sit from $1001
|
||||
_STUB = bytes([0x0B, 0x10, 0x0A, 0x00, 0x9E,
|
||||
0x34, 0x31, 0x32, 0x38, 0x00, 0x00, 0x00])
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def _assemble() -> bytes:
|
||||
if not have_xa():
|
||||
raise AssemblerError("The 'xa' (xa65) assembler was not found on PATH.\n"
|
||||
"Install it with: sudo apt install xa65")
|
||||
proc = subprocess.run(["xa", "-o", "/dev/stdout", "viewer.s"],
|
||||
capture_output=True, cwd=VIEWER_DIR)
|
||||
if proc.returncode != 0:
|
||||
raise AssemblerError(f"xa failed:\n{proc.stderr.decode()}")
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def build_prg(payload: bytes) -> bytes:
|
||||
"""payload = bitmap(8000) + attr(1000) + ch(1000); returns the loadable PRG."""
|
||||
bitmap, attr, ch = payload[:8000], payload[8000:9000], payload[9000:10000]
|
||||
code = _assemble()
|
||||
mem = bytearray()
|
||||
mem += _STUB # $1001..
|
||||
mem += b"\x00" * (ML_ORG - BASIC_START - len(mem))
|
||||
mem += code # $1020..
|
||||
if len(mem) > ATTR_ORG - BASIC_START:
|
||||
raise AssemblerError("viewer code overruns the $1800 matrix area")
|
||||
mem += b"\x00" * (ATTR_ORG - BASIC_START - len(mem))
|
||||
mem += attr # $1800..
|
||||
mem += b"\x00" * (CH_ORG - BASIC_START - len(mem))
|
||||
mem += ch # $1C00..
|
||||
mem += b"\x00" * (BITMAP_ORG - BASIC_START - len(mem))
|
||||
mem += bitmap # $2000..
|
||||
return bytes([BASIC_START & 0xFF, BASIC_START >> 8]) + bytes(mem)
|
||||
26
lenser/c16/viewer/viewer.s
Normal file
26
lenser/c16/viewer/viewer.s
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
; Commodore 16 (TED 7360/8360) hires bitmap viewer.
|
||||
;
|
||||
; The whole picture is already laid out in RAM by the PRG (loaded via quickload):
|
||||
; $1800 attribute matrix (luminance) 1000 bytes
|
||||
; $1C00 colour matrix (hue) 1000 bytes
|
||||
; $2000 bitmap 8000 bytes
|
||||
; This just programs the TED for 320x200 hires bitmap mode and holds the display.
|
||||
|
||||
* = $1020
|
||||
|
||||
start:
|
||||
sei
|
||||
lda #$08
|
||||
sta $ff12 ; bitmap base = $2000
|
||||
lda #$18
|
||||
sta $ff14 ; video matrix base = $1800 (hue matrix at $1C00)
|
||||
lda #$00
|
||||
sta $ff15 ; background colour (per-cell in hires; black)
|
||||
lda #$00
|
||||
sta $ff19 ; border / frame colour = black
|
||||
lda #$08
|
||||
sta $ff07 ; MCM off (hires), 40 columns
|
||||
lda #$3b
|
||||
sta $ff06 ; bitmap mode on, display on, 25 rows (set last)
|
||||
loop:
|
||||
jmp loop
|
||||
Loading…
Add table
Add a link
Reference in a new issue