EZX snap format

(Ñ) by Vladimir Kladov, 2003

This format is intended to use it in EmuZWin 2 Spectrum emulator. It allows to save exact state of Spectrum machine, including current registers, memory and other internal data, and also tape state, inserted TR-DOS-disks (with entire content), debug environment, pokes, keyboard redirection and other necessary information.

EZX must be started with four characters signature 'Emuz', and always should have extension 'EZX'. The first part of the file contains fixed part, representing current Spectrum machine snapshot. This part of a file should always be read (except may be a case when only a screen requested for load). It's structure (including signature) is:

TSpecData = packed Record
Signature : array[ 0..3 ] of Char; // 'Emuz'
ROM0, ROM1: TMemoryBank; // ROM0=S128 rom; ROM1=S48 rom
RAMs      : array[ 0..7 ] of TMemoryBank;
Lock7FFD  : Boolean;     // true, if S48K only
State     : TSpecState;  // registers, etc.
Sound     : TSoundChipState;  // sound chip AY state
ReleaseDescriptor: DWORD;     // not used
Tape      : TTapeData;        // current tape reader state
end;

Substructures used are defined as follows:

TMemoryBank = array[ 0..16383 ] of Byte;

TSpecState = packed Record
AF, BC, DE, HL, IX, IY,
AFalt, BCalt, DEalt, HLalt, 
PC, SP               : Word;
I, R, HiddenReg      : Byte;
IFF1, IFF2, IntSignal: Boolean;
ImMode               : Byte;
TactsFromLastInt     : DWord;
BankROM_0000,                   // 0=rom S128, 1=rom S48
BankRAM_C000,                   // 0..7 
BankVideo            : Byte;    // 0=RAM Bank 5, 1=RAM Bank 7
BorderColor          : Byte;    // 0..7
MIC                  : Boolean; 
JustAfterEI          : Boolean; // Interrupt disable for a single instruction if TRUE
Flash                : Boolean; // TRUE if flushed
FramesFromLastFlashSwitch: Byte; // 0..15
end;

TSoundChipState = packed record
LastFFFD             : Byte;
Regs                 : array[ 0..15 ] of Byte;
end;

TTapeData = packed record
    TapeImgData: Pointer;
    TapeImgLen: Integer;
    TapePosition: Integer;
    PulseLength: Integer; // current pulse length
    TactsLeast: Integer;  // how many tacts current pulse should continue
    BitIndex: Integer;
    CurByte: Byte;
    Pilot: Integer;       // when LoadSpectrum called, 1 if to double tone except SpeedLock1 & 2
    Active: Boolean;
end;

The last structure should be ignored if following chunks certain to tape image and tape reader state are not loaded.

Other chunks always contains four-byte chunk name and its data length to allow skipping unsupported chunks.


'TAPC' chunk - call stack of tape recorder:

offset size   description
0      4      name         = 'TAPC'
4      4      size         = size of data (must be a multiple of 4)
8      size   size/4 dwords (return positions)
Data of this chunk has its own format, may be not so compact, but
very easy to implement and therefore allowing to represent any tape
recording in exact pilses. See description of tape format below.

'TAPL' chunk - loop stack of tape recorder:

offset size   description
0      4      name         = 'TAPL'
4      4      size         = size of data (must be a multiple of 8)
8      size   size/8 qwords (counters and correspondent jump positions)

'INIT' chunk - initial state of the Spectrum machine
Actually, dat of this chunk has exactly the same format as the first fixed
data structure. It can be used to re-play game without loading it again
from the snap/tape file.

offset size   description
0      4      name         = 'INIT'
4      4      size         = size of data (always = Sizeof(TSpecData))
8      Sizeof(TSpecData)   copy of TSpecData structure

'ITPC' chunk - call stack of tape recorder in its initial state:

offset size   description
0      4      name         = 'ITPC'
4      4      size         = size of data (must be a multiple of 4)
8      size   size/4 dwords (return positions)

'ITPL' chunk - loop stack of tape recorder in its initial state:

offset size   description
0      4      name         = 'ITPL'
4      4      size         = size of data (must be a multiple of 8)
8      size   size/8 qwords (counters and correspondent jump positions)

