SZX snapshots¶
How Xpeccy+ reads and writes zx-state (.szx) files: what it takes from each standard block,
what it adds, and where. The format is Spectaculator’s, version 1.5, maintained by Jonathan
Needle (the specification); this page only
covers what the spec leaves open and what Xpeccy+ keeps beyond it. How to save and open a
snapshot is in Snapshots. The code is src/libxpeccy/filetypes/szx.c
(the standard blocks) and szx_ext.c (our own data); where this page and the code disagree,
the code is right.
The page is written for the authors of other programs. The format has no version newer than 1.5, and machines it never named - TSConf, TurboSound FM, the ZX Spectrum Next - are now common. If each program keeps them its own way, a snapshot can only be opened where it was made. So everything Xpeccy+ writes is described here, field by field: another program can read our files and write files we read, and for TSConf and TurboSound FM the last section proposes blocks to share instead of each program inventing its own.
Two kinds of file¶
A machine the spec names - 16K, 48K, 128K, +2, +2A, +3, Pentagon 128/512/1024, Scorpion - is written with its own machine id and the standard blocks, and any reader that knows SZX loads it. What the standard blocks have no room for (a second and third sound chip, the FM half, the exact raster phase, the disk controller in mid-command) goes into the creator block, where other readers skip it.
A machine it cannot name - ZX Evolution (BaseConf, TSConf), ATM Turbo 2+, Profi, Phoenix, ALF - is written with machine id
0x80. The standard blocks are still there (CPU, memory, AY, disks, tape), but only Xpeccy+ knows which machine to build for it, so other readers refuse the file. Ids 0..16 are the spec’s;0x80was picked well clear of them.
Both are loaded on the model they name. A Pentagon file opened while a 128K is running switches the machine to a Pentagon first, since the frame position in the file means nothing on a machine with another frame. Only a machine of the user’s own (one saved under its own id) on the same core is left as it is. Settings the file records and the machine does not match - ULA timings, board issue, ULA+, an AY, a Beta 128, a Covox, a device not emulated - are not changed: the user is told and the load goes on. A joystick, a mouse and a General Sound are only logged, as writers record whatever they were set to, used or not.
Standard blocks¶
Read means what Xpeccy+ takes from a block; written means what it puts in one.
Block |
Read |
Written |
|---|---|---|
|
name and version; |
|
|
all of it, see below |
all of it |
|
border, |
the same |
|
every page, compressed or not |
every page, compressed |
|
chip 0’s sixteen registers and the selected register |
the same |
|
ULA+ state, when the machine has ULA+ |
when it has ULA+ |
|
the WD1793 registers and the system register, see below |
the same |
|
the image, linked or embedded |
a link, or the disk as a UDI when it is carried |
|
the image, linked or embedded (see below) |
a link, or the |
|
nothing: the drives are the machine’s |
the drive count |
|
the image, linked or embedded, and the block it stood at |
a link and the block |
|
the level |
the level |
|
a custom ROM, put in place of the machine’s |
the custom ROM, when one was put in |
|
the General Sound’s CPU, ports and RAM |
the same |
|
compared with the machine, never taken: they are settings |
what the machine has |
|
skipped: not emulated; the user is told |
- |
any other |
skipped |
- |
What the spec leaves open, and how it is read here:
Z80R.dwCyclesStartis where the machine stands in its frame, in T from the start of the frame interrupt; the raster, the contention phase and the floating bus are put there. A.z80or.snacannot say this, which is why SZX is the default format. The T is the CPU’s at the speed it runs when the snapshot is taken, turbo included. Our ownRAYrecord (below) carries the exact dot as well.Z80R.chHoldIntReqCyclesis what is left of the INT pulse, in T. A pulse not yet taken can still be taken after the load.Z80R.chFlags:SUPPRESS_INTSis “the last instruction was EI”,HALTEDthe HALT state,FSET“the last instruction changed the flags” (for SCF/CCF’s undocumented bits).SUPPRESS_INTSandHALTEDexclude each other.IFF1 and IFF2: inside an NMI handler IFF1 is 0 and IFF2 1, and the RETN at its end enables interrupts with no EI. A reader that gates INT on IFF1 alone handles this anyway; one that keeps a separate “EI was last” flag has to set it from IFF1 or IFF2.
Z80R.wMemPtris the Z80’s internal WZ register; it decides bits 3 and 5 of the flags afterBIT n,(HL).AY.chCurrentRegisteris the byte last written toFFFD, all eight bits. An AY or YM only answers when the upper four bits are 0000: any other number deselects it until the next one, so writes go nowhere and a read sees the floating bus. A reader should keep the byte, not mask it to four bits.B128: the system register (FF) is set, not written. Written, its MR bit going high would start the RESTORE a real WD1793 runs coming out of reset, which the machine saved had finished long ago. TheCONNECTEDflag is not looked at on reading, as some writers leave it clear on a Pentagon.RAMPon a Pentagon 512/1024 numbers pages 0..31 and 0..63 the way7FFDandEFF7page them.Links in
TAPE,BDSKandDSKare written as full paths in the host’s own form. On reading, a link that does not resolve is looked for beside the snapshot by its file name, and an image already in the drive is left there, so a disk written to since is not lost.An embedded
DSK: the spec namesZXSTDSKF_EMBEDDEDandZXSTDSKF_COMPRESSEDbut marks them not implemented, and says only thatchDatais a file name. Xpeccy+ reads them the wayTAPEhas it: withEMBEDDEDset,chDatais the.dskfile’s own bytes anddwUncompressedSizeits length; withCOMPRESSEDtoo, the bytes are a zlib stream that unpacks to that length. It writes an extended.dsk(EXTENDED CPC DSK File), packed when that gains anything.An embedded
BDSKis read as the type itschDiskTypenames - TRD, SCL, FDI or UDI - and written as a UDI, whatever file the disk came from: a UDI keeps the whole track, sector layout and protection included.Carried media: an image written into the file (the tape as its file is, a Beta disk as a UDI, a +3 disk as a
.dsk) still goes back in under its old name, from ourLINKrecord. A disk that was unsaved when the file was written, or whose file is gone, comes back unsaved, so the program asks to save it.Empty drives: a drive with no
BDSKorDSKblock, or noTAPEblock, keeps what is in it on loading: the file says nothing about it. The user can keep the file’s media out altogether (Load media on the Saves page); a quick load always puts them in.
Our data: the creator block’s chData¶
Some readers report every block they do not know to the user as an error, even though the
load succeeds. So Xpeccy+ adds no blocks of its own: its data goes in CRTR.chData, which the
spec gives to “the program that created the file” and every reader skips.
CRTR block
char szCreator[32] "Xpeccy+"
word chMajorVersion
word chMinorVersion
chData:
char tag[8] "Xpeccy+\0"
record... to the end of the block
A record is shaped like a block:
dword id four characters, like a block id
dword len
byte data[len]
Most records are a list of named fields:
byte keylen
char key[keylen] not terminated
word len
byte data[len]
A reader takes a field only when it has one of that name and that size, and leaves the rest as the standard blocks set it. That is what lets a build read a file of an older or newer one: a field that changed size is skipped, not misread. An unknown record is skipped whole.
What a field holds is Xpeccy+’s own memory layout - the C structure the emulator runs on,
not a description of the hardware. That is fine for a snapshot that has to put the machine
back exactly, and useless to anyone else. Only MACH, MAP, RAY, TS, FDCP, FM n
and SDCD have a layout of their own, written down here (TS is fields with one byte after them); the
rest are opaque.
Which machine: MACH and id 0x80¶
MACH is always the first record:
char core[] the core's name, zero terminated ("ZX48", "TSLab", "PentEvo"...)
char machine[] the machine's id, zero terminated ("zx48", "evo-tsconf"...)
A file with machine id 0x80 is built as the machine MACH names. The other records are only
taken when the file’s core is the running core; on another core only the sound chips’ records
(TS, CHPn, FM n) are, since the ports, flags and video mean something different there.
The records¶
Record |
What it holds |
|---|---|
|
core and machine id, above |
|
the Z80 beyond |
|
the machine: turbo, the ear input’s RC state, the floating bus’s last byte, frame and line counters, the core’s ports and registers, the CMOS clock, TSConf’s own state |
|
the pager’s map, 256 entries of 256 bytes each: |
|
the video: mode, border, flash, INT pulse, TSConf’s video state and line buffer |
|
the palette, on a machine whose palette is its own (ATM, Evo, TSConf, Profi) |
|
|
|
TurboSound: mutes, status-read mode, then |
|
each sound chip’s counters, envelope, selected register and all 256 register bytes |
|
a YM2203’s FM engine, |
|
the beeper’s level and filter |
|
SAA1099, Soundrive/Covox |
|
the disk interface and its controller |
|
the controller’s command in progress: |
|
each drive: motor, head, track, position on the track, whether its disk was unsaved |
|
the IDE interface and its two devices |
|
SMUC’s NVRAM |
|
the SD card |
|
General Sound beyond |
|
the tape’s position inside the block, the level, the pause |
|
a carried image’s old name: |
|
the ZX Spectrum Next: see below |
|
the Next’s SD card, named, not carried: see below |
|
the Next’s ESP-01 module: see below |
The order of reading matters in one place: the drives and the tape are mounted after the
snapshot is in, so FLPn, DIF and TAPE wait until their images are back.
TSConf¶
TSConf is a ZX Evolution configuration, so its files carry machine id 0x80 and MACH names
TSLab / evo-tsconf. Its state is in three records, all opaque:
COMP- the#xxAFports as the core keeps them (most in its register array, the DMA addresses in its 32-bit registers), the cache configuration and the cache’s tag RAM, a DMA transfer in progress (source, destination, length, burst count, the word in flight and the DRAM cycles banked for it), the power-up flag, and the Evo’s 256-byte clock RAM.VID- the video side: graphics and tile offsets, tile and sprite pages, palette select, the INT line and position and the lines it moves on each frame, the values written but not yet taken at the start of the blanking, CRAM (512 bytes), SFILE (512 bytes), the tile map rows read ahead, and the line buffer half drawn when the snapshot was taken.MAP- windows 1-3 exist only in the pager’s map (#11AF..#13AFwrite it directly), so the map is carried as it is rather than rebuilt from ports.
RAY puts the raster back to the dot, which matters here: TSConf moves its INT anywhere in
the frame, and a T is a different number of dots at each of its three speeds.
ZX Spectrum Next¶
A Next file carries machine id 0x80 and MACH names ZXNext / zxnext. The memory is the
board’s SRAM in RAMP blocks, pages 0 to 127 on 2 MB and 0 to 63 on 1 MB: the ROMs the firmware
loaded from the card, the divMMC and Multiface memory and the RAM banks, all where the board
keeps them. A file of the other size runs the Next on its own. The rest is in records of our
own, all opaque:
NEXT- the board, part by part: the nextregs, the memory map and the ports, the copper and its program, the palettes, the sprites and their patterns, the layers and the picture the mixer is drawing, the DMA, the interrupt controller, the CTC, the AY and the DACs, the divMMC paging, the keyboard maps, both UARTs, and the SRAM behind the three banks the CPU sees as fast memory.NCLK- the Next’s times count from when the machine was made, in its 28 MHz clock, so the clock comes back with them; and whether the firmware’s start is still to come (a recording begun at power-on).RTC- the DS1307 clock chip, its battery side too.VCLK- the video clock’s fraction of a dot.SDCD- the SD card: a card is far too big to carry, but NextZXOS keeps what it read of it in memory. The record names the card (an image or a folder), the folder a program opened from elsewhere was put on it as, and a print of what the card holds as the Next reads it. A load puts the card back in and that folder back on it, and says so when what it holds has changed since. A collection an older build put on whole (see Starting programs) goes back empty: the program is in memory already.
byte flags b0: a folder, not an image; b1: the print at the end follows
qword old print 0 with b1; without it, a print of every directory entry, not compared
char path[] zero terminated
byte folders how many follow (one at a time is put on a card)
for each:
char name[] its name in the card's root, UTF-8, zero terminated
char host[] where it is on the computer, UTF-8, zero terminated; the
program's file when it went alone (an older reader finds no
folder there)
qword print with b1: every file and folder by its name (the long one, without
case), a file's size and write time - not where they lie, the order
or the folders' times; 0 none
ESP- the ESP-01 module, which runs outside the machine: its UART, boot, the command being received, the replies waiting to go out, the Wi-Fi and TCP settings and what its links hold. Empty when no module is fitted. The network side is not in it: a load finds the connections closed, and the module goes on as when the far end drops them.
RAY puts the raster back to the dot, as for TSConf: the Next runs at four speeds.
TurboSound FM¶
A TurboSound FM is two YM2203s behind one pair of ports. A write to FFFD of %11111xxx is
the board’s own command, not a register number, and neither chip sees it: bit 0 picks the
chip, bit 1 clear means a read of FFFD returns the status, and bit 2 set turns the FM halves
off. Any other byte goes to the selected chip.
How it is kept:
The standard
AYblock holds chip 0’s SSG half, so any SZX reader gets the first chip’s music, without FM.TSholds the command’s state: which chip is selected, the status-read mode, and the mutes.CHPnholds each chip’s SSG counters and its 256 register bytes:0x00..0x0Fthe SSG,0x10..0xFEthe FM registers as last written,0xFFthe status. The register number incurRegis all eight bits.FM nis the FM engine’s own state - operator phases, envelope stages and levels, the LFO, both timers, the busy flag, the prescaler - in the order of the FM core Xpeccy+ uses. It is size-checked like a field, so a build whose engine changed skips it and keeps the registers.
The FM registers alone do not put a note back: they say what was last written, not how far each
envelope has gone. A reader that has only them can restart the notes from the registers, which
is what Xpeccy+ does when FM n does not fit.