8bitlenser/lenser/spectrum/snapshot.py
2026-07-03 19:35:35 -07:00

50 lines
2.1 KiB
Python

"""Build a 48K ZX Spectrum .SNA snapshot (and raw .SCR) from a screen image.
A .SNA bakes the whole 48K RAM plus the CPU state. We put the 6912-byte screen
at $4000, a 3-byte idle stub (DI; JR $) at $8000, and set SP so the loader's
RETN jumps to the stub -- the ULA then displays the screen forever while the CPU
idles, so the picture appears the instant MAME loads the file.
.SNA 27-byte header: I, HL', DE', BC', AF', HL, DE, BC, IY, IX, IFF2, R, AF, SP,
IM, border; followed by 49152 bytes of RAM ($4000-$FFFF).
"""
from __future__ import annotations
import struct
RAM_BASE = 0x4000
RAM_SIZE = 0xC000 # 48K ($4000-$FFFF)
SCREEN_LEN = 6912 # 6144 bitmap + 768 attributes
STUB_ADDR = 0x8000 # idle loop DI; JR $
STUB = bytes([0xF3, 0x18, 0xFE])
SP_ADDR = 0xFF00 # stack holds the stub address for the loader's RETN
def build_sna(screen: bytes, border: int = 0) -> bytes:
if len(screen) != SCREEN_LEN:
raise ValueError(f"screen must be {SCREEN_LEN} bytes, got {len(screen)}")
ram = bytearray(RAM_SIZE)
ram[0x0000:SCREEN_LEN] = screen # $4000 screen
off = STUB_ADDR - RAM_BASE
ram[off:off + len(STUB)] = STUB # $8000 idle stub
sp = SP_ADDR - RAM_BASE # return address for RETN
ram[sp] = STUB_ADDR & 0xFF
ram[sp + 1] = (STUB_ADDR >> 8) & 0xFF
header = bytearray(27)
header[0x00] = 0x3F # I
# HL',DE',BC',AF',HL,DE,BC,IY,IX all zero
header[0x13] = 0x00 # IFF2 = 0 (interrupts off)
header[0x14] = 0x00 # R
struct.pack_into("<H", header, 0x17, SP_ADDR) # SP
header[0x19] = 0x01 # IM 1
header[0x1A] = border & 0x07 # border colour
return bytes(header) + bytes(ram)
def build_scr(screen: bytes) -> bytes:
"""The standard 6912-byte ZX Spectrum screen file (raw $4000 dump)."""
if len(screen) != SCREEN_LEN:
raise ValueError(f"screen must be {SCREEN_LEN} bytes, got {len(screen)}")
return bytes(screen)