'POKS' chunk - a list of pokes supplied:

offset size   description
0      4      name         = 'POKS'
4      4      size         = size of entire data
8      Sequence of strings (separated with #13 and #10 as usual text file)
       in .POK-file format.

'BRKS' chunk - a list of break points for the debugger:

offset size   description
0      4      name        = 'BRKS'
4      4      size        = size of entire data
8      4      N = number of TBreakPoint structures
12     ...    array of TBreakPoint structures

where TBreakPoint is defined as follows:

TPoke = packed record
  MemBank   : Byte;    // 0=ROM0, 1=ROM1, 10H..17H=RAM0..RAM7
  Addr      : Word;    // 0000H..3FFFH in a bank
  Counter   : DWORD;
  StopWhen  : Byte;
  CondCount : Byte;
  Conditions: array[1..CondCount] of array[0..31] of Char;
end;

each condition is a string of format <TARGET>[<MASKorOFFSET>]<OP><VALUE>
where <TARGET> ::= { A | B | BC | (BC) | C | D | DE | (DE) | E | F |
 H | HL | (HL) | I | IFF1 | IFF2 | IM | IX | (IX) | IY | (IY) | L |
 (Addr) | PC | (PC) | R | RAM | ROM | SP | TState }
if target is (IX) or (IY), <MASKorOFFSET> is used as an offset and
is written like (IX+XXXX) (X=hexadecimal digit). If target is (Addr),
<MASKorOFFSET> also used to indicate an addr (in form (XXXX)) Otherwise it is used as
a mask and is written like &XXXX or &XX;
      <OP> ::= { = | <> | < | <= | > | >= };
      <VALUE> always is hexadecimal byte or word written like XX or XXXX.

'LABS' chunk - a list of labels for the debugger:

offset size   description
0      4      name        = 'LABS'
4      4      size        = size of entire data
8      4      N = number of TLabelDef structures
12     ...    array of TLabelDef structures

where TLabelDef is defined as follows:

TLabelDef = packed record
  MemBank   : Byte;
  Addr      : Word;
  LabelName : array of Char; // until #00
end;

LabelName can be any ASCII.

'ASMZ' chunk - a text in assembler window:

offset size   description
0      4      name         = 'ASMZ'
4      4      size         = size of entire data
8      size   text (lines are separated with #13#10, the text is finished with #00)

'KEYS' chunk - key re-definition table.

offset size   description
0      4      name = 'KEYS'
4      4      size = size of entire data
8      Sum(i=1..N: Sizeof(TKeyMap) or Sizeof(TKeyMapOld) ) = 
	      array of key definitions

where each TKeyMap or TKeyMapOld is defined as follows:

  TKeyMapOld = packed Record
    PCKey: Byte;
    ZXKey1: DWORD;
    ZXKey2: DWORD;
    Style: TPressStyle;
    Timer: Integer; // time to press key(s) again (pressLoop style)
    Hold: Integer;  // time to hold key(s) pressed (pressAutoUp, pressLoop)
  end;
  TKeyMap = packed Record
    PCKey: Word;
    ZXKey1: DWORD;
    ZXKey2: DWORD;
    Style: TPressStyle;
    Timer: Integer; // time to press key(s) again (pressLoop style)
    Hold: Integer;  // time to hold key(s) pressed (pressAutoUp, pressLoop)
  end;
If the first byte of a record is 3A or 3B, new TKeyMap is present,
otherwise it is the TKeyMapOld (which differ by the first field length).
New TKeyMap intruduced to allow representing Joystick1 (3A) and Joystick2 (3B)
directions and keys:
Function Joystick1 Joystick2
Up 013A 013B
Down 023A 023B
Left 033A 033B
Right 043A 043B
Fire (Button1) 053A 053B
Button2 063A 063B

...

Button32 243A 243B
TPressStyle = ( pressNormal, pressFixed, pressAutoUp, pressLoop );
ZXKeyN fields contains ZX Spectrum key coded as follows:
const
     kCapsShft = $0FE; kZ     = $0FD; kX     = $0FB; kC     = $0F7; kV     = $0EF;
     kA        = $1FE; kS     = $1FD; kD     = $1FB; kF     = $1F7; kG     = $1EF;
     kQ        = $2FE; kW     = $2FD; kE     = $2FB; kR     = $2F7; kT     = $2EF;
     k1        = $3FE; k2     = $3FD; k3     = $3FB; k4     = $3F7; k5     = $3EF;
     k0        = $4FE; k9     = $4FD; k8     = $4FB; k7     = $4F7; k6     = $4EF;
     kP        = $5FE; kO     = $5FD; kI     = $5FB; kU     = $5F7; kY     = $5EF;
     kEnter    = $6FE; kL     = $6FD; kK     = $6FB; kJ     = $6F7; kH     = $6EF;
     kSpace    = $7FE; kSymShft = $7FD; kM   = $7FB; kN     = $7F7; kB     = $7EF;
     jR        = $8FE; jL     = $8FD; jD     = $8FB; jU     = $8F7; jFire  = $8EF;
     k_        = $FFFF; (no key)

'COLR' chunk - color adjustment table:

offset size   description
0      4      name = 'COLR'
4      Sizeof(TColorTable) - a table of colors to use it to represent screen.

TColorTable = packed record 
NormalBlack, NormalRed, NormalBlue, NormalMagenta,
NormalGreen, NormalCyan, NormalYellow, NormalWhite,
BrightBlack, BrightRed, BrightBlue, BrightMagenta,
BrightGreen, BrightCyan, BrightYellow, BrightWhite: TRGB;
end;

TRGB = packed record
  R, G, B: Byte;
end;

'LDIR' chunk - flag to allow fast LDIR/LDDR/CPIR/CPDR emulation:

offset size   description
0      4      name = 'LDIR'
4      4      size = 1 (size of entire data)
8      1      flag = 0 LDIR/LDDR/CPIR/CPDR fast emulation disabled,
                     1 enabled (default)
In EmuZWin, fast LDIR/... emulation does not affect emulation 
accuracy though, so it can be left turned on always.

'DISK' chunk - TR-DOS disk inserted:

This chunk is introduced in version 2.2 of EmuZWin, having TR-DOS support. A single EZX file can contain up to four TR-DOS disks inserted (A,B,C,D).

offset size   description
0      4      name = 'DISK'
4      4      size (size of entire disk data, for disks with standard 256-byte sectors, can be calculated as 262*(sectors count)+4+1 bytes).
8      1      Disk unit letter ('A', 'B', 'C', 'D')
9      1      Current Track #
10     1      Disk states
              D0: 1-ReadOnly; D1: 1-head down; D2: 0=FM, 1=MFM
11     1      Reserved to store other unit states
--- starting from here, content of all sectors listed excluding those
    containing nulls:
12     1      Cylinder # (0..254, though 83 usually is maximum for

              physical disk, value 255 means end of disk data and
              should be the last byte in the disk image).
13     1      Head (0-down, 1-up)

14     1      Track ID stored in the sector label (it can differ from Track # above
              and can be of any byte value.
15     1      Side # stored in the sector label (usually the same as Head above)
16     1      Sector ID (1..16 usually, but any byte value possible).
              The same track can have several sectors with the same ID.
17     1      Sector size code (0=128 bytes, 1=256 bytes, 2=512 bytes, 3=1024bytes,

              4=4096 bytes, other values can not be used, though actual data size
              is stored separately).
18     2      Control codes
20     2      Sector data length (though usually this value = 256, it can be different for non-standart formats).
22     (SectorDataLength) Actual sector data
              (usually of 256 bytes length)
--- from here, the next sector starts, ets. until Track# = FF

Other notes: order of Tracks, Sectors, Sides does not matter (though EmuZ itself stores its sorted). Also, duplicated sectors allowed (reserved for non-standard formatted disks).


'BETA' chunk - TR-DOS controller state:

This chunk is introduced in version 2.2 of EmuZWin, having TR-DOS support.

offset size   description
0      4      name = 'BETA'
4      4      size (size of entire chunk data, 9).
8      1      D1,D0: Current disk unit (0='A', 1='B', 2='C', 3='D');
              D2: Seek direction; D3: Head down;
              D5,D4: Current operation (0=no operation, 1=read, 2=write,
                     3=format - the last is not implemented,
                     just reserved)
              D6: not used, 0; D7: 1=TR-DOS ROM paged on
9      1      Operation Track# (0..254)
10     1      Operation Sector# (D7: Side; D6-D0: Sector# 0..127)
11     1      Sectors count to finish read/write operation (0..255)
12     2      Data byte offset from start of the sector
14     2      reserved to store time (from last interrupt in current frame) when working with current byte started, to emulate working with disk exactly
16     1      reserved to store controller status byte.


'PRVW' chunk - Preview bitmap

This chunk is introduced in version 2.2, and allows to pass any bitmap as a preview image. Such bitmap should be returned as a single chunk in the additional stream, if any. It is ignored excluding case, when a plugin is called to return Screen Only.

offset size   description
0      4      name = 'PRVW'
4      4      size (size of entire chunk data).

8      (size) bitmap file image.


'TAPE' chunk - Tape image

This chunk usually is located first, but it is described here since it has most complicated structure and can contain a lot of sub-structures listed below

offset size   description
0      4      name = 'TAPE'
4      4      size (size of entire chunk data).

8      (size) tape data, see its format below.


DESCRIPTION OF TAPE FORMAT USED INTERNALLY IN EZX FILE FORMAT

Tape data contains blocks of variable length, identifying by the first byte. Format of each block is very different, but it is not planned to be extended or changed later, since all possible requirements can be expressed using existing blocks.


ID=0 Standard Data Block (TAP)

offset size   description
0      2      Length of Data
2      Length Data

Such block does not require pilot or synchronization defined separately (these are generated before the block). But if a pause needed after the block it should be defined as a separate tones block.


ID=1 Tones Data Block

offset size   description
0      2      Length of pulse in Spectrum T-states (1/350000 sec)
2      4      Tones count (actually bits count in following data)
6      (TonesCount+7)/8 bytes. Each bit represents one tone of
       high (1) or low (0) level. Bits in each byte are scanned
       from b7 to b0.

See also block ID=0C (Pure Data) below.


ID=2 Stop Command

No data. Tape is stopped until user click toolbar button "Play".


ID=3 Jump

offset size   description
0      4      Integer offset relative to start position of the
              block (just after ID byte).

Changes order of block to read. A byte pointed by the offset must be ID of the block to jump to.


ID=4 Loop start

offset size   description
0      4      Loop count.


ID=5 Loop end

No data.


ID=6 Call

offset size   description
0      4      Integer offset to a block called.


ID=7 Return from call

No data.


ID=8 Menu to display or text

offset size   description
0      1      Number of menu items. If 1, this block is just
              Text (can be used to place a description in a
              catalog).
1      4      Jump offset (relative to size byte, located just
              after ID byte).
5      Zero-terminated ASCII string.
x      4      Jump offset for second menu item.
..............


ID=9 Jump if S128

offset size   description
0      4      Integer jump offset.


ID=0A Message to display

offset size   description
0      2      Time to display message (0 - until OK pressed).
2      Zero-terminated ASCII string (#0D character to separate
       lines if necessary).


ID=0B Wait until IN (FE)

No data. This command can be added to stop playing tape until data from port FE requested from the Spectrum.


ID=0C Pure data

offset size   description
0      2      L0 = Length of pulse correspondent to value "0"
              in Spectrum T-states (1/350000 sec)
2      2      L1 = Length of pulse correspondent to value "1" in Spectrum T-states (1/350000 sec)
4      4      Tones count (actually bits count in following data)
8      (TonesCount+7)/8 bytes. Each bit represents one encoded value
       0 or 1, each of those will be decoded to a pare of low and high
       level pulse each of correspondent length (L0 for value 0, L1 for
       value 1). Bits in each byte are scanned from b7 to b0.

This command is added in version 2.1 (release 2.0) and is useful to represent TZX Pure Data Block or data part of Turbo Loading Data Block, especially in case when Zero Pulse Length and One Pulse Length are not relative to each other as p:q (p,q=1..8), but are very far from such releation, e.g. 1:1,8556.


Last update: 3-Nov-2003

http://bonanzas.rinet.ru mailto: bonanzas@online.sinor.ru