From MultimediaWiki
Jump to navigation Jump to search

VQA stands for Vector Quantized Animation and is a FMV format used in a number of computer games by Westwood Studios.

Technical Description

TODO: import and combine Gordan Ugarkovic's documents from http://multimedia.cx/VQA_INFO.TXT and http://multimedia.cx/HC-VQA.TXT.



This document only applies to VQA files in the original C&C and C&C: Red Alert (version 2) as well as Legend of Kyrandia III : Malcolm's Revenge (version 1). It DOES NOT apply to the HiColor VQAs found in Westwood's newer games. They use a somewhat different approach to compressing video data (I will provide some facts about their sound stream, though). Look at my other document (HC-VQA.TXT) for a description of the HiColor VQA movies.


Throughout the document, I will assume that:

  • CHAR is 1 byte in size, unsigned,
  • SHORT is 2 bytes in size, unsigned,
  • LONG is 4 bytes in size.

Each VQA file is comprised of a series of chunks. A chunk can contain other sub-chunks nested in it. Every chunk has a 4 letter ID (all uppercase letters) and a LONG written using Motorola byte ordering system (first comes the Most Significant Byte), unlike the usual Intel system (Least Significant Byte first).

For example, if you had a value 0x12345678 in hexadecimal, using the Intel notation it would be written as 78 56 34 12, while using Motorola's 12 34 56 78.

NOTE: Some chunk IDs start with a NULL byte (0x00) because of reasons that will become apparent later. You should just skip this byte and assume the next 4 letters hold the chunk ID.

Following the chunk header is the chunk data.

Typical VQA File

Here is a scheme of a typical VQA file (nested chunks are indented):

  FINF       <-  Frame data positions
  SND?    \  <-  First sound chunk, contains 1/2 second of sound
  SND?     |     <- Contains 1 frame's worth of sound
  VQFR     |     <- Contains various video data chunks
    CBF?   | 1st frame data
    CBP?   |
    CPL?   |
    VPT?  /
  SND?    \
  VQFR     | 2nd frame data
    CBP?   |
    VPT?  /
  SND?    \
  VQFR     | 3rd frame data
    CBP?   |
    VPT?  /
. . .

NOTE: There can also be some other chunks (i.e. PINF, PINH, SN2J) included, but they are not relevant (?!) for viewing the movie, so they can easily be skipped.

FORM chunk

This chunk is the main chunk, containing all other chunks. In case of version 2 and 3 movies, its size is actually the size of the entire file minus the size of the chunk header (8 bytes). Version 1 movies seem to have this set to the length of the header VQHD + FINF chunk.

Immediately after the chunk's header, a 4-character signature, "WVQA" is located. Then come all the other chunks.

VQHD chunk ("VQa HeaDer" ???)

This is the header chunk, containing vital information about the movie. Its size is always 42 bytes. The information is structured like this:

struct VQAHeader
 short  Version;       /* VQA version number                         */
 short  Flags;         /* VQA flags                                  */
 short  NumFrames;     /* Number of frames                           */
 short  Width;         /* Movie width (pixels)                       */
 short  Height;        /* Movie height (pixels)                      */
 char   BlockW;        /* Width of each image block (pixels)         */
 char   BlockH;        /* Height of each image block (pixels)        */
 char   FrameRate;     /* Frame rate of the VQA                      */
 char   CBParts;       /* How many images use the same lookup table  */
 short  Colors;        /* Maximum number of colors used in VQA       */
 short  MaxBlocks;     /* Maximum number of image blocks             */
 long   Unknown1;      /* Always 0 ???                               */
 short  Unknown2;      /* Some kind of size ???                      */
 short  Freq;          /* Sound sampling frequency                   */
 char   Channels;      /* Number of sound channels                   */
 char   Bits;          /* Sound resolution                           */
 long   Unknown3;      /* Always 0 ???                               */
 short  Unknown4;      /* 0 in old VQAs, 4 in HiColor ones ???       */
 long   MaxCBFZSize;   /* 0 in old VQAs, max. CBFZ size in HiColor   */
 long   Unknown5;      /* Always 0 ???                               */

