77 lines
3.7 KiB
Python
77 lines
3.7 KiB
Python
"""Build an Amstrad CPC .SNA snapshot (CPCEMU v1) from a screen image.
|
|
|
|
MAME loads a .sna by restoring the Z80, Gate Array (mode + 16-pen palette),
|
|
CRTC and 64K RAM, so the picture appears instantly with no loader. We bake the
|
|
16K screen at &C000, set the Gate Array palette + mode, program a standard 80x200
|
|
CRTC screen, and point the CPU at a tiny ``DI: JR $`` idle stub -- the CRTC then
|
|
DMAs the screen forever while the Z80 idles.
|
|
|
|
Header layout (offsets) per MAME's amstrad_handle_snapshot:
|
|
0x00 "MV - SNA" signature 0x10 version
|
|
0x11 AF 0x13 BC 0x15 DE 0x17 HL 0x19 R 0x1a I 0x1b IFF1 0x1c IFF2
|
|
0x1d IX 0x1f IY 0x21 SP 0x23 PC 0x25 IM 0x26 AF' .. 0x2c HL'
|
|
0x2e GA selected pen 0x2f..0x3f 17 ink numbers (16 pens + border)
|
|
0x40 GA multi-config (mode/rom) 0x41 RAM config
|
|
0x42 CRTC selected reg 0x43..0x54 18 CRTC regs 0x55 upper ROM
|
|
0x56..0x58 PPI A/B/C 0x59 PPI control 0x5a PSG reg 0x5b..0x6a PSG regs
|
|
0x6b memory size (KB) 0x100 RAM dump
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
|
|
SCREEN_BASE = 0xC000
|
|
SCREEN_LEN = 0x4000 # 16K
|
|
STUB_ADDR = 0x8000 # idle loop DI; JR $ (central RAM, always present)
|
|
STUB = bytes([0xF3, 0x18, 0xFE])
|
|
RAM_SIZE = 0x10000 # 64K
|
|
|
|
# Standard CPC 50Hz CRTC register set for a 40x25-char (80x200 byte) screen at
|
|
# &C000 (R12/R13 = 0x30/0x00).
|
|
CRTC = [0x3F, 0x28, 0x2E, 0x8E, 0x26, 0x00, 0x19, 0x1E,
|
|
0x00, 0x07, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00]
|
|
|
|
|
|
def screen_offset(y: int, bx: int) -> int:
|
|
"""Offset within the 16K screen for scan line ``y`` (0-199), byte column
|
|
``bx`` (0-79) -- the CPC's CRTC interleave (8 lines per char row)."""
|
|
return (y % 8) * 0x800 + (y // 8) * 80 + bx
|
|
|
|
|
|
def build_sna(screen: bytes, inks, mode: int, border: int = 20) -> bytes:
|
|
"""screen: 16384 bytes (&C000 RAM); inks: 16 hardware ink numbers (pens);
|
|
mode: 0/1/2; border: hardware ink number for the border pen."""
|
|
if len(screen) != SCREEN_LEN:
|
|
raise ValueError(f"screen must be {SCREEN_LEN} bytes, got {len(screen)}")
|
|
if not 1 <= len(inks) <= 16:
|
|
raise ValueError(f"need 1-16 ink numbers, got {len(inks)}")
|
|
# the Gate Array always has 16 pen slots; modes 1/2 use only the first 4/2.
|
|
inks = list(inks) + [border] * (16 - len(inks))
|
|
|
|
ram = bytearray(RAM_SIZE)
|
|
ram[STUB_ADDR:STUB_ADDR + len(STUB)] = STUB
|
|
ram[SCREEN_BASE:SCREEN_BASE + SCREEN_LEN] = screen
|
|
|
|
h = bytearray(0x100)
|
|
h[0x00:0x10] = b"MV - SNAPSHOT V1" # signature (MAME checks "MV - SNA")
|
|
h[0x10] = 1 # version
|
|
# Z80: everything 0 except SP/PC/IM; DI (IFF=0) and HALT-free idle stub.
|
|
struct.pack_into("<H", h, 0x21, STUB_ADDR) # SP
|
|
struct.pack_into("<H", h, 0x23, STUB_ADDR) # PC
|
|
h[0x25] = 1 # IM 1
|
|
# Gate Array palette: 16 pens + border, each a 5-bit hardware ink number.
|
|
h[0x2e] = 0 # selected pen
|
|
for pen in range(16):
|
|
h[0x2f + pen] = inks[pen] & 0x1F
|
|
h[0x2f + 16] = border & 0x1F # border ink
|
|
h[0x40] = (mode & 0x03) | 0x0C # mode + both ROMs disabled (RAM)
|
|
h[0x41] = 0x00 # RAM config 0 (standard 64K)
|
|
h[0x42] = 0 # CRTC selected reg
|
|
for i, v in enumerate(CRTC):
|
|
h[0x43 + i] = v & 0xFF
|
|
h[0x55] = 0 # upper ROM
|
|
h[0x59] = 0x82 # PPI control (conventional)
|
|
h[0x5b + 7] = 0x3F # PSG mixer: all channels off (silent)
|
|
struct.pack_into("<H", h, 0x6b, 64) # 64K dump
|
|
|
|
return bytes(h) + bytes(ram)
|