First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
1
lenser/bbc/__init__.py
Normal file
1
lenser/bbc/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""BBC Micro (Model B) image conversion and DFS disk / sideways-ROM export."""
|
||||
17
lenser/bbc/convert/__init__.py
Normal file
17
lenser/bbc/convert/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"""BBC Micro conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
from ... import imageprep
|
||||
from . import mode0, mode1, mode2, mode5, mono
|
||||
|
||||
_MODULES = {"mode0": mode0, "mode1": mode1, "mode2": mode2, "mode5": mode5,
|
||||
"mono": mono}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="mode2", palette_name="bbc",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None, base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES[mode]
|
||||
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/bbc/convert/_common.py
Normal file
72
lenser/bbc/convert/_common.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Shared helpers for the BBC Micro converters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither
|
||||
from ...convert.base import Conversion, perceptual_error, _box_blur
|
||||
from ...palette import srgb_to_lab
|
||||
from .. import palette as bpal
|
||||
|
||||
|
||||
def _choose_physicals(img_lab, n, dither_mode):
|
||||
"""Pick the n physical colours (of 8) that best reproduce the image, then
|
||||
dither once with that palette. Candidates are ranked by a fast vectorised
|
||||
proxy -- the perceptual error of the nearest-colour (un-dithered)
|
||||
reconstruction -- so we do NOT run the slow Floyd dither for all C(8,n)
|
||||
combinations (doing so made the 4-colour modes hang for a minute+); Floyd runs
|
||||
only on the winning palette. Returns (physical_indices, logical_idx, error)."""
|
||||
plab_all = bpal.phys_lab()
|
||||
H, W, _ = img_lab.shape
|
||||
target_blur = _box_blur(img_lab)
|
||||
best_combo, best_score = None, np.inf
|
||||
for combo in itertools.combinations(range(8), n):
|
||||
sub = plab_all[list(combo)]
|
||||
nidx = ((img_lab[:, :, None, :] - sub[None, None]) ** 2).sum(-1).argmin(-1)
|
||||
diff = _box_blur(sub[nidx]) - target_blur
|
||||
score = float(np.sqrt((diff ** 2).sum(-1)).mean())
|
||||
if score < best_score:
|
||||
best_score, best_combo = score, list(combo)
|
||||
sub = plab_all[best_combo]
|
||||
allowed = np.tile(np.arange(n), (H, W, 1))
|
||||
idx = dither.quantize(img_lab, allowed, sub, dither_mode).astype(np.uint8)
|
||||
return best_combo, idx, perceptual_error(idx, img_lab, sub)
|
||||
|
||||
|
||||
def build(img_rgb, *, mode, bbc_mode, ncol, bpp, width, height, base,
|
||||
dither_mode, mono=False):
|
||||
if mono:
|
||||
L = srgb_to_lab(img_rgb)[..., 0]
|
||||
img_lab = np.zeros((height, width, 3))
|
||||
img_lab[..., 0] = L
|
||||
plab = np.zeros((2, 3)); plab[:, 0] = bpal.mono_lab()[:, 0]
|
||||
allowed = np.tile(np.array([0, 1]), (height, width, 1))
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
physicals = [0, 7] # black, white
|
||||
err = perceptual_error(idx, img_lab, plab)
|
||||
prgb = bpal.PHYS[[0, 7]].astype(np.uint8)
|
||||
else:
|
||||
img_lab = srgb_to_lab(img_rgb)
|
||||
if ncol >= 8:
|
||||
physicals = list(range(8))
|
||||
plab = bpal.phys_lab()
|
||||
allowed = np.tile(np.arange(8), (height, width, 1))
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
err = perceptual_error(idx, img_lab, plab)
|
||||
else:
|
||||
physicals, idx, err = _choose_physicals(img_lab, ncol, dither_mode)
|
||||
prgb = bpal.PHYS[physicals].astype(np.uint8)
|
||||
|
||||
data = bpal.pack(idx, width, bpp)
|
||||
preview = prgb[idx]
|
||||
return Conversion(
|
||||
mode=mode, width=width, height=height,
|
||||
pixel_aspect=(4 / 3) / (width / height),
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=base,
|
||||
viewer="bbc", preview_rgb=preview, error=err,
|
||||
meta={"palette": "bbc", "dither": dither_mode, "bbc_mode": bbc_mode,
|
||||
"ncol": ncol, "physicals": physicals, "base": base},
|
||||
)
|
||||
9
lenser/bbc/convert/mode0.py
Normal file
9
lenser/bbc/convert/mode0.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""BBC MODE 0: 640x256, 2-colour (black & white). 20K screen at &3000."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 640, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode0", bbc_mode=0, ncol=2, bpp=1,
|
||||
width=WIDTH, height=HEIGHT, base=0x3000,
|
||||
dither_mode=dither_mode, mono=True)
|
||||
8
lenser/bbc/convert/mode1.py
Normal file
8
lenser/bbc/convert/mode1.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""BBC MODE 1: 320x256, 4-colour (best 4 of 8). 20K screen at &3000."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 320, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode1", bbc_mode=1, ncol=4, bpp=2,
|
||||
width=WIDTH, height=HEIGHT, base=0x3000, dither_mode=dither_mode)
|
||||
8
lenser/bbc/convert/mode2.py
Normal file
8
lenser/bbc/convert/mode2.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""BBC MODE 2: 160x256, 8-colour. 20K screen at &3000."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 160, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode2", bbc_mode=2, ncol=8, bpp=4,
|
||||
width=WIDTH, height=HEIGHT, base=0x3000, dither_mode=dither_mode)
|
||||
8
lenser/bbc/convert/mode5.py
Normal file
8
lenser/bbc/convert/mode5.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""BBC MODE 5: 160x256, 4-colour (best 4 of 8). 10K screen at &5800 (fits a ROM)."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 160, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode5", bbc_mode=5, ncol=4, bpp=2,
|
||||
width=WIDTH, height=HEIGHT, base=0x5800, dither_mode=dither_mode)
|
||||
15
lenser/bbc/convert/mono.py
Normal file
15
lenser/bbc/convert/mono.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""BBC monochrome -- MODE 0's 640x256 black & white, exposed as the standard
|
||||
``mono`` mode for cross-platform parity (tone carried by dithering)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import mode0
|
||||
|
||||
WIDTH, HEIGHT, PIXEL_ASPECT = mode0.WIDTH, mode0.HEIGHT, mode0.PIXEL_ASPECT
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
conv = mode0.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
conv.mode = "mono"
|
||||
return conv
|
||||
24
lenser/bbc/exporter.py
Normal file
24
lenser/bbc/exporter.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Build a BBC Micro DFS disk (.ssd) from a conversion."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import ssd
|
||||
from .viewer.assemble import LOAD_ADDR, build_viewer
|
||||
|
||||
|
||||
def export_ssd(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="pal") -> str:
|
||||
"""Two DFS files: the !BOOT loader (sets mode + palette, *LOADs IMG) and the
|
||||
IMG screen data. Boot option 2 (*RUN !BOOT) so SHIFT+BREAK autostarts it."""
|
||||
if not output_path.lower().endswith(".ssd"):
|
||||
output_path += ".ssd"
|
||||
m = conv.meta
|
||||
viewer = build_viewer(m["bbc_mode"], m["ncol"], m["physicals"], m["base"],
|
||||
display=display, seconds=seconds, video=video)
|
||||
# !BOOT autostarts on SHIFT+BREAK (real hardware); PIC is the same loader
|
||||
# under a name with no '!' so it can be *RUN from a command line / emulator.
|
||||
files = [
|
||||
("!BOOT", LOAD_ADDR, LOAD_ADDR, viewer),
|
||||
("PIC", LOAD_ADDR, LOAD_ADDR, viewer),
|
||||
("IMG", m["base"], m["base"], conv.data),
|
||||
]
|
||||
return ssd.write_ssd(output_path, files, title="8BITLENSER", boot_option=2)
|
||||
70
lenser/bbc/palette.py
Normal file
70
lenser/bbc/palette.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""BBC Micro (Video ULA + 6845) palette and screen packing.
|
||||
|
||||
8 physical colours -- pure digital RGB. Modes map 1/2/4 bits-per-pixel of
|
||||
*logical* colour through a programmable palette (VDU 19) to these physicals.
|
||||
|
||||
Screen memory is character-cell interleaved: 8x8 cells ordered left-to-right then
|
||||
top-to-bottom; within a cell the bytes go by scanline, and each scanline spans
|
||||
1/2/4 bytes (2/4/8-colour). Pixel bits within a byte are interleaved, leftmost
|
||||
pixel in the high bits.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab
|
||||
|
||||
# Physical colours 0..7.
|
||||
PHYS = np.array([
|
||||
(0, 0, 0), # 0 black
|
||||
(255, 0, 0), # 1 red
|
||||
(0, 255, 0), # 2 green
|
||||
(255, 255, 0), # 3 yellow
|
||||
(0, 0, 255), # 4 blue
|
||||
(255, 0, 255), # 5 magenta
|
||||
(0, 255, 255), # 6 cyan
|
||||
(255, 255, 255), # 7 white
|
||||
], dtype=np.float64)
|
||||
|
||||
|
||||
def phys_lab() -> np.ndarray:
|
||||
return srgb_to_lab(PHYS)
|
||||
|
||||
|
||||
def mono_lab() -> np.ndarray:
|
||||
return srgb_to_lab(PHYS[[0, 7]]) # black + white
|
||||
|
||||
|
||||
def _byte_for_pixels(vals, bits_per_pixel):
|
||||
"""Encode the pixels covering one byte into the BBC interleaved layout.
|
||||
Leftmost pixel uses the highest bit of each bit-plane group."""
|
||||
n = len(vals) # pixels per byte (8/4/2)
|
||||
b = 0
|
||||
for bit in range(bits_per_pixel - 1, -1, -1): # high plane first
|
||||
for i, v in enumerate(vals): # left pixel first
|
||||
b = (b << 1) | ((v >> bit) & 1)
|
||||
return b
|
||||
|
||||
|
||||
def pack(idx: np.ndarray, width: int, bits_per_pixel: int) -> bytes:
|
||||
"""Pack a (height, width) logical-colour array into BBC screen bytes.
|
||||
|
||||
The BBC layout is universal: one byte holds ``ppb`` horizontally-adjacent
|
||||
pixels, and 8 consecutive bytes step down the 8 raster lines of that
|
||||
byte-column before moving one byte-column to the right; whole character rows
|
||||
(8 raster lines) then follow top-to-bottom. So
|
||||
addr = char_row*(num_byte_cols*8) + byte_col*8 + raster
|
||||
"""
|
||||
h, w = idx.shape
|
||||
ppb = 8 // bits_per_pixel # pixels per byte
|
||||
num_byte_cols = w // ppb
|
||||
row_stride = num_byte_cols * 8 # bytes per character row
|
||||
out = bytearray((w * h) // ppb)
|
||||
for y in range(h):
|
||||
base = (y // 8) * row_stride + (y % 8)
|
||||
for bc in range(num_byte_cols):
|
||||
x0 = bc * ppb
|
||||
vals = [int(idx[y, x0 + p]) for p in range(ppb)]
|
||||
out[base + bc * 8] = _byte_for_pixels(vals, bits_per_pixel)
|
||||
return bytes(out)
|
||||
72
lenser/bbc/ssd.py
Normal file
72
lenser/bbc/ssd.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Write an Acorn DFS single-sided disk image (.ssd) natively.
|
||||
|
||||
DFS layout: sectors 0-1 are the catalogue; files follow from sector 2, each
|
||||
starting on a 256-byte sector boundary. Addresses are 18-bit (load/exec/length
|
||||
high bits packed into the per-file flag byte). Boot option (*OPT 4,n) lives in
|
||||
sector 1 byte 6 bits 4-5.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
SECTOR = 256
|
||||
TRACKS = 80
|
||||
SECTORS = TRACKS * 10 # 800 sectors = 200K, single sided
|
||||
|
||||
|
||||
class DfsError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _name7(name: str) -> bytes:
|
||||
n = "".join(c for c in name.upper() if 32 <= ord(c) < 127)[:7]
|
||||
return n.ljust(7).encode("ascii")
|
||||
|
||||
|
||||
def build_ssd(files, title="8BITLENSER", boot_option=0) -> bytes:
|
||||
"""files: list of (name, load_addr, exec_addr, data). boot_option 0-3
|
||||
(*OPT 4,n): 0 none, 1 *LOAD, 2 *RUN, 3 *EXEC of the first matching $.!BOOT."""
|
||||
if len(files) > 31:
|
||||
raise DfsError("DFS holds at most 31 files")
|
||||
|
||||
cat0 = bytearray(SECTOR)
|
||||
cat1 = bytearray(SECTOR)
|
||||
t = title.ljust(12)[:12].encode("ascii", "replace")
|
||||
cat0[0:8] = t[0:8]
|
||||
cat1[0:4] = t[8:12]
|
||||
cat1[4] = 0 # cycle number
|
||||
cat1[5] = len(files) * 8 # (#files) * 8
|
||||
cat1[6] = ((SECTORS >> 8) & 0x03) | ((boot_option & 0x03) << 4)
|
||||
cat1[7] = SECTORS & 0xFF
|
||||
|
||||
body = bytearray()
|
||||
start_sector = 2
|
||||
for i, (name, load, exec_, data) in enumerate(files):
|
||||
e = 8 + i * 8
|
||||
cat0[e:e + 7] = _name7(name)
|
||||
cat0[e + 7] = ord("$") # directory '$' (unlocked)
|
||||
length = len(data)
|
||||
cat1[e + 0] = load & 0xFF
|
||||
cat1[e + 1] = (load >> 8) & 0xFF
|
||||
cat1[e + 2] = exec_ & 0xFF
|
||||
cat1[e + 3] = (exec_ >> 8) & 0xFF
|
||||
cat1[e + 4] = length & 0xFF
|
||||
cat1[e + 5] = (length >> 8) & 0xFF
|
||||
cat1[e + 6] = (((exec_ >> 16) & 0x03) << 6 |
|
||||
((length >> 16) & 0x03) << 4 |
|
||||
((load >> 16) & 0x03) << 2 |
|
||||
((start_sector >> 8) & 0x03))
|
||||
cat1[e + 7] = start_sector & 0xFF
|
||||
nsec = (length + SECTOR - 1) // SECTOR
|
||||
body += bytes(data) + bytes((-length) % SECTOR)
|
||||
start_sector += nsec
|
||||
|
||||
img = bytes(cat0) + bytes(cat1) + bytes(body)
|
||||
if len(img) > SECTORS * SECTOR:
|
||||
raise DfsError("files exceed disk capacity")
|
||||
return img + bytes(SECTORS * SECTOR - len(img))
|
||||
|
||||
|
||||
def write_ssd(path: str, files, title="8BITLENSER", boot_option=0) -> str:
|
||||
with open(path, "wb") as f:
|
||||
f.write(build_ssd(files, title, boot_option))
|
||||
return path
|
||||
0
lenser/bbc/viewer/__init__.py
Normal file
0
lenser/bbc/viewer/__init__.py
Normal file
102
lenser/bbc/viewer/assemble.py
Normal file
102
lenser/bbc/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Assemble the BBC 6502 viewer with `xa` and patch in per-image parameters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LOAD_ADDR = 0x1900 # DFS PAGE
|
||||
WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2}
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def _xa(wrapper: str) -> bytes:
|
||||
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)
|
||||
|
||||
|
||||
SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
|
||||
|
||||
|
||||
def build_slideshow_viewer(bbc_mode: int, ncol: int, base: int, palettes,
|
||||
advance: str = "both", seconds: int = 10,
|
||||
loop: bool = True, video: str = "pal") -> bytes:
|
||||
"""Return the multi-image BBC loader (origin $1900).
|
||||
|
||||
``palettes`` is one physical-colour list per image (each truncated/padded to
|
||||
ncol) -- emitted as the ss_pal table the loader indexes by slide; the screen
|
||||
base hex is patched into the OSCLI *LOAD string.
|
||||
"""
|
||||
if not palettes:
|
||||
raise AssemblerError("a slideshow needs at least one image")
|
||||
rate = 60 if video == "ntsc" else 50
|
||||
flat = []
|
||||
for p in palettes:
|
||||
row = list(p[:ncol])
|
||||
flat += row + [0] * (ncol - len(row))
|
||||
table = ",".join(str(b & 0xFF) for b in flat)
|
||||
wrapper = (f"#define WAITMODE {SS_WAITMODE[advance]}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define RATE {rate}\n"
|
||||
f"#define NIMAGES {len(palettes)}\n"
|
||||
f"#define LOOPFLAG {1 if loop else 0}\n"
|
||||
f"#define MODE {bbc_mode}\n"
|
||||
f"#define NCOL {ncol}\n"
|
||||
'#include "slideshow.s"\n'
|
||||
"ss_pal:\n"
|
||||
f" .byte {table}\n")
|
||||
raw = bytearray(_xa(wrapper))
|
||||
off = raw.find(b"LOAD 00 ")
|
||||
if off < 0:
|
||||
raise AssemblerError("could not locate the OSCLI string to patch")
|
||||
raw[off + 8:off + 12] = f"{base:04X}".encode("ascii") # screen-base hex
|
||||
return bytes(raw)
|
||||
|
||||
|
||||
def build_viewer(bbc_mode: int, ncol: int, physicals, base: int,
|
||||
display: str = "forever", seconds: int = 0,
|
||||
video: str = "pal") -> bytes:
|
||||
"""Return the assembled loader (origin $1900) with the mode/palette/screen-base
|
||||
parameters patched in."""
|
||||
waitmode = WAIT_MODES.get(display, 0)
|
||||
rate = 60 if video == "ntsc" else 50 # bbcb is PAL (50 Hz)
|
||||
wrapper = (f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define RATE {rate}\n"
|
||||
'#include "bbc.s"\n')
|
||||
raw = bytearray(_xa(wrapper))
|
||||
|
||||
off = raw.find(b"LOAD IMG ")
|
||||
if off < 0:
|
||||
raise AssemblerError("could not locate the OSCLI string to patch")
|
||||
raw[off + 9:off + 13] = f"{base:04X}".encode("ascii") # screen-base hex
|
||||
raw[off + 14] = bbc_mode & 0xFF # p_mode
|
||||
raw[off + 15] = ncol & 0xFF # p_ncol
|
||||
for i, p in enumerate(physicals[:8]): # p_pal
|
||||
raw[off + 16 + i] = p & 0xFF
|
||||
return bytes(raw)
|
||||
97
lenser/bbc/viewer/bbc.s
Normal file
97
lenser/bbc/viewer/bbc.s
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
; lenser -- BBC Micro viewer (6502), loaded at PAGE (&1900) and *RUN.
|
||||
;
|
||||
; Sets the screen MODE, programmes the logical->physical palette, then *LOADs the
|
||||
; image file "IMG" straight into screen memory, and holds it per the display
|
||||
; option. Parameters come from a fixed block the packager fills in (so the code
|
||||
; is constant); see build_viewer() in assemble.py.
|
||||
;
|
||||
; #defines from the wrapper -- WAITMODE (0 forever, 1 key, 2 seconds), WAITSECS, RATE.
|
||||
;
|
||||
; assembled by bbc/viewer/assemble.py via xa
|
||||
|
||||
OSWRCH = $FFEE
|
||||
OSRDCH = $FFE0
|
||||
OSBYTE = $FFF4
|
||||
OSCLI = $FFF7
|
||||
|
||||
* = $1900
|
||||
start:
|
||||
; ---- VDU 22, mode ----
|
||||
lda #22
|
||||
jsr OSWRCH
|
||||
lda p_mode
|
||||
jsr OSWRCH
|
||||
|
||||
; ---- palette via VDU 19, logical, physical, 0,0,0 for each logical ----
|
||||
ldx #0
|
||||
palloop:
|
||||
cpx p_ncol
|
||||
beq paldone
|
||||
lda #19
|
||||
jsr OSWRCH
|
||||
txa
|
||||
jsr OSWRCH ; logical colour = X
|
||||
lda p_pal,x
|
||||
jsr OSWRCH ; physical
|
||||
lda #0
|
||||
jsr OSWRCH
|
||||
jsr OSWRCH
|
||||
jsr OSWRCH
|
||||
inx
|
||||
bne palloop
|
||||
paldone:
|
||||
|
||||
; ---- hide the text cursor (VDU 23,1,0;0;0;0;) ----
|
||||
lda #23
|
||||
jsr OSWRCH
|
||||
lda #1
|
||||
jsr OSWRCH
|
||||
ldx #8
|
||||
curz:
|
||||
lda #0
|
||||
jsr OSWRCH
|
||||
dex
|
||||
bne curz
|
||||
|
||||
; ---- *LOAD the image straight into screen memory ----
|
||||
ldx #<cmd
|
||||
ldy #>cmd
|
||||
jsr OSCLI
|
||||
|
||||
; ---- hold the picture ----
|
||||
#if WAITMODE == 0
|
||||
hang:
|
||||
jmp hang
|
||||
#endif
|
||||
#if WAITMODE == 1
|
||||
jsr OSRDCH ; block until a key
|
||||
rts ; back to BASIC
|
||||
#endif
|
||||
#if WAITMODE == 2
|
||||
lda #<(WAITSECS*RATE)
|
||||
sta cnt
|
||||
lda #>(WAITSECS*RATE)
|
||||
sta cnt+1
|
||||
swait:
|
||||
lda #19
|
||||
jsr OSBYTE ; OSBYTE 19 = wait for vertical sync
|
||||
lda cnt
|
||||
bne sdec
|
||||
dec cnt+1
|
||||
sdec:
|
||||
dec cnt
|
||||
lda cnt
|
||||
ora cnt+1
|
||||
bne swait
|
||||
rts ; back to BASIC
|
||||
#endif
|
||||
|
||||
cnt: .byte 0,0
|
||||
|
||||
; OSCLI string -- the packager patches in the right screen-base hex
|
||||
cmd: .byte "LOAD IMG ", "0000", 13
|
||||
|
||||
; parameter block -- the packager fills these in
|
||||
p_mode: .byte 0
|
||||
p_ncol: .byte 0
|
||||
p_pal: .byte 0,0,0,0,0,0,0,0
|
||||
174
lenser/bbc/viewer/slideshow.s
Normal file
174
lenser/bbc/viewer/slideshow.s
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
; lenser -- BBC Micro slideshow loader (6502), loaded at PAGE (&1900) and *RUN.
|
||||
;
|
||||
; Sets the screen MODE once, then steps through NIMAGES pictures stored as DFS
|
||||
; files "00".."NN". For each slide it programmes that image's logical->physical
|
||||
; palette (from the ss_pal table), *LOADs the file straight into screen memory,
|
||||
; and waits (key / seconds / both) before advancing.
|
||||
;
|
||||
; #defines from the build wrapper --
|
||||
; WAITMODE 1 key / 2 seconds / 3 both WAITSECS timeout RATE 50/60 fps
|
||||
; NIMAGES image count LOOPFLAG 1 wrap / 0 stop MODE screen mode NCOL colours
|
||||
; The packager patches the screen-base hex into the OSCLI string and appends the
|
||||
; ss_pal table (NCOL physical-colour bytes per image).
|
||||
|
||||
OSWRCH = $FFEE
|
||||
OSRDCH = $FFE0
|
||||
OSBYTE = $FFF4
|
||||
OSCLI = $FFF7
|
||||
|
||||
palptr = $70
|
||||
cnt = $72
|
||||
|
||||
* = $1900
|
||||
start:
|
||||
; ---- VDU 22, mode (once) ----
|
||||
lda #22
|
||||
jsr OSWRCH
|
||||
lda #MODE
|
||||
jsr OSWRCH
|
||||
|
||||
; ---- hide the text cursor (VDU 23,1,0;0;0;0;) ----
|
||||
lda #23
|
||||
jsr OSWRCH
|
||||
lda #1
|
||||
jsr OSWRCH
|
||||
ldx #8
|
||||
curz:
|
||||
lda #0
|
||||
jsr OSWRCH
|
||||
dex
|
||||
bne curz
|
||||
|
||||
lda #0
|
||||
sta ssidx
|
||||
|
||||
ssmain:
|
||||
; ---- palptr = ss_pal + ssidx*NCOL ----
|
||||
lda #<ss_pal
|
||||
sta palptr
|
||||
lda #>ss_pal
|
||||
sta palptr+1
|
||||
ldx ssidx
|
||||
beq setpal
|
||||
pmul:
|
||||
clc
|
||||
lda palptr
|
||||
adc #NCOL
|
||||
sta palptr
|
||||
bcc pm1
|
||||
inc palptr+1
|
||||
pm1:
|
||||
dex
|
||||
bne pmul
|
||||
setpal:
|
||||
; ---- VDU 19, logical, physical, 0,0,0 for each logical colour ----
|
||||
ldx #0
|
||||
pl:
|
||||
cpx #NCOL
|
||||
beq pldone
|
||||
lda #19
|
||||
jsr OSWRCH
|
||||
txa
|
||||
jsr OSWRCH ; logical colour = X
|
||||
txa
|
||||
tay
|
||||
lda (palptr),y
|
||||
jsr OSWRCH ; physical colour
|
||||
lda #0
|
||||
jsr OSWRCH
|
||||
jsr OSWRCH
|
||||
jsr OSWRCH
|
||||
inx
|
||||
bne pl
|
||||
pldone:
|
||||
|
||||
; ---- build the filename digits "NN" into the OSCLI string ----
|
||||
lda ssidx
|
||||
ldx #$2f
|
||||
sec
|
||||
nten:
|
||||
inx
|
||||
sbc #10
|
||||
bcs nten
|
||||
adc #10
|
||||
ora #$30
|
||||
sta cmd+6 ; ones digit
|
||||
txa
|
||||
sta cmd+5 ; tens digit
|
||||
|
||||
; ---- *LOAD NN <base> straight into screen memory ----
|
||||
ldx #<cmd
|
||||
ldy #>cmd
|
||||
jsr OSCLI
|
||||
|
||||
jsr sswait
|
||||
|
||||
inc ssidx
|
||||
lda ssidx
|
||||
cmp #NIMAGES
|
||||
bcc ssmain
|
||||
#if LOOPFLAG == 1
|
||||
lda #0
|
||||
sta ssidx
|
||||
jmp ssmain
|
||||
#else
|
||||
rts ; back to BASIC
|
||||
#endif
|
||||
|
||||
; ---- wait (returns), flushing the keyboard buffer first ----
|
||||
sswait:
|
||||
lda #15
|
||||
ldx #1
|
||||
jsr OSBYTE ; flush input buffers
|
||||
#if WAITMODE == 1
|
||||
jsr OSRDCH ; block until a key
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 2
|
||||
lda #<(WAITSECS*RATE)
|
||||
sta cnt
|
||||
lda #>(WAITSECS*RATE)
|
||||
sta cnt+1
|
||||
sw2:
|
||||
lda #19
|
||||
jsr OSBYTE ; wait for vertical sync (1 frame)
|
||||
lda cnt
|
||||
bne sd2
|
||||
dec cnt+1
|
||||
sd2:
|
||||
dec cnt
|
||||
lda cnt
|
||||
ora cnt+1
|
||||
bne sw2
|
||||
rts
|
||||
#endif
|
||||
#if WAITMODE == 3
|
||||
lda #<(WAITSECS*RATE)
|
||||
sta cnt
|
||||
lda #>(WAITSECS*RATE)
|
||||
sta cnt+1
|
||||
sw3:
|
||||
lda #129
|
||||
ldx #0
|
||||
ldy #0
|
||||
jsr OSBYTE ; INKEY(0) -- poll keyboard, no wait
|
||||
cpy #$ff
|
||||
bne sw3d ; Y != $FF -> a key was pressed
|
||||
lda #19
|
||||
jsr OSBYTE ; wait one frame
|
||||
lda cnt
|
||||
bne sd3
|
||||
dec cnt+1
|
||||
sd3:
|
||||
dec cnt
|
||||
lda cnt
|
||||
ora cnt+1
|
||||
bne sw3
|
||||
sw3d:
|
||||
rts
|
||||
#endif
|
||||
|
||||
ssidx: .byte 0
|
||||
; OSCLI string -- packager patches the 4-hex screen base; loader patches "NN"
|
||||
cmd: .byte "LOAD 00 0000", 13
|
||||
; ss_pal table (NCOL bytes per image) appended by the build wrapper
|
||||
Loading…
Add table
Add a link
Reference in a new issue