Version denotes the VQA version. Valid values are:

  • 1 - First VQAs, used only in Legend of Kyrandia III.
  • 2 - Used in C&C, Red Alert, Lands of Lore II, Dune 2000.
  • 3 - Lands of Lore III (?), Blade Runner (?), Nox and Tiberian Sun. These VQAs are HiColor (15 bit).

Flags most probably contain some flags. I only know that bit 0 (LSB)

  • denotes whether the VQA has a soundtrack or not.
  • (1 = Has sound, 0 = No sound)

Width is usually 320, Height is usually 156 or 200 although one movie in

  • Red Alert is 640x400 in size (the start movie for the Win95 version).

Each frame of a VQA movie is comprised of a series of blocks that are BlockW pixels in width and BlockH pixels in height. Imagine the frame is a mosaic with the blocks being 'pieces' that make up the frame.

BlockW is the width and BlockH is the height of each screen block. In VQAs version 2 (and perhaps 1) the blocks are usually 4x2.

FrameRate is always 15 in C&C and RA and seems to be 10 in LoK III.

CBParts denotes how many frames use the same lookup table. It also implies how many parts the new block lookup table is split into. In C&C and RA it is always 8.

Colors indicates the maximum number of colors used by the VQA. The HiColor VQAs have this set to 0, while the old movies have 256 or less in here.

Freq is usually 22050 Hz. Note that version 1 movies can have this set to 0 Hz. In that case, you should use 22050 Hz.

Channels specifies the number of sound channels, i.e. is the sound mono or stereo. Channels=1 -> sound is mono, Channels=2 -> stereo. C&C and RA almost always use mono sound. Version 1 can have this set to 0, but you should use 1 (mono sound). The majority of Tiberian Sun movies use stereo sound instead.

  • Bits indicates whether the sound is 8 bit or 16 bit.

Bits=8 -> 8 bit sound, Bits=16 -> 16 bit sound (surprise! :). The Legend of Kyrandia III: Malcolm's Revenge uses 8 bits where C&C, RA, TS, Dune 2000, Lands of Lore III use 16 bits. Note, again, that version 1 of the VQAs can have this set to 0 in which case 8 bits are assumed.

MaxCBFZSize is a new entry, specific to the HiColor VQAs. It tells you the size of the largest CBFZ chunk, in bytes.

Following the chunk data are the sub-chunks.

FINF chunk ("Frame INFormation" ???)

This chunk contains the positions (absolute from the start of the VQA) of data for every frame. That means that it points to the SND? chunk associated with that frame, which is followed by a VQFR chunk containing frame's video data.

The positions are given as LONGs which are in normal Intel byte order.

NOTE: Some frame positions are 0x40000000 too large. This is a flag indicating those frames contain a new color palette. To get the actual positions, you should subtract 0x40000000 from the values.

NOTE #2: To get the actual position of the frame data you have to multiply the value by 2. This is why some chunk IDs start with 0x00. Since you multiply by 2, you can't get an odd chunk position so if the chunk position would normally be odd, a 0x00 is inserted to make it even.

SND? chunk ("SouND" ???)

These chunks contain the sound data for the movie. The last byte of the ID can be either '0', '1' or '2' so the actual IDs would be "SND0", "SND1" and "SND2".

In VQAs version 1 (Legend of Kyrandia 3) there are NumFrames sound chunks. Old 8 bit VQA movies of version 2 have NumFrames+1 sound chunks. Note, however, that Dune2000 movies, which are also version 2, but HiColor have NumFrames sound chunks, instead. Version 3 movies also have NumFrames sound chunks.

In case of the old, 8 bit VQAs, the first chunk contains half a second (ver. 2) or more (ver. 1) of the wave data, in all (?) the HiColor movies the first chunk contains exactly the amount of sound required for one frame of the movie. The downside is this requires a somewhat more advanced buffering technique on the side of the player in order to allow smooth playback.

SND0 chunk

This one contains the raw 8 or 16 bit PCM wave data. If the data is 8 bit, the sound is unsigned and if it is 16 bit, the samples are signed.

SND1 chunk

