588 lines
24 KiB
Python
588 lines
24 KiB
Python
"""Slideshow: pack several converted images onto one drive image with an
|
|
on-machine viewer that steps through them.
|
|
|
|
A slideshow is locked to a single platform. Each slide carries its own image
|
|
adjustments (the same PrepOptions as a normal conversion) plus its own mode /
|
|
palette / dither, so the queue can freely mix e.g. hires and multicolor on the
|
|
C64. The viewer advances on a keypress, after a number of seconds, or both.
|
|
|
|
This module owns:
|
|
* the data model (SlideItem / Slideshow),
|
|
* conversion of an item to bytes (delegating to platforms.convert),
|
|
* the storage engine that sizes a slideshow against a disk format and refuses
|
|
to overfill it,
|
|
* the C64 build (assemble the slideshow viewer + write the data files).
|
|
|
|
Only disk-image platforms can host a slideshow (a cartridge or snapshot is a
|
|
single fixed payload). C64 is implemented here as the reference; other disk
|
|
platforms register via SLIDESHOW_PLATFORMS as they gain viewers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
from dataclasses import dataclass, field, replace
|
|
|
|
from . import diskimage, imageprep, platforms
|
|
from .convert.base import Conversion
|
|
|
|
# Platforms whose output is a real multi-file filesystem image AND that have a
|
|
# slideshow viewer. C64 first; others are added as their viewers land.
|
|
SLIDESHOW_PLATFORMS = ("c64", "c128", "atari", "bbc", "apple", "iigs", "amiga")
|
|
|
|
# Advance behaviours (how the viewer leaves a slide). "forever" is intentionally
|
|
# absent -- a slideshow that never advances is just a single-image viewer.
|
|
ADVANCE_MODES = ("key", "seconds", "both")
|
|
|
|
|
|
@dataclass
|
|
class SlideItem:
|
|
"""One picture in the queue, with everything needed to reproduce its bytes."""
|
|
source_path: str
|
|
mode: str = "auto"
|
|
palette: str = "colodore"
|
|
dither: str = "atkinson"
|
|
mono_base: str = "grayscale"
|
|
prep: imageprep.PrepOptions = field(default_factory=imageprep.PrepOptions)
|
|
|
|
|
|
@dataclass
|
|
class Slideshow:
|
|
platform: str = "c64"
|
|
disk_format: str = "d64"
|
|
advance: str = "both"
|
|
seconds: int = 10
|
|
loop: bool = True
|
|
items: list[SlideItem] = field(default_factory=list)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# conversion
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def convert_item(show: Slideshow, item: SlideItem,
|
|
intensive: bool = False) -> Conversion:
|
|
"""Convert one slide to a Conversion (bytes + viewer key + load address)."""
|
|
return platforms.convert(show.platform, item.source_path, item.mode,
|
|
item.palette, item.dither, intensive, item.prep,
|
|
item.mono_base)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# storage engine
|
|
# --------------------------------------------------------------------------- #
|
|
# Each disk platform maps to a "filesystem" capacity model. CBM-DOS (c1541 d64/
|
|
# d71/d81, shared by the C64 and C128) counts 254-byte data blocks and caps the
|
|
# directory; other filesystems get their own model as they are added.
|
|
_FS_CBM = "cbm"
|
|
_FS_ATR = "atr"
|
|
_FS_DFS = "dfs"
|
|
_FS_APPLE = "apple"
|
|
_FS_IIGS = "iigs"
|
|
_FS_AMIGA = "amiga"
|
|
_PLATFORM_FS = {
|
|
"c64": _FS_CBM,
|
|
"c128": _FS_CBM,
|
|
"atari": _FS_ATR,
|
|
"bbc": _FS_DFS,
|
|
"apple": _FS_APPLE,
|
|
"iigs": _FS_IIGS,
|
|
"amiga": _FS_AMIGA,
|
|
}
|
|
|
|
# Per-format usable capacity (254-byte blocks) and directory-entry cap for CBM-DOS.
|
|
_CBM_DIR_CAP = {"d64": 144, "d71": 144, "d81": 296}
|
|
|
|
# Atari single-density ATR: 720 x 128-byte sectors. The self-booting viewer
|
|
# reserves the first few sectors (fixed) and the raw images follow it; there is
|
|
# no filesystem/directory, so the only cap is the sector count.
|
|
_ATR_TOTAL_SECTORS = 720
|
|
_ATR_BOOT_SECTORS = 8
|
|
_ATR_BLOCK = 128
|
|
|
|
# Acorn DFS single-sided: 800 x 256-byte sectors, sectors 0-1 are the catalogue,
|
|
# at most 31 files.
|
|
_DFS_TOTAL_SECTORS = 800
|
|
_DFS_CATALOGUE_SECTORS = 2
|
|
_DFS_BLOCK = 256
|
|
_DFS_MAX_FILES = 31
|
|
|
|
# Apple II HGR slideshow loads every image into RAM at once ($4000-$BFFF), so the
|
|
# binding limit is that 128-page (32K) buffer -- 4 HGR images of 32 pages each --
|
|
# not the disk. Measured in 256-byte pages.
|
|
_APPLE_BLOCK = 256
|
|
_APPLE_BUFFER_PAGES = 0x80 # $4000-$BFFF
|
|
|
|
# Apple IIgs: a 5.25" ProDOS-order .dsk (35 x 16 x 256 = 560 sectors). Each 32K
|
|
# SHR image is read into bank 0 then banked to extended RAM; the binding limit is
|
|
# the small disk (boot + stage2 + N*128 sectors).
|
|
_IIGS_TOTAL_SECTORS = 560
|
|
_IIGS_VIEWER_SECTORS = 4 # boot + stage2 (+ slack)
|
|
_IIGS_BLOCK = 256
|
|
_IIGS_MAX_IMAGES = 4
|
|
|
|
# Amiga: the slideshow loads every image into chip RAM ($20000-$7FFFF) at once
|
|
# and cycles them there, so the 384K RAM region (not the 880K floppy) is the
|
|
# binding limit. Measured in 512-byte sectors.
|
|
_AMIGA_BLOCK = 512
|
|
_AMIGA_RAM_SECTORS = (0x80000 - 0x20000) // 512 # 768
|
|
|
|
|
|
def image_nbytes(conv: Conversion) -> int:
|
|
"""On-disk byte size of one slide's picture data (Amiga's conv.data is a dict
|
|
of planes/colours, so its size is the assembled copper+bitplanes blob)."""
|
|
if isinstance(conv.data, dict):
|
|
from .amiga.viewer import image_blob
|
|
d = conv.data
|
|
return len(image_blob(d["planes"], d["colors"], d["nplanes"], d["ham"]))
|
|
return len(conv.data)
|
|
|
|
|
|
def _platform_fs(platform: str) -> str:
|
|
fs = _PLATFORM_FS.get(platform)
|
|
if fs is None:
|
|
raise NotImplementedError(f"slideshow storage not implemented for {platform}")
|
|
return fs
|
|
|
|
|
|
@dataclass
|
|
class Budget:
|
|
used_blocks: int
|
|
total_blocks: int
|
|
files: int
|
|
file_cap: int
|
|
fits: bool
|
|
reason: str = ""
|
|
block_size: int = 254 # bytes per block/sector (CBM 254, ATR 128)
|
|
|
|
@property
|
|
def used_bytes(self) -> int:
|
|
return self.used_blocks * self.block_size
|
|
|
|
@property
|
|
def total_bytes(self) -> int:
|
|
return self.total_blocks * self.block_size
|
|
|
|
|
|
def _cbm_blocks(prg_len: int) -> int:
|
|
"""CBM blocks a PRG of prg_len bytes (incl. its 2-byte load address) uses."""
|
|
return max(1, math.ceil(prg_len / 254))
|
|
|
|
|
|
def item_blocks(platform: str, fmt: str, data_len: int) -> int:
|
|
"""Blocks/sectors one picture file occupies."""
|
|
fs = _platform_fs(platform)
|
|
if fs == _FS_CBM:
|
|
return _cbm_blocks(data_len + 2) # + 2-byte PRG load address
|
|
if fs == _FS_ATR:
|
|
return max(1, math.ceil(data_len / _ATR_BLOCK))
|
|
if fs == _FS_DFS:
|
|
return max(1, math.ceil(data_len / _DFS_BLOCK))
|
|
if fs == _FS_APPLE:
|
|
return max(1, math.ceil(data_len / _APPLE_BLOCK))
|
|
if fs == _FS_IIGS:
|
|
return max(1, math.ceil(data_len / _IIGS_BLOCK))
|
|
if fs == _FS_AMIGA:
|
|
return max(1, math.ceil(data_len / _AMIGA_BLOCK))
|
|
raise NotImplementedError(f"slideshow storage not implemented for {platform}")
|
|
|
|
|
|
def viewer_blocks(platform: str, fmt: str, viewer_len: int) -> int:
|
|
"""Blocks/sectors the boot viewer itself occupies."""
|
|
fs = _platform_fs(platform)
|
|
if fs == _FS_CBM:
|
|
return _cbm_blocks(viewer_len)
|
|
if fs == _FS_ATR:
|
|
return _ATR_BOOT_SECTORS # fixed boot-sector reservation
|
|
if fs == _FS_DFS:
|
|
return 2 * max(1, math.ceil(viewer_len / _DFS_BLOCK)) # !BOOT + PIC copies
|
|
if fs == _FS_APPLE:
|
|
return 0 # boot loader lives at $0800, not the buffer
|
|
if fs == _FS_IIGS:
|
|
return _IIGS_VIEWER_SECTORS
|
|
if fs == _FS_AMIGA:
|
|
return 0 # boot block isn't in the image RAM region
|
|
raise NotImplementedError(f"slideshow storage not implemented for {platform}")
|
|
|
|
|
|
def capacity_blocks(platform: str, fmt: str) -> int:
|
|
fs = _platform_fs(platform)
|
|
if fs == _FS_CBM:
|
|
if fmt not in diskimage.BLOCKS_FREE:
|
|
raise ValueError(f"{fmt} is not a slideshow disk format for {platform}")
|
|
return diskimage.BLOCKS_FREE[fmt]
|
|
if fs == _FS_ATR:
|
|
return _ATR_TOTAL_SECTORS
|
|
if fs == _FS_DFS:
|
|
return _DFS_TOTAL_SECTORS - _DFS_CATALOGUE_SECTORS
|
|
if fs == _FS_APPLE:
|
|
return _APPLE_BUFFER_PAGES # RAM buffer, not the disk
|
|
if fs == _FS_IIGS:
|
|
return _IIGS_TOTAL_SECTORS
|
|
if fs == _FS_AMIGA:
|
|
return _AMIGA_RAM_SECTORS
|
|
raise NotImplementedError(f"slideshow storage not implemented for {platform}")
|
|
|
|
|
|
def dir_cap(platform: str, fmt: str) -> int:
|
|
fs = _platform_fs(platform)
|
|
if fs == _FS_CBM:
|
|
return _CBM_DIR_CAP.get(fmt, 144)
|
|
if fs == _FS_ATR:
|
|
return _ATR_TOTAL_SECTORS # no directory; bound by sectors
|
|
if fs == _FS_DFS:
|
|
return _DFS_MAX_FILES
|
|
if fs == _FS_APPLE:
|
|
return _APPLE_BUFFER_PAGES # bound by RAM, not a directory
|
|
if fs == _FS_IIGS:
|
|
return _IIGS_TOTAL_SECTORS
|
|
if fs == _FS_AMIGA:
|
|
return _AMIGA_RAM_SECTORS
|
|
raise NotImplementedError(f"slideshow storage not implemented for {platform}")
|
|
|
|
|
|
def _block_size(platform: str) -> int:
|
|
fs = _platform_fs(platform)
|
|
if fs == _FS_ATR:
|
|
return _ATR_BLOCK
|
|
if fs == _FS_DFS:
|
|
return _DFS_BLOCK
|
|
if fs == _FS_APPLE:
|
|
return _APPLE_BLOCK
|
|
if fs == _FS_IIGS:
|
|
return _IIGS_BLOCK
|
|
if fs == _FS_AMIGA:
|
|
return _AMIGA_BLOCK
|
|
return 254
|
|
|
|
|
|
def budget(platform: str, fmt: str, data_lens: list[int],
|
|
viewer_len: int) -> Budget:
|
|
"""Size a slideshow against a disk format.
|
|
|
|
``data_lens`` are the per-image payload lengths (len(conv.data)); ``viewer_len``
|
|
is the assembled viewer length. The viewer plus one file per image must fit
|
|
both the block/sector capacity and (where applicable) the directory cap.
|
|
"""
|
|
total = capacity_blocks(platform, fmt)
|
|
cap = dir_cap(platform, fmt)
|
|
used = (viewer_blocks(platform, fmt, viewer_len)
|
|
+ sum(item_blocks(platform, fmt, n) for n in data_lens))
|
|
files = 1 + len(data_lens) # viewer + one per image
|
|
fits = used <= total and files <= cap
|
|
reason = ""
|
|
if used > total:
|
|
reason = f"{used} blocks needed, {fmt} holds {total}"
|
|
elif files > cap:
|
|
reason = f"{files} files exceeds the {fmt} directory ({cap})"
|
|
return Budget(used, total, files, cap, fits, reason, _block_size(platform))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# build (C64 reference)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def _mode_byte(conv: Conversion) -> int:
|
|
"""Per-image VIC setup selector for the simple slideshow viewer.
|
|
|
|
0 = hires/mono (single bitmap + screen), 1 = multicolor (+ colour RAM + bg).
|
|
"""
|
|
return 1 if conv.viewer == "multicolor" else 0
|
|
|
|
|
|
# viewer keys that each slideshow engine flavor accepts
|
|
_SIMPLE_VIEWERS = {"hires", "multicolor"} # mono uses the "hires" viewer
|
|
_FLI_VIEWERS = {"fli", "fli_ntsc"}
|
|
_INTERLACE_VIEWERS = {"interlace"}
|
|
|
|
|
|
def slideshow_flavor(convs: list[Conversion]) -> str:
|
|
"""Pick the engine for a queue, or raise if its modes can't share a viewer.
|
|
|
|
A slideshow must be all simple (hires/multicolor/mono), all FLI, or all
|
|
interlace -- FLI/IFLI each need their own raster engine and memory map, so
|
|
they can't be mixed with the simple modes or each other.
|
|
"""
|
|
viewers = {c.viewer for c in convs}
|
|
if viewers <= _SIMPLE_VIEWERS:
|
|
return "simple"
|
|
if viewers <= _FLI_VIEWERS:
|
|
return "fli"
|
|
if viewers <= _INTERLACE_VIEWERS:
|
|
return "interlace"
|
|
raise ValueError(
|
|
"a slideshow must be all hires/multicolor/mono, all FLI, or all "
|
|
"interlace -- these modes cannot be mixed on one disc")
|
|
|
|
|
|
def data_filename(i: int) -> str:
|
|
"""Disk filename for slide ``i`` (two PETSCII digits the viewer rebuilds)."""
|
|
return f"{i:02d}"
|
|
|
|
|
|
def supports_slideshow(platform: str) -> bool:
|
|
return platform in SLIDESHOW_PLATFORMS
|
|
|
|
|
|
# Disk formats offered for a slideshow on each platform.
|
|
_SS_FORMATS = {
|
|
"c64": ["d64", "d71", "d81"],
|
|
"c128": ["d64"],
|
|
"atari": ["atr"],
|
|
"bbc": ["ssd"],
|
|
"apple": ["dsk"],
|
|
"iigs": ["dsk"],
|
|
"amiga": ["adf"],
|
|
}
|
|
|
|
|
|
def disk_formats(platform: str) -> list[str]:
|
|
return _SS_FORMATS.get(platform, [])
|
|
|
|
|
|
def check_modes(platform: str, convs: list[Conversion]) -> None:
|
|
"""Raise ValueError if the queue's modes can't share one slideshow viewer."""
|
|
if platform == "c64":
|
|
slideshow_flavor(convs) # raises on an illegal mix
|
|
elif platform == "c128":
|
|
bad = [c.meta.get("vdc_mode") for c in convs
|
|
if c.meta.get("vdc_mode") != "hicolor"]
|
|
if bad:
|
|
raise ValueError(
|
|
"C128 slideshows support the 640x200 hicolor/mono VDC mode only; "
|
|
"the 80x100 'color' mode is not yet supported")
|
|
elif platform == "atari":
|
|
viewers = {c.viewer for c in convs}
|
|
if not viewers <= {"gr15", "gr9", "gr8", "gr15dli"} or len(viewers) != 1:
|
|
raise ValueError(
|
|
"Atari slideshows support gr15 / gr9 / gr8 / gr15dli -- all slides "
|
|
"must use the same one of these modes")
|
|
elif platform == "bbc":
|
|
modes = {c.meta.get("bbc_mode") for c in convs}
|
|
if len(modes) != 1:
|
|
raise ValueError(
|
|
"BBC slideshows must use a single screen mode -- all slides must "
|
|
"be the same BBC mode")
|
|
elif platform == "apple":
|
|
viewers = {c.viewer for c in convs}
|
|
if viewers != {"hgr"}:
|
|
raise ValueError(
|
|
"Apple slideshows currently support HGR (hgr_color/hgr_mono) only")
|
|
if len(convs) > _APPLE_BUFFER_PAGES // 0x20:
|
|
raise ValueError(
|
|
f"Apple HGR slideshows hold at most {_APPLE_BUFFER_PAGES // 0x20} "
|
|
"images (they load into RAM at once)")
|
|
elif platform == "iigs":
|
|
viewers = {c.viewer for c in convs}
|
|
if viewers != {"iigs"}:
|
|
raise ValueError("IIgs slideshows support the SHR mode only")
|
|
if len(convs) > _IIGS_MAX_IMAGES:
|
|
raise ValueError(
|
|
f"IIgs SHR slideshows hold at most {_IIGS_MAX_IMAGES} images "
|
|
"(32K each on a 140K 5.25\" disk)")
|
|
elif platform == "amiga":
|
|
# every Amiga image is a self-contained copper+bitplanes blob, so any
|
|
# lowres/HAM mix is fine; only the 880K floppy bounds the count.
|
|
b = budget("amiga", "adf", [image_nbytes(c) for c in convs], 0)
|
|
if not b.fits:
|
|
raise ValueError(f"Amiga slideshow does not fit an .adf: {b.reason}")
|
|
|
|
|
|
def viewer_length(show: Slideshow, convs: list[Conversion],
|
|
video: str = "pal") -> int:
|
|
"""Assembled length of the slideshow viewer PRG (for the storage budget)."""
|
|
if show.platform == "c64":
|
|
from .viewer.assemble import build_slideshow_prg
|
|
return len(build_slideshow_prg(
|
|
[_mode_byte(c) for c in convs], advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop, video=video,
|
|
flavor=slideshow_flavor(convs)))
|
|
if show.platform == "c128":
|
|
from .c128.viewer.assemble import build_slideshow_prg
|
|
return len(build_slideshow_prg(
|
|
[c.meta.get("fgbg", 0) for c in convs], advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop, video=video))
|
|
if show.platform == "atari":
|
|
# the boot viewer occupies the fixed boot-sector reservation regardless
|
|
# of its exact length, which is what budget() charges for it.
|
|
return _ATR_BOOT_SECTORS * _ATR_BLOCK
|
|
if show.platform == "bbc":
|
|
from .bbc.viewer.assemble import build_slideshow_viewer
|
|
m0 = convs[0].meta
|
|
return len(build_slideshow_viewer(
|
|
m0["bbc_mode"], m0["ncol"], m0["base"],
|
|
[c.meta["physicals"] for c in convs], advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop, video=video))
|
|
if show.platform == "apple":
|
|
return 0 # boot loader is at $0800, outside the image buffer
|
|
if show.platform == "iigs":
|
|
return 0 # budget charges a fixed boot+stage2 sector reservation
|
|
if show.platform == "amiga":
|
|
return 0 # budget charges the fixed 2-sector boot block
|
|
raise NotImplementedError(f"viewer_length missing for {show.platform}")
|
|
|
|
|
|
def _check_budget(show: Slideshow, data_lens: list[int], viewer_len: int) -> None:
|
|
b = budget(show.platform, show.disk_format, data_lens, viewer_len)
|
|
if not b.fits:
|
|
raise diskimage.DiskError(
|
|
f"slideshow does not fit a {show.disk_format}: {b.reason}; "
|
|
f"use a larger disk format or remove images")
|
|
|
|
|
|
def build_disk(show: Slideshow, output_path: str, *, intensive: bool = False,
|
|
disk_name: str | None = None, video: str = "pal",
|
|
convs: list[Conversion] | None = None) -> str:
|
|
"""Convert every slide, assemble the slideshow viewer, and write the disk.
|
|
|
|
Pass ``convs`` to reuse already-computed conversions (the GUI caches them);
|
|
otherwise each item is converted here. Raises diskimage.DiskError if the
|
|
chosen format cannot hold the show (the budget is checked up front so the
|
|
message names the offending limit).
|
|
"""
|
|
if not supports_slideshow(show.platform):
|
|
raise NotImplementedError(
|
|
f"slideshow is not implemented for platform {show.platform}")
|
|
if not show.items:
|
|
raise ValueError("slideshow has no images")
|
|
if show.advance not in ADVANCE_MODES:
|
|
raise ValueError(f"advance must be one of {ADVANCE_MODES}")
|
|
if convs is None:
|
|
convs = [convert_item(show, it, intensive) for it in show.items]
|
|
|
|
if show.platform == "c64":
|
|
return _build_disk_c64(show, output_path, convs, disk_name, video)
|
|
if show.platform == "c128":
|
|
return _build_disk_c128(show, output_path, convs, disk_name, video)
|
|
if show.platform == "atari":
|
|
return _build_disk_atari(show, output_path, convs, disk_name, video)
|
|
if show.platform == "bbc":
|
|
return _build_disk_bbc(show, output_path, convs, disk_name, video)
|
|
if show.platform == "apple":
|
|
return _build_disk_apple(show, output_path, convs, disk_name, video)
|
|
if show.platform == "iigs":
|
|
return _build_disk_iigs(show, output_path, convs, disk_name, video)
|
|
if show.platform == "amiga":
|
|
return _build_disk_amiga(show, output_path, convs, disk_name, video)
|
|
raise NotImplementedError(f"slideshow build missing for {show.platform}")
|
|
|
|
|
|
def _build_disk_c64(show, output_path, convs, disk_name, video) -> str:
|
|
from .viewer.assemble import build_data_prg, build_slideshow_prg
|
|
flavor = slideshow_flavor(convs) # validates modes are uniform-enough
|
|
modes = [_mode_byte(c) for c in convs]
|
|
viewer = build_slideshow_prg(modes, advance=show.advance, seconds=show.seconds,
|
|
loop=show.loop, video=video, flavor=flavor)
|
|
_check_budget(show, [len(c.data) for c in convs], len(viewer))
|
|
|
|
stem = os.path.splitext(os.path.basename(output_path))[0]
|
|
name = diskimage.petscii_name(disk_name or stem or "slideshow")
|
|
files = [(name, viewer)]
|
|
for i, c in enumerate(convs):
|
|
files.append((data_filename(i), build_data_prg(c.data, c.data_addr)))
|
|
fmt = diskimage.fmt_from_path(output_path, show.disk_format)
|
|
return diskimage.build_disk(output_path, fmt, name, "01", files)
|
|
|
|
|
|
# C128 slideshow data files load into RAM bank 0 at $4000 (the viewer copies them
|
|
# to VDC RAM); only the 640x200 hicolor/mono VDC mode is supported.
|
|
_C128_DATA_ADDR = 0x4000
|
|
|
|
|
|
def _build_disk_c128(show, output_path, convs, disk_name, video) -> str:
|
|
from .c128.viewer.assemble import build_slideshow_prg
|
|
from .viewer.assemble import build_data_prg
|
|
bad = [c.meta.get("vdc_mode") for c in convs if c.meta.get("vdc_mode") != "hicolor"]
|
|
if bad:
|
|
raise ValueError(
|
|
"C128 slideshows support the 640x200 hicolor/mono VDC mode only "
|
|
f"(got {bad[0]!r}); the 80x100 'color' mode is not yet supported")
|
|
fgbg = [c.meta.get("fgbg", 0) for c in convs]
|
|
viewer = build_slideshow_prg(fgbg, advance=show.advance, seconds=show.seconds,
|
|
loop=show.loop, video=video)
|
|
_check_budget(show, [len(c.data) for c in convs], len(viewer))
|
|
|
|
files = [("pic", viewer)] # boots via RUN"PIC"
|
|
for i, c in enumerate(convs):
|
|
files.append((data_filename(i), build_data_prg(c.data, _C128_DATA_ADDR)))
|
|
stem = os.path.splitext(os.path.basename(output_path))[0]
|
|
name = diskimage.petscii_name(disk_name or stem or "slideshow")
|
|
return diskimage.build_disk(output_path, "d64", name, "cv", files)
|
|
|
|
|
|
def _build_disk_atari(show, output_path, convs, disk_name, video) -> str:
|
|
from .atari import atr
|
|
from .atari.viewer.assemble import build_slideshow_stub
|
|
check_modes("atari", convs) # uniform gr15/gr9/gr8 (raises otherwise)
|
|
spi = max(item_blocks("atari", "atr", len(c.data)) for c in convs)
|
|
base = _ATR_BOOT_SECTORS + 1
|
|
stub = build_slideshow_stub(convs[0].viewer, len(convs), base, spi,
|
|
advance=show.advance, seconds=show.seconds,
|
|
loop=show.loop, video=video)
|
|
_check_budget(show, [len(c.data) for c in convs], len(stub))
|
|
if not output_path.lower().endswith(".atr"):
|
|
output_path += ".atr"
|
|
return atr.write_slideshow_atr(output_path, stub, [c.data for c in convs],
|
|
boot_sectors=_ATR_BOOT_SECTORS, spi=spi)
|
|
|
|
|
|
def _build_disk_bbc(show, output_path, convs, disk_name, video) -> str:
|
|
from .bbc import ssd
|
|
from .bbc.viewer.assemble import LOAD_ADDR, build_slideshow_viewer
|
|
check_modes("bbc", convs) # uniform BBC mode (raises otherwise)
|
|
m0 = convs[0].meta
|
|
viewer = build_slideshow_viewer(
|
|
m0["bbc_mode"], m0["ncol"], m0["base"],
|
|
[c.meta["physicals"] for c in convs], advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop, video=video)
|
|
_check_budget(show, [len(c.data) for c in convs], len(viewer))
|
|
if not output_path.lower().endswith(".ssd"):
|
|
output_path += ".ssd"
|
|
# !BOOT autostarts on SHIFT+BREAK; PIC is the same loader *RUN from a command.
|
|
files = [("!BOOT", LOAD_ADDR, LOAD_ADDR, viewer),
|
|
("PIC", LOAD_ADDR, LOAD_ADDR, viewer)]
|
|
for i, c in enumerate(convs):
|
|
files.append((data_filename(i), c.meta["base"], c.meta["base"], c.data))
|
|
title = diskimage.petscii_name(disk_name or "8bitlenser", 12).upper() or "8BITLENSER"
|
|
ssd.write_ssd(output_path, files, title=title, boot_option=2)
|
|
return output_path
|
|
|
|
|
|
def _build_disk_apple(show, output_path, convs, disk_name, video) -> str:
|
|
from .apple import dsk
|
|
from .apple.viewer.assemble import build_slideshow_stub
|
|
check_modes("apple", convs) # uniform HGR (raises otherwise)
|
|
_check_budget(show, [len(c.data) for c in convs], 0)
|
|
boot = build_slideshow_stub(len(convs), advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop)
|
|
payload = b"".join(c.data for c in convs) # images read contiguously to $4000+
|
|
if not output_path.lower().endswith((".dsk", ".do")):
|
|
output_path += ".dsk"
|
|
return dsk.write_dsk(output_path, dsk.build_boot_dsk(boot, payload))
|
|
|
|
|
|
def _build_disk_iigs(show, output_path, convs, disk_name, video) -> str:
|
|
from .apple import dsk
|
|
from .iigs.viewer.assemble import build_slideshow
|
|
check_modes("iigs", convs) # uniform SHR, <=4 images
|
|
_check_budget(show, [len(c.data) for c in convs], 0)
|
|
boot, stage2, _ = build_slideshow(len(convs), advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop, video=video)
|
|
# payload = stage2 (padded to whole sectors) then the SHR images, all read
|
|
# contiguously by the boot: stage2 -> $0900, each image -> bank-0 $2000.
|
|
payload = bytes(stage2) + bytes((-len(stage2)) % 256)
|
|
payload += b"".join(c.data for c in convs)
|
|
if not output_path.lower().endswith((".dsk", ".do")):
|
|
output_path += ".dsk"
|
|
return dsk.write_dsk(output_path, dsk.build_boot_dsk(boot, payload))
|
|
|
|
|
|
def _build_disk_amiga(show, output_path, convs, disk_name, video) -> str:
|
|
from .amiga import viewer
|
|
check_modes("amiga", convs) # any blobs; bounded by the floppy
|
|
if not output_path.lower().endswith(".adf"):
|
|
output_path += ".adf"
|
|
adf = viewer.build_slideshow_adf([c.data for c in convs], advance=show.advance,
|
|
seconds=show.seconds, loop=show.loop, video=video)
|
|
return viewer.write_adf(adf, output_path)
|