It contains 8 bit sound compressed using Westwood's own proprietary ADPCM algorithm. The chunk has a 4 byte header:

 struct SND1Header
  short OutSize;
  short Size;

These values are needed for the decoding algoritm (see APPENDIX C). The encoded samples follow immediately after the header. It's important to know this algorithm produces UNSIGNED sound, unlike the IMA ADPCM algorithm supplied here (see below). It is, however very simple to adapt both algorithms to produce either signed or unsigned sample output...

SND2 chunk

It contains the 16 bit sound data compressed using the IMA ADPCM algorithm which compresses 16 bits into 4. That's why the SND2 chunks are 4 times smaller than SND0 chunks and they are used almost all the time. For the description of the algorithm, see later in the document.

Different VQA versions have different stereo sample layout. In case of Tiberian Sun stereo sound is encoded the same way as mono except the SND2 chunk is split into two halfs. The first half contains the left channel sound and the second half contains the right channel. The layout of nibbles is as follows: LL LL LL LL LL ... RR RR RR RR RR. Old movies (C&C, RA) use a different layout. Here, the nibbles are packed together like this: LL RR LL RR LL RR ... This means that two left channel samples are packed into one byte and then two right channel samples are packed into another.

Naturally, the size of a stereo SND2 chunk is exactly twice as big as a mono SND2.

It is important to note that, when decoding, you have to keep separate values of Index and Cur_Sample for each channel (see APPENDIX B).

VQFR chunk ("Vector Quantized FRame" ???)

A chunk that includes many nested sub-chunks which contain video data. It doesn't contain any data itself so the sub-chunks follow immediately after the VQFR chunk header. All following sub-chunks are nested inside a VQFR chunk. They can all contain '0' or 'Z' as the last byte of their ID.

  • If the last byte is '0' it means that the chunk data is uncompressed.
  • If the last byte is 'Z' it means that the data is compressed using

Format80 compression. You can find its description in APPENDIX A.

CBF? chunk ("CodeBook, Full" ???)

Lookup table containing the screen block data as an array of elements that each are BlockW*BlockH bytes long. It is always located in the data for the first frame. In Vector Quantization terminology these tables are called CODEBOOKS.

There can be max. 0x0f00 of these elements (blocks) at any one time in normal VQAs and 0xff00 in the hi-res VQAs (Red Alert 95 start movie) although I seriously doubt that so many blocks (0xff00 = 65280 blocks) would ever be used.

The uncompressed version of these chunks ("CBF0") is used mainly in the original Command & Conquer, while the compressed version ("CBFZ") is used in C&C: Red Alert.

CBP? chunk ("CodeBook Part" ???)

Like CBF?, but it contains a part of the lookup table, so to get the new complete table you need to append CBParts of these in frame order. Once you get the complete table and display the current frame, replace the old table with the new one. As in CBF? chunk, the uncompressed chunks are used in C&C and the compressed chunks are used in Red Alert.

NOTE: If the chunks are CBFZ, first you need to append CBParts of them and then decompress the data, NOT decompress each chunk individually.

CPL? chunk ("Color PaLette" ???)

The simplest one of all... Contains a palette for the VQA. It is an array of red, green and blue values (in that order, all have a size of 1 byte). Seems that the values range from 0-255, but you should mask out the bits 6 and 7 to get the correct palette (VGA hardware uses only bits 0..5 anyway).

VPT? chunk: ("Vector Pointer Table" ???)

This chunk contains the indexes into the block lookup table which contains the data to display the frame. The image blocks are called VECTORS in Vector Quantization terminology.

These chunks are always compressed, but I guess the uncompressed ones can also be used (although this would lower the overall compression achieved).

The size of this index table is (Width/BlockW)*(Height/BlockH)*2 bytes.

Now, there is a catch: version 2 VQAs use a different layout of the index table than version 1 VQAs. I will first describe the version 2 table format, as it's more common.


The index table is an array of CHARs and is split into 2 parts - the top half and the bottom half.

Now, if you want to diplay the block at coordinates (in block units), say (bx,by) you should read two bytes from the table, one from the top and one from the bottom half:


If HiVal=0x0f (0xff for the start movie of Red Alert 95) you should simply fill the block with color LoVal, otherwise you should copy the block with index number HiVal*256+LoVal from the lookup table.

Do that for every block on the screen (remember, there are Width/BlockW blocks in the horizontal direction and Height/BlockH blocks in the vertical direction) and you've decoded your first frame!

NOTE: I was unable to find an entry in the VQA header which determines whether 0x0f or 0xff is used in HiVal to signal that the block is of uniform color. I assume that the Wy entry of the header implies this: If BlockW=2 -> 0x0f is used, if BlockH=4 -> 0xff is used.


Here, the index table is simply an array of SHORTs written in normal, Intel byte order.

The LoVal and HiVal are given as:


If HiVal=0xff, the block is of uniform color which is (255-LoVal). Otherwise, write the block with index number (HiVal*256+LoVal)/8.

Appendix A

FORMAT80 COMPRESSION METHOD by Vladan Bato (bat22@geocities.com)

There are several different commands, with different sizes : from 1 to 5 bytes. The positions mentioned below always refer to the destination buffer (i.e. the uncompressed image). The relative positions are relative to the current position in the destination buffer, which is one byte beyond the last written byte.

I will give some sample code at the end.

(1) 1 byte

     | 1 | 0 |   |   |   |   |   |   |
     This one means : copy next Count bytes as is from Source to Dest.

(2) 2 bytes

 +---+---+---+---+---+---+---+---+   +---+---+---+---+---+---+---+---+
 | 0 |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
 +---+---+---+---+---+---+---+---+   +---+---+---+---+---+---+---+---+
           |                             |
        Count-3                    Relative Pos.
 This means copy Count bytes from Dest at Current Pos.-Rel. Pos. to
 Current position.
 Note that you have to add 3 to the number you find in the bits 4-6 of the
 first byte to obtain the Count.
 Note that if the Rel. Pos. is 1, that means repeat Count times the previous

(3) 3 bytes

 +---+---+---+---+---+---+---+---+   +---------------+---------------+
 | 1 | 1 |   |   |   |   |   |   |   |               |               |
 +---+---+---+---+---+---+---+---+   +---------------+---------------+
         \_______________________/                  Pos
 Copy Count bytes from Pos, where Pos is absolute from the start of the
 destination buffer. (Pos is a word, that means that the images can't be
 larger than 64K)

(4) 4 bytes

 +---+---+---+---+---+---+---+---+   +-------+-------+  +-------+
 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |   |       |       |  |       |
 +---+---+---+---+---+---+---+---+   +-------+-------+  +-------+
                                           Count          Color
 Write Color Count times.
 (Count is a word, color is a byte)

(5) 5 bytes

 +---+---+---+---+---+---+---+---+   +-------+-------+  +-------+-------+
 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |   |       |       |  |       |       |
 +---+---+---+---+---+---+---+---+   +-------+-------+  +-------+-------+
                                           Count               Pos
 Copy Count bytes from Dest. starting at Pos. Pos is absolute from the start
 of the Destination buffer.
 Both Count and Pos are words.

These are all the commands I found out. Maybe there are other ones, but I haven't seen them yet.

All the images end with a 80h command.

To make things more clearer here's a piece of code that will uncompress the image.

 DP = destination pointer
 SP = source pointer
 Source and Dest are the two buffers

   b7:=Com shr 7;  {b7 is bit 7 of Com}
   case b7 of
     0 : begin  {copy command (2)}
           {Count is bits 4-6 + 3}
           Count:=(Com and $7F) shr 4 + 3;
           {Position is bits 0-3, with bits 0-7 of next byte}
           Posit:=(Com and $0F) shl 8+Source[SP];
           {Starting pos=Cur pos. - calculated value}
           for i:=Posit to Posit+Count-1 do
     1 : begin
           {Check bit 6 of Com}
           b6:=(Com and $40) shr 6;
           case b6 of
             0 : begin  {Copy as is command (1)}
                   Count:=Com and $3F;  {mask 2 topmost bits}
                   if Count=0 then break; {EOF marker}
                   for i:=1 to Count do
             1 : begin  {large copy, very large copy and fill commands}
                   {Count = (bits 0-5 of Com) +3}
                   {if Com=FEh then fill, if Com=FFh then very large copy}
                   Count:=Com and $3F;
                   if Count<$3E then {large copy (3)}
                     {Next word = pos. from start of image}
                     for i:=Posit to Posit+Count-1 do
                   else if Count=$3F then   {very large copy (5)}
                     {next 2 words are Count and Pos}
                     for i:=Posit to Posit+Count-1 do
                   end else
                   begin   {Count=$3E, fill (4)}
                     {Next word is count, the byte after is color}
                     for i:=0 to Count-1 do
 until false;

Note that you won't be able to compile this code, because the typecasting won't work. (But I'm sure you'll be able to fix it).

Appendix B

IMA ADPCM DECOMPRESSION by Vladan Bato (bat22@geocities.com) http://www.geocities.com/SiliconValley/8682

Note that the current sample value and index into the Step Table should be initialized to 0 at the start and are maintained across the chunks (see below).


It is the exact opposite of the above. It receives 4-bit codes in input and produce 16-bit samples in output.

Again you have to mantain an Index into the Step Table an the current sample value.

The tables used are the same as for compression.

Here's the code :

 while there_is_more_data do
   if (Code and $8) <> 0 then Sb:=1 else Sb:=0;
   Code:=Code and $7;
   {Separate the sign bit from the rest}
   Delta:=(Step_Table[Index]*Code) div 4 + Step_Table[Index] div 8;
   {The last one is to minimize errors}
   if Sb=1 then Delta:=-Delta;
   if Cur_Sample>32767 then Cur_Sample:=32767
   else if Cur_Sample<-32768 then Cur_Sample:=-32768;
   if Index<0 then Index:=0;
   if Index>88 the Index:=88;

Again, this can be done more efficiently (no need for multiplication).

The Get_Next_Code function should return the next 4-bit code. It must extract it from the input buffer (note that two 4-bit codes are stored in the same byte, the first one in the lower bits).

The Output_Sample function should write the signed 16-bit sample to the output buffer.


 Index_Adjust : array [0..7] of integer = (-1,-1,-1,-1,2,4,6,8);


 Steps_Table : array [0..88] of integer =(
       7,     8,     9,     10,    11,    12,     13,    14,    16,
       17,    19,    21,    23,    25,    28,     31,    34,    37,
       41,    45,    50,    55,    60,    66,     73,    80,    88,
       97,    107,   118,   130,   143,   157,    173,   190,   209,
       230,   253,   279,   307,   337,   371,    408,   449,   494,
       544,   598,   658,   724,   796,   876,    963,   1060,  1166,
       1282,  1411,  1552,  1707,  1878,  2066,   2272,  2499,  2749,
       3024,  3327,  3660,  4026,  4428,  4871,   5358,  5894,  6484,
       7132,  7845,  8630,  9493,  10442, 11487,  12635, 13899, 15289,
       16818, 18500, 20350, 22385, 24623, 27086,  29794, 32767 );

Appendix C

WESTWOOD STUDIOS' ADPCM DECOMPRESSION by Asatur V. Nazarian (samael@avn.mccme.ru)

WS ADPCM Decompression Algorithm

Each SND1 chunk may be decompressed independently of others. This lets you implement seeking/skipping for WS ADPCM sounds (unlike IMA ADPCM ones). But during the decompression of the given chunk a variable (CurSample) should be maintained for this whole chunk:

SHORT CurSample;
BYTE  InputBuffer[InputBufferSize]; // input buffer containing the whole chunk
WORD  wSize, wOutSize; // Size and OutSize values from this chunk's header
BYTE  code;
CHAR  count; // this is a signed char!
WORD  i; // index into InputBuffer
WORD  input; // shifted input

if (wSize==wOutSize) // such chunks are NOT compressed
 for (i=0;i<wOutSize;i++)
     Output(InputBuffer[i]); // send to output stream
 return; // chunk is done!

// otherwise we need to decompress chunk

CurSample=0x80; // unsigned 8-bit

// note that wOutSize value is crucial for decompression!

while (wOutSize>0) // until wOutSize is exhausted!
 switch (code) // parse code
   case 2: // no compression...

if (count & 0x20) { count<<=3; // here it's significant that (count) is signed: CurSample+=count>>3; // the sign bit will be copied by these shifts!


wOutSize--; // one byte added to output } else // copy (count+1) bytes from input to output { for (count++;count>0;count--,wOutSize--,i++) Output(InputBuffer[i]); CurSample=InputBuffer[i-1]; // set (CurSample) to the last byte sent to output } break;

   case 1: // ADPCM 8-bit -> 4-bit

for (count++;count>0;count--) // decode (count+1) bytes { code=InputBuffer[i++];

CurSample+=WSTable4bit[(code & 0x0F)]; // lower nibble

CurSample=Clip8BitSample(CurSample); Output((BYTE)CurSample);

CurSample+=WSTable4bit[(code >> 4)]; // higher nibble

CurSample=Clip8BitSample(CurSample); Output((BYTE)CurSample);

wOutSize-=2; // two bytes added to output } break;

   case 0: // ADPCM 8-bit -> 2-bit

for (count++;count>0;count--) // decode (count+1) bytes { code=InputBuffer[i++];

CurSample+=WSTable2bit[(code & 0x03)]; // lower 2 bits

CurSample=Clip8BitSample(CurSample); Output((BYTE)CurSample);

CurSample+=WSTable2bit[((code>>2) & 0x03)]; // lower middle 2 bits

CurSample=Clip8BitSample(CurSample); Output((BYTE)CurSample);

CurSample+=WSTable2bit[((code>>4) & 0x03)]; // higher middle 2 bits

CurSample=Clip8BitSample(CurSample); Output((BYTE)CurSample);

CurSample+=WSTable2bit[((code>>6) & 0x03)]; // higher 2 bits

CurSample=Clip8BitSample(CurSample); Output((BYTE)CurSample);

wOutSize-=4; // 4 bytes sent to output } break;

   default: // just copy (CurSample) (count+1) times to output

for (count++;count>0;count--,wOutSize--) Output((BYTE)CurSample);


HIBYTE and LOBYTE are just higher and lower bytes of WORD:

#define HIBYTE(word) ((word) >> 8)
#define LOBYTE(word) ((word) & 0xFF)

Note that depending on your compiler you may need to use additional byte separation in these defines, e.g. (((byte) >> 8) & 0xFF). The same holds for 4-bit and 2-bit nibble separation in the code above.

WSTable4bit and WSTable2bit are the delta tables given in the next section.

Output() is just a placeholder for any action you would like to perform for decompressed sample value.

Clip8BitSample is quite evident:

SHORT Clip8BitSample(SHORT sample)
 if (sample>255)
    return 255;
 else if (sample<0)
    return 0;
    return sample;

This algorithm is ONLY for mono 8-bit unsigned sound, as I've never seen any other sound format used with WS ADPCM compression.

Of course, the decompression routine described above may be greatly optimized.


CHAR WSTable2bit[]=

CHAR WSTable4bit[]=
   -9, -8, -6, -5, -4, -3, -2, -1,
    0,  1,  2,  3,  4,  5,  6,  8


by Gordan Ugarkovic (ugordan@yahoo.com) http://members.xoom.com/ugordan

This document describes how to view Westwood's HiColor VQA movies. These are version 3 movies, but there are at least some version 2 VQAs that are in this format. I will not describe the whole VQA layout here, I will just explain how to display the VIDEO stream of the VQA, that is, how to decompress the CBFZ and VPTR/VPRZ chunks.

First a little warning: I'm not sure which flag denotes the VQA is 8 bit or 15 bit. I'm pretty convinced it's either bit 4 (0x10) or bit 2 (0x04) of the Flags entry (see my VQA_INFO.TXT) in the header. Another way would be to check the Colors entry in the header, if it is 0 it could imply a HiColor movie.

There's a major difference between the old (8 bit, 256 color) and the new, HiColor VQAs. Lookup tables are no longer split up into several CBP? chunks, instead they come in one piece (a CBFZ chunk). Two lookup tables can now be many frames apart, not just 8 (as usual). This is indicated by the CBParts entry of the header (see VQA_INFO.TXT), which is set to 0. Subsequent frames use the last lookup table loaded, of course.

Another thing: It appears the first CBFZ chunk comes inside the VQFR chunk but the other ones seem to be located inside their own chunks, called VQFL, which are followed by the usual VQFR chunks (containing VPTR/VPRZ chunks).

Also, the movies are 15 bit, NOT 16 bit. There is a difference because in 16 bit color depth there are 6 bits for the green channel, but the VQAs use 5.

The CBFZ chunks

These are a bit modified since the 8 bit VQAs. If the first byte of the chunk is not NULL (0x00), it means the chunk is compressed using the standard Format80 algorithm (see Vladan Bato's text on C&C file formats), starting from that byte. If the first byte is NULL, the chunk is compressed using a modified version of Format80 (see below), starting from the next byte of the chunk. The original Format80 algorithm is used when the amount of data to be compressed is less than 64 KB, otherwise the 'new' algorithm is used.

When decompressed properly, a CBFZ chunk expands into 15 bit pixels packed as shorts in normal Intel byte order. The red, green and blue values are packed like this:

 15      bit      0
  arrrrrgg gggbbbbb
  HI byte  LO byte

The r,g,b values make up a pixel and they can range from 0-31. As in the old CBFZ chunks, these pixels make up the block lookup table (also called a codebook). The a (alpha) value is used in Blade Runner in overlay videos to indicate a transparent pixel.

The VPTR chunks

These chunks use some sort of differential, run-length algorithm that only records changes from the previous frame. Therefore, the previous frame bitmap must be maintained throughout all the frames (you could just draw the blocks that changed, though). This makes dropping frames (in case of bad performance) impossible.

When decoding, you take a short int (Intel) from the chunk and examine its 3 most significant bits (bits 15,14,13). These bits make up a code prefix that determines which action is to be done.

Here's a list of the prefixes I encountered and their description (Val is the short int value):

  000 - Skip Count blocks. Count is (Val & 0x1fff).
  001 - Write block number (Val & 0xff) Count times.
        Count is (((Val/256) & 0x1f)+1)*2. Note that this can only
        index the first 256 blocks.
  010 - Write block number (Val & 0xff) and then write Count blocks
        getting their indexes by reading next Count bytes from
        the VPTR chunk. Count is (((Val/256) & 0x1f)+1)*2.
        Again, the block numbers range from 0-255.
  011 - Write block (Val & 0x1fff).
  100 - Same as 011 but skip pixels with alpha bit set.
  101 - Write block (Val & 0x1fff) Count times. Count is the next
        byte from the VPTR chunk.
  110 - Same as 101 but skip pixels with alpha bit set.

After this, you take the next short int and repeat the above process.

Prefixes 100 and 110 are used in Blade Runner for overlay videos.

Every row of blocks is processed individually of others. When you encounter the end of a row, proceed to the next row (blocks are processed left to right, top to down). Repeat this process until all blocks in the frame are covered and that's it!

Note that the above implies an absolute maximum of 8192 blocks (0x1fff+1).

As for the VPRZ chunks, these are just VPTR chunks compressed with the standard Format80 algorithm.

The modified Format80 scheme

This is really only a small modification of the basic algorithm. The only commands that are modified are commands (3) and (5) See Vladan's text). Instead of using offsets ABSOLUTE from the start of the destination buffer, offsets RELATIVE to the current destination pointer are used. If you ask me, I don't see why this approach wasn't used in the first place as it would suffer no disadvantage with the old files and it would be much easier to compress even larger amounts of data. The guys at WW were just careless, I guess... :-)

Anyway, in Vladan's algorithm, there is a line in command (3) that says:


it should say:


Likewise, for command (5):


it should be:


Games Using VQA

Version 1

Versions 2

Versions 3

Other Documentation

http://www.gamers.org/pub/idgames2/planetquake/planetcnc/cncdz/ has lots of data about this format. See vqa_overview.zip and vqafilesguild.zip. Also there is a decoder (vqatoavi) that decodes the dune2000 